Compare commits
55 Commits
enforce-se
...
no-mmio
| Author | SHA1 | Date | |
|---|---|---|---|
| abcf6b8256 | |||
| ca6dae1744 | |||
| b6acebcb11 | |||
| ba702043f0 | |||
| 2a4c60c1dd | |||
| a1532f813b | |||
| 26d5727b19 | |||
| 680e132318 | |||
| 90a5d989e7 | |||
| de40153fa4 | |||
| e9936e01c2 | |||
| e28c2a390c | |||
| 63d1830429 | |||
| 88cc6acb4d | |||
| 3b31720c4d | |||
| f9bbae81aa | |||
| 1c793deece | |||
| d5b50a9fc0 | |||
| 168f168c33 | |||
| 312068eebf | |||
| 5118798c30 | |||
| 831c9c4a38 | |||
| 23ca10472a | |||
| 6f05dd9d1d | |||
| 19cc8e626b | |||
| ceeda05798 | |||
| 222b1cc0d7 | |||
| b74c91457e | |||
| 1c75d895fc | |||
| 271218b733 | |||
| 80213bf88f | |||
| fa08df21a5 | |||
| 80ac6c03b5 | |||
| d4f31bc617 | |||
| 13d02c14e0 | |||
| 84f9e9a10f | |||
| fcc0fd671a | |||
| ee372572a6 | |||
| 7930e1ea86 | |||
| 4dd6e12e46 | |||
| 1d4d421097 | |||
| 3f4b57635e | |||
| 86566f1c14 | |||
| 3f1f0aa7c2 | |||
| 8fe147b8f9 | |||
| 5193a5d222 | |||
| 1f6393e7d5 | |||
| 9b2a5926a6 | |||
| e13735b624 | |||
| 3218af38d0 | |||
| 1cf64ffaef | |||
| 998b9a9525 | |||
| 27bad3a699 | |||
| 1570aeffcb | |||
| 09ee80f590 |
@ -1,10 +1,5 @@
|
|||||||
#!/bin/bash -ex
|
#!/bin/bash -ex
|
||||||
|
|
||||||
# TODO: Work around pip install issues with Python 3.12 in the GitHub runner image.
|
|
||||||
# See: https://github.com/actions/runner-images/issues/8709
|
|
||||||
PYTHON_PATH=$(brew --prefix python@3.11)
|
|
||||||
export PATH=$PYTHON_PATH/bin:$PYTHON_PATH/libexec/bin:$PATH
|
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake .. -GNinja \
|
cmake .. -GNinja \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
#!/bin/bash -ex
|
#!/bin/bash -ex
|
||||||
|
|
||||||
# TODO: Work around pip install issues with Python 3.12 in the GitHub runner image.
|
|
||||||
# See: https://github.com/actions/runner-images/issues/8709
|
|
||||||
PYTHON_PATH=$(brew --prefix python@3.11)
|
|
||||||
export PATH=$PYTHON_PATH/bin:$PYTHON_PATH/libexec/bin:$PATH
|
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake .. -GNinja \
|
cmake .. -GNinja \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
|||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: linux
|
OS: linux
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
@ -66,7 +66,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: macos
|
OS: macos
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
@ -133,7 +133,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: windows
|
OS: windows
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
@ -188,7 +188,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: android
|
OS: android
|
||||||
TARGET: universal
|
TARGET: universal
|
||||||
steps:
|
steps:
|
||||||
@ -239,7 +239,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
CCACHE_SLOPPINESS: time_macros
|
||||||
OS: ios
|
OS: ios
|
||||||
TARGET: arm64
|
TARGET: arm64
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -79,9 +79,15 @@
|
|||||||
[submodule "sirit"]
|
[submodule "sirit"]
|
||||||
path = externals/sirit
|
path = externals/sirit
|
||||||
url = https://github.com/yuzu-emu/sirit
|
url = https://github.com/yuzu-emu/sirit
|
||||||
|
[submodule "faad2"]
|
||||||
|
path = externals/faad2/faad2
|
||||||
|
url = https://github.com/knik0/faad2
|
||||||
[submodule "library-headers"]
|
[submodule "library-headers"]
|
||||||
path = externals/library-headers
|
path = externals/library-headers
|
||||||
url = https://github.com/citra-emu/ext-library-headers.git
|
url = https://github.com/citra-emu/ext-library-headers.git
|
||||||
[submodule "libadrenotools"]
|
[submodule "libadrenotools"]
|
||||||
path = externals/libadrenotools
|
path = externals/libadrenotools
|
||||||
url = https://github.com/bylaws/libadrenotools
|
url = https://github.com/bylaws/libadrenotools
|
||||||
|
[submodule "oaknut"]
|
||||||
|
path = externals/oaknut
|
||||||
|
url = https://github.com/merryhime/oaknut.git
|
||||||
|
|||||||
@ -81,9 +81,6 @@ CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support
|
|||||||
|
|
||||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||||
|
|
||||||
CMAKE_DEPENDENT_OPTION(ENABLE_MF "Use Media Foundation decoder (preferred over FFmpeg)" ON "WIN32" OFF)
|
|
||||||
CMAKE_DEPENDENT_OPTION(ENABLE_AUDIOTOOLBOX "Use AudioToolbox decoder (preferred over FFmpeg)" ON "APPLE" OFF)
|
|
||||||
|
|
||||||
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
|
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
|
||||||
|
|
||||||
# Compile options
|
# Compile options
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
|
||||||
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||||
# Params:
|
# Params:
|
||||||
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
||||||
@ -52,28 +54,38 @@ function(download_qt target)
|
|||||||
get_external_prefix(qt base_path)
|
get_external_prefix(qt base_path)
|
||||||
file(MAKE_DIRECTORY "${base_path}")
|
file(MAKE_DIRECTORY "${base_path}")
|
||||||
|
|
||||||
|
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
||||||
if (DOWNLOAD_QT_TOOL)
|
if (DOWNLOAD_QT_TOOL)
|
||||||
set(prefix "${base_path}/Tools")
|
set(prefix "${base_path}/Tools")
|
||||||
set(install_args install-tool --outputdir ${base_path} ${host} desktop ${target})
|
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||||
else()
|
else()
|
||||||
set(prefix "${base_path}/${target}/${arch_path}")
|
set(prefix "${base_path}/${target}/${arch_path}")
|
||||||
if (host_arch_path)
|
if (host_arch_path)
|
||||||
set(host_flag "--autodesktop")
|
set(host_flag "--autodesktop")
|
||||||
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
||||||
endif()
|
endif()
|
||||||
set(install_args install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
||||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT EXISTS "${prefix}")
|
if (NOT EXISTS "${prefix}")
|
||||||
message(STATUS "Downloading binaries for Qt...")
|
message(STATUS "Downloading binaries for Qt...")
|
||||||
|
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(aqt_path "${base_path}/aqt.exe")
|
set(aqt_path "${base_path}/aqt.exe")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
https://github.com/miurahr/aqtinstall/releases/download/v3.1.7/aqt.exe
|
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
||||||
${aqt_path} SHOW_PROGRESS)
|
${aqt_path} SHOW_PROGRESS)
|
||||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
|
elseif (APPLE)
|
||||||
|
set(aqt_path "${base_path}/aqt-macos")
|
||||||
|
file(DOWNLOAD
|
||||||
|
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
||||||
|
${aqt_path} SHOW_PROGRESS)
|
||||||
|
execute_process(COMMAND chmod +x ${aqt_path})
|
||||||
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
|
WORKING_DIRECTORY ${base_path})
|
||||||
else()
|
else()
|
||||||
# aqt does not offer binary releases for other platforms, so download and run from pip.
|
# aqt does not offer binary releases for other platforms, so download and run from pip.
|
||||||
set(aqt_install_path "${base_path}/aqt")
|
set(aqt_install_path "${base_path}/aqt")
|
||||||
|
|||||||
@ -10,16 +10,20 @@ set(HASH_FILES
|
|||||||
"${VIDEO_CORE}/renderer_opengl/gl_shader_util.h"
|
"${VIDEO_CORE}/renderer_opengl/gl_shader_util.h"
|
||||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp"
|
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp"
|
||||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h"
|
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h"
|
||||||
|
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp"
|
||||||
|
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.h"
|
||||||
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp"
|
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp"
|
||||||
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.h"
|
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.h"
|
||||||
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.cpp"
|
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.cpp"
|
||||||
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.h"
|
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.h"
|
||||||
|
"${VIDEO_CORE}/shader/generator/pica_fs_config.cpp"
|
||||||
|
"${VIDEO_CORE}/shader/generator/pica_fs_config.h"
|
||||||
"${VIDEO_CORE}/shader/generator/shader_gen.cpp"
|
"${VIDEO_CORE}/shader/generator/shader_gen.cpp"
|
||||||
"${VIDEO_CORE}/shader/generator/shader_gen.h"
|
"${VIDEO_CORE}/shader/generator/shader_gen.h"
|
||||||
"${VIDEO_CORE}/shader/generator/shader_uniforms.cpp"
|
"${VIDEO_CORE}/shader/generator/shader_uniforms.cpp"
|
||||||
"${VIDEO_CORE}/shader/generator/shader_uniforms.h"
|
"${VIDEO_CORE}/shader/generator/shader_uniforms.h"
|
||||||
"${VIDEO_CORE}/shader/generator/spv_shader_gen.cpp"
|
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.cpp"
|
||||||
"${VIDEO_CORE}/shader/generator/spv_shader_gen.h"
|
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
||||||
"${VIDEO_CORE}/shader/shader.cpp"
|
"${VIDEO_CORE}/shader/shader.cpp"
|
||||||
"${VIDEO_CORE}/shader/shader.h"
|
"${VIDEO_CORE}/shader/shader.h"
|
||||||
"${VIDEO_CORE}/pica.cpp"
|
"${VIDEO_CORE}/pica.cpp"
|
||||||
|
|||||||
29
CMakeModules/aqt_config.ini
Normal file
29
CMakeModules/aqt_config.ini
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[aqt]
|
||||||
|
concurrency: 2
|
||||||
|
|
||||||
|
[mirrors]
|
||||||
|
trusted_mirrors:
|
||||||
|
https://download.qt.io
|
||||||
|
blacklist:
|
||||||
|
https://qt.mirror.constant.com
|
||||||
|
https://mirrors.ocf.berkeley.edu
|
||||||
|
https://mirrors.ustc.edu.cn
|
||||||
|
https://mirrors.tuna.tsinghua.edu.cn
|
||||||
|
https://mirrors.geekpie.club
|
||||||
|
https://mirrors-wan.geekpie.club
|
||||||
|
https://mirrors.sjtug.sjtu.edu.cn
|
||||||
|
fallbacks:
|
||||||
|
https://qtproject.mirror.liquidtelecom.com/
|
||||||
|
https://mirrors.aliyun.com/qt/
|
||||||
|
https://ftp.jaist.ac.jp/pub/qtproject/
|
||||||
|
https://ftp.yz.yamagata-u.ac.jp/pub/qtproject/
|
||||||
|
https://qt-mirror.dannhauer.de/
|
||||||
|
https://ftp.fau.de/qtproject/
|
||||||
|
https://mirror.netcologne.de/qtproject/
|
||||||
|
https://mirrors.dotsrc.org/qtproject/
|
||||||
|
https://www.nic.funet.fi/pub/mirrors/download.qt-project.org/
|
||||||
|
https://master.qt.io/
|
||||||
|
https://mirrors.ukfast.co.uk/sites/qt.io/
|
||||||
|
https://ftp2.nluug.nl/languages/qt/
|
||||||
|
https://ftp1.nluug.nl/languages/qt/
|
||||||
|
|
||||||
90
externals/CMakeLists.txt
vendored
90
externals/CMakeLists.txt
vendored
@ -41,9 +41,15 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Catch2
|
# Catch2
|
||||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
add_library(catch2 INTERFACE)
|
||||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
if(USE_SYSTEM_CATCH2)
|
||||||
add_subdirectory(catch2)
|
find_package(Catch2 3.0.0 REQUIRED)
|
||||||
|
else()
|
||||||
|
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||||
|
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||||
|
add_subdirectory(catch2)
|
||||||
|
endif()
|
||||||
|
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
|
||||||
|
|
||||||
# Crypto++
|
# Crypto++
|
||||||
if(USE_SYSTEM_CRYPTOPP)
|
if(USE_SYSTEM_CRYPTOPP)
|
||||||
@ -85,6 +91,11 @@ if ("x86_64" IN_LIST ARCHITECTURE)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Oaknut
|
||||||
|
if ("arm64" IN_LIST ARCHITECTURE)
|
||||||
|
add_subdirectory(oaknut EXCLUDE_FROM_ALL)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Dynarmic
|
# Dynarmic
|
||||||
if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
|
if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
|
||||||
if(USE_SYSTEM_DYNARMIC)
|
if(USE_SYSTEM_DYNARMIC)
|
||||||
@ -156,24 +167,12 @@ endif()
|
|||||||
# Open Source Archives
|
# Open Source Archives
|
||||||
add_subdirectory(open_source_archives)
|
add_subdirectory(open_source_archives)
|
||||||
|
|
||||||
|
# faad2
|
||||||
|
add_subdirectory(faad2 EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# Dynamic library headers
|
# Dynamic library headers
|
||||||
add_library(library-headers INTERFACE)
|
add_library(library-headers INTERFACE)
|
||||||
|
|
||||||
if (USE_SYSTEM_FDK_AAC_HEADERS)
|
|
||||||
find_path(SYSTEM_FDK_AAC_INCLUDES NAMES fdk-aac/aacdecoder_lib.h)
|
|
||||||
if (SYSTEM_FDK_AAC_INCLUDES STREQUAL "SYSTEM_FDK_AAC_INCLUDES-NOTFOUND")
|
|
||||||
message(WARNING "System fdk-aac headers not found. Falling back on bundled headers.")
|
|
||||||
else()
|
|
||||||
message(STATUS "Using system fdk_aac headers.")
|
|
||||||
target_include_directories(library-headers SYSTEM INTERFACE ${SYSTEM_FDK_AAC_INCLUDES})
|
|
||||||
set(FOUND_FDK_AAC_HEADERS ON)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if (NOT FOUND_FDK_AAC_HEADERS)
|
|
||||||
message(STATUS "Using bundled fdk_aac headers.")
|
|
||||||
target_include_directories(library-headers SYSTEM INTERFACE ./library-headers/fdk-aac/include)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (USE_SYSTEM_FFMPEG_HEADERS)
|
if (USE_SYSTEM_FFMPEG_HEADERS)
|
||||||
find_path(SYSTEM_FFMPEG_INCLUDES NAMES libavutil/avutil.h)
|
find_path(SYSTEM_FFMPEG_INCLUDES NAMES libavutil/avutil.h)
|
||||||
if (SYSTEM_FFMPEG_INCLUDES STREQUAL "SYSTEM_FFMPEG_INCLUDES-NOTFOUND")
|
if (SYSTEM_FFMPEG_INCLUDES STREQUAL "SYSTEM_FFMPEG_INCLUDES-NOTFOUND")
|
||||||
@ -334,7 +333,13 @@ if (ENABLE_WEB_SERVICE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# lodepng
|
# lodepng
|
||||||
add_subdirectory(lodepng)
|
if(USE_SYSTEM_LODEPNG)
|
||||||
|
add_library(lodepng INTERFACE)
|
||||||
|
find_package(lodepng REQUIRED)
|
||||||
|
target_link_libraries(lodepng INTERFACE lodepng::lodepng)
|
||||||
|
else()
|
||||||
|
add_subdirectory(lodepng)
|
||||||
|
endif()
|
||||||
|
|
||||||
# (xperia64): Only use libyuv on Android b/c of build issues on Windows and mandatory JPEG
|
# (xperia64): Only use libyuv on Android b/c of build issues on Windows and mandatory JPEG
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
@ -345,24 +350,47 @@ endif()
|
|||||||
|
|
||||||
# OpenAL Soft
|
# OpenAL Soft
|
||||||
if (ENABLE_OPENAL)
|
if (ENABLE_OPENAL)
|
||||||
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
if(USE_SYSTEM_OPENAL)
|
||||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
add_library(OpenAL INTERFACE)
|
||||||
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
find_package(OpenAL REQUIRED)
|
||||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
target_link_libraries(OpenAL INTERFACE OpenAL::OpenAL)
|
||||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
else()
|
||||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
||||||
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
||||||
set(LIBTYPE "STATIC" CACHE STRING "")
|
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
||||||
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
||||||
|
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
||||||
|
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
||||||
|
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
||||||
|
set(LIBTYPE "STATIC" CACHE STRING "")
|
||||||
|
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# VMA
|
# VMA
|
||||||
add_library(vma INTERFACE)
|
if(USE_SYSTEM_VMA)
|
||||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
add_library(vma INTERFACE)
|
||||||
|
find_package(VulkanMemoryAllocator REQUIRED)
|
||||||
|
if(TARGET GPUOpen::VulkanMemoryAllocator)
|
||||||
|
message(STATUS "Found VulkanMemoryAllocator")
|
||||||
|
target_link_libraries(vma INTERFACE GPUOpen::VulkanMemoryAllocator)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
add_library(vma INTERFACE)
|
||||||
|
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||||
|
endif()
|
||||||
|
|
||||||
# vulkan-headers
|
# vulkan-headers
|
||||||
add_library(vulkan-headers INTERFACE)
|
add_library(vulkan-headers INTERFACE)
|
||||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
if(USE_SYSTEM_VULKAN_HEADERS)
|
||||||
|
find_package(Vulkan REQUIRED)
|
||||||
|
if(TARGET Vulkan::Headers)
|
||||||
|
message(STATUS "Found Vulkan headers")
|
||||||
|
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||||
|
endif()
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@ -15,13 +15,17 @@ option(USE_SYSTEM_DYNARMIC "Use the system dynarmic (instead of the bundled one)
|
|||||||
option(USE_SYSTEM_FMT "Use the system fmt (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_FMT "Use the system fmt (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_XBYAK "Use the system xbyak (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_XBYAK "Use the system xbyak (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_INIH "Use the system inih (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_INIH "Use the system inih (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_FDK_AAC_HEADERS "Use the system fdk-aac headers (instead of the bundled one)" OFF)
|
|
||||||
option(USE_SYSTEM_FFMPEG_HEADERS "Use the system FFmpeg headers (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_FFMPEG_HEADERS "Use the system FFmpeg headers (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_GLSLANG "Use the system glslang and SPIR-V libraries (instead of the bundled ones)" OFF)
|
option(USE_SYSTEM_GLSLANG "Use the system glslang and SPIR-V libraries (instead of the bundled ones)" OFF)
|
||||||
option(USE_SYSTEM_ZSTD "Use the system Zstandard library (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_ZSTD "Use the system Zstandard library (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_ENET "Use the system libenet (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_ENET "Use the system libenet (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_CRYPTOPP "Use the system cryptopp (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_CRYPTOPP "Use the system cryptopp (instead of the bundled one)" OFF)
|
||||||
option(USE_SYSTEM_CUBEB "Use the system cubeb (instead of the bundled one)" OFF)
|
option(USE_SYSTEM_CUBEB "Use the system cubeb (instead of the bundled one)" OFF)
|
||||||
|
option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)" OFF)
|
||||||
|
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
||||||
|
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
||||||
|
option(USE_SYSTEM_VULKAN_HEADERS "Use the system Vulkan headers (instead of the bundled ones)" OFF)
|
||||||
|
option(USE_SYSTEM_CATCH2 "Use the system Catch2 (instead of the bundled one)" OFF)
|
||||||
|
|
||||||
# Qt and MoltenVK are handled separately
|
# Qt and MoltenVK are handled separately
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
@ -36,13 +40,17 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_DYNARMIC "Disable system Dynarmic" OFF "US
|
|||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FMT "Disable system fmt" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FMT "Disable system fmt" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_XBYAK "Disable system xbyak" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_XBYAK "Disable system xbyak" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_INIH "Disable system inih" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_INIH "Disable system inih" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FDK_AAC_HEADERS "Disable system fdk_aac" OFF "USE_SYSTEM_LIBS" OFF)
|
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FFMPEG_HEADERS "Disable system ffmpeg" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FFMPEG_HEADERS "Disable system ffmpeg" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_GLSLANG "Disable system glslang" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_GLSLANG "Disable system glslang" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ZSTD "Disable system Zstandard" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ZSTD "Disable system Zstandard" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ENET "Disable system libenet" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ENET "Disable system libenet" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CRYPTOPP "Disable system cryptopp" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CRYPTOPP "Disable system cryptopp" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VULKAN_HEADERS "Disable system Vulkan headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CATCH2 "Disable system Catch2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
|
||||||
set(LIB_VAR_LIST
|
set(LIB_VAR_LIST
|
||||||
SDL2
|
SDL2
|
||||||
@ -57,13 +65,17 @@ set(LIB_VAR_LIST
|
|||||||
FMT
|
FMT
|
||||||
XBYAK
|
XBYAK
|
||||||
INIH
|
INIH
|
||||||
FDK_AAC_HEADERS
|
|
||||||
FFMPEG_HEADERS
|
FFMPEG_HEADERS
|
||||||
GLSLANG
|
GLSLANG
|
||||||
ZSTD
|
ZSTD
|
||||||
ENET
|
ENET
|
||||||
CRYPTOPP
|
CRYPTOPP
|
||||||
CUBEB
|
CUBEB
|
||||||
|
LODEPNG
|
||||||
|
OPENAL
|
||||||
|
VMA
|
||||||
|
VULKAN_HEADERS
|
||||||
|
CATCH2
|
||||||
)
|
)
|
||||||
|
|
||||||
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
||||||
|
|||||||
36
externals/cmake-modules/FindOpenAL.cmake
vendored
Normal file
36
externals/cmake-modules/FindOpenAL.cmake
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
if(NOT OPENAL_FOUND)
|
||||||
|
pkg_check_modules(OPENAL_TMP openal)
|
||||||
|
|
||||||
|
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
||||||
|
PATHS
|
||||||
|
${OPENAL_TMP_INCLUDE_DIRS}
|
||||||
|
/usr/include/AL
|
||||||
|
/usr/include
|
||||||
|
/usr/local/include/AL
|
||||||
|
/usr/local/include
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(OPENAL_LIBRARY_DIRS NAMES openal
|
||||||
|
PATHS
|
||||||
|
${OPENAL_TMP_LIBRARY_DIRS}
|
||||||
|
/usr/lib
|
||||||
|
/usr/local/lib
|
||||||
|
)
|
||||||
|
|
||||||
|
if(OPENAL_INCLUDE_DIRS AND OPENAL_LIBRARY_DIRS)
|
||||||
|
set(OPENAL_FOUND TRUE CACHE INTERNAL "OpenAL found")
|
||||||
|
message(STATUS "Found OpenAL: ${OPENAL_LIBRARY_DIRS}, ${OPENAL_INCLUDE_DIRS}")
|
||||||
|
else()
|
||||||
|
set(OPENAL_FOUND FALSE CACHE INTERNAL "OpenAL found")
|
||||||
|
message(STATUS "OpenAL not found.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPENAL_FOUND AND NOT TARGET OpenAL::OpenAL)
|
||||||
|
add_library(OpenAL::OpenAL UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(OpenAL::OpenAL PROPERTIES
|
||||||
|
INCLUDE_DIRECTORIES ${OPENAL_INCLUDE_DIRS}
|
||||||
|
INTERFACE_LINK_LIBRARIES ${OPENAL_LIBRARY_DIRS}
|
||||||
|
IMPORTED_LOCATION ${OPENAL_LIBRARY_DIRS}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
31
externals/cmake-modules/Findlodepng.cmake
vendored
Normal file
31
externals/cmake-modules/Findlodepng.cmake
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
if(NOT LODEPNG_FOUND)
|
||||||
|
find_path(LODEPNG_INCLUDE_DIRS NAMES lodepng.h
|
||||||
|
PATHS
|
||||||
|
/usr/include
|
||||||
|
/usr/local/include
|
||||||
|
)
|
||||||
|
|
||||||
|
find_library(LODEPNG_LIBRARY_DIRS NAMES lodepng
|
||||||
|
PATHS
|
||||||
|
/usr/lib
|
||||||
|
/usr/local/lib
|
||||||
|
)
|
||||||
|
|
||||||
|
if(LODEPNG_INCLUDE_DIRS AND LODEPNG_LIBRARY_DIRS)
|
||||||
|
set(LODEPNG_FOUND TRUE CACHE INTERNAL "Found lodepng")
|
||||||
|
message(STATUS "Found lodepng: ${LODEPNG_LIBRARY_DIRS}, ${LODEPNG_INCLUDE_DIRS}")
|
||||||
|
else()
|
||||||
|
set(LODEPNG_FOUND FALSE CACHE INTERNAL "Found lodepng")
|
||||||
|
message(STATUS "Lodepng not found.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(LODEPNG_FOUND AND NOT TARGET lodepng::lodepng)
|
||||||
|
add_library(lodepng::lodepng UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(lodepng::lodepng PROPERTIES
|
||||||
|
INCLUDE_DIRECTORIES ${LODEPNG_INCLUDE_DIRS}
|
||||||
|
INTERFACE_LINK_LIBRARIES ${LODEPNG_LIBRARY_DIRS}
|
||||||
|
IMPORTED_LOCATION ${LODEPNG_LIBRARY_DIRS}
|
||||||
|
)
|
||||||
|
|
||||||
|
endif()
|
||||||
88
externals/faad2/CMakeLists.txt
vendored
Normal file
88
externals/faad2/CMakeLists.txt
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Sources cut down to just what we need for AAC-LC.
|
||||||
|
set(FAAD2_SOURCE_DIR "faad2/libfaad")
|
||||||
|
add_library(faad2 STATIC EXCLUDE_FROM_ALL
|
||||||
|
"${FAAD2_SOURCE_DIR}/bits.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/cfft.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/common.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/decoder.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/drc.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/error.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/filtbank.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/huffman.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/is.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/mdct.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/mp4.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/ms.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/output.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/pns.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/pulse.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/specrec.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/syntax.c"
|
||||||
|
"${FAAD2_SOURCE_DIR}/tns.c"
|
||||||
|
)
|
||||||
|
target_include_directories(faad2 PUBLIC faad2/include PRIVATE "${FAAD2_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Configure compile definitions.
|
||||||
|
|
||||||
|
# Read version from properties file for configuring constant.
|
||||||
|
file(READ faad2/properties.json FAAD_PROPERTIES_JSON)
|
||||||
|
string(JSON FAAD_VERSION GET ${FAAD_PROPERTIES_JSON} PACKAGE_VERSION)
|
||||||
|
message(STATUS "Building faad2 version ${FAAD_VERSION}")
|
||||||
|
|
||||||
|
# Check for functions and headers.
|
||||||
|
include(CheckFunctionExists)
|
||||||
|
include(CheckIncludeFiles)
|
||||||
|
check_function_exists(getpwuid HAVE_GETPWUID)
|
||||||
|
check_function_exists(lrintf HAVE_LRINTF)
|
||||||
|
check_function_exists(memcpy HAVE_MEMCPY)
|
||||||
|
check_function_exists(strchr HAVE_STRCHR)
|
||||||
|
check_function_exists(strsep HAVE_STRSEP)
|
||||||
|
check_include_files(dlfcn.h HAVE_DLFCN_H)
|
||||||
|
check_include_files(errno.h HAVE_ERRNO_H)
|
||||||
|
check_include_files(float.h HAVE_FLOAT_H)
|
||||||
|
check_include_files(inttypes.h HAVE_INTTYPES_H)
|
||||||
|
check_include_files(IOKit/IOKitLib.h HAVE_IOKIT_IOKITLIB_H)
|
||||||
|
check_include_files(limits.h HAVE_LIMITS_H)
|
||||||
|
check_include_files(mathf.h HAVE_MATHF_H)
|
||||||
|
check_include_files(stdint.h HAVE_STDINT_H)
|
||||||
|
check_include_files(stdio.h HAVE_STDIO_H)
|
||||||
|
check_include_files(stdlib.h HAVE_STDLIB_H)
|
||||||
|
check_include_files(strings.h HAVE_STRINGS_H)
|
||||||
|
check_include_files(string.h HAVE_STRING_H)
|
||||||
|
check_include_files(sysfs/libsysfs.h HAVE_SYSFS_LIBSYSFS_H)
|
||||||
|
check_include_files(sys/stat.h HAVE_SYS_STAT_H)
|
||||||
|
check_include_files(sys/time.h HAVE_SYS_TIME_H)
|
||||||
|
check_include_files(sys/types.h HAVE_SYS_TYPES_H)
|
||||||
|
check_include_files(unistd.h HAVE_UNISTD_H)
|
||||||
|
|
||||||
|
# faad2 uses a relative include for its config.h which breaks under CMake.
|
||||||
|
# We can use target_compile_definitions to pass on the configuration instead.
|
||||||
|
target_compile_definitions(faad2 PRIVATE
|
||||||
|
-DFAAD_VERSION=${FAAD_VERSION}
|
||||||
|
-DPACKAGE_VERSION=\"${FAAD_VERSION}\"
|
||||||
|
-DSTDC_HEADERS
|
||||||
|
-DHAVE_GETPWUID=${HAVE_GETPWUID}
|
||||||
|
-DHAVE_LRINTF=${HAVE_LRINTF}
|
||||||
|
-DHAVE_MEMCPY=${HAVE_MEMCPY}
|
||||||
|
-DHAVE_STRCHR=${HAVE_STRCHR}
|
||||||
|
-DHAVE_STRSEP=${HAVE_STRSEP}
|
||||||
|
-DHAVE_DLFCN_H=${HAVE_DLFCN_H}
|
||||||
|
-DHAVE_ERRNO_H=${HAVE_ERRNO_H}
|
||||||
|
-DHAVE_FLOAT_H=${HAVE_FLOAT_H}
|
||||||
|
-DHAVE_INTTYPES_H=${HAVE_INTTYPES_H}
|
||||||
|
-DHAVE_IOKIT_IOKITLIB_H=${HAVE_IOKIT_IOKITLIB_H}
|
||||||
|
-DHAVE_LIMITS_H=${HAVE_LIMITS_H}
|
||||||
|
-DHAVE_MATHF_H=${HAVE_MATHF_H}
|
||||||
|
-DHAVE_STDINT_H=${HAVE_STDINT_H}
|
||||||
|
-DHAVE_STDIO_H=${HAVE_STDIO_H}
|
||||||
|
-DHAVE_STDLIB_H=${HAVE_STDLIB_H}
|
||||||
|
-DHAVE_STRINGS_H=${HAVE_STRINGS_H}
|
||||||
|
-DHAVE_STRING_H=${HAVE_STRING_H}
|
||||||
|
-DHAVE_SYSFS_LIBSYSFS_H=${HAVE_SYSFS_LIBSYSFS_H}
|
||||||
|
-DHAVE_SYS_STAT_H=${HAVE_SYS_STAT_H}
|
||||||
|
-DHAVE_SYS_TIME_H=${HAVE_SYS_TIME_H}
|
||||||
|
-DHAVE_SYS_TYPES_H=${HAVE_SYS_TYPES_H}
|
||||||
|
-DHAVE_UNISTD_H=${HAVE_UNISTD_H}
|
||||||
|
# Only compile for AAC-LC decoding.
|
||||||
|
-DLC_ONLY_DECODER -DDISABLE_SBR
|
||||||
|
)
|
||||||
1
externals/faad2/faad2
vendored
Submodule
1
externals/faad2/faad2
vendored
Submodule
Submodule externals/faad2/faad2 added at 09b3c850c6
6
externals/glad/Readme.md
vendored
6
externals/glad/Readme.md
vendored
@ -3,3 +3,9 @@ These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL
|
|||||||
```
|
```
|
||||||
python -m glad --profile core --out-path glad/ --api "gl=4.3,gles2=3.2" --generator=c
|
python -m glad --profile core --out-path glad/ --api "gl=4.3,gles2=3.2" --generator=c
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also generate the source using [this site](https://glad.dav1d.de/):
|
||||||
|
1. Select '4.3' for GL, '3.2' for GLES2, and 'Core' for Profile.
|
||||||
|
2. Input the currently supported extensions from [here](https://github.com/citra-emu/citra/blob/master/externals/glad/include/glad/glad.h#L9), plus any new required extensions.
|
||||||
|
3. Click Generate and download the generated source zip.
|
||||||
|
4. Unzip the new source over the current glad source files.
|
||||||
27
externals/glad/include/glad/glad.h
vendored
27
externals/glad/include/glad/glad.h
vendored
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||||
|
|
||||||
Language/Generator: C/C++
|
Language/Generator: C/C++
|
||||||
Specification: gl
|
Specification: gl
|
||||||
@ -10,6 +10,7 @@
|
|||||||
GL_AMD_blend_minmax_factor,
|
GL_AMD_blend_minmax_factor,
|
||||||
GL_ARB_buffer_storage,
|
GL_ARB_buffer_storage,
|
||||||
GL_ARB_clear_texture,
|
GL_ARB_clear_texture,
|
||||||
|
GL_ARB_fragment_shader_interlock,
|
||||||
GL_ARB_get_texture_sub_image,
|
GL_ARB_get_texture_sub_image,
|
||||||
GL_ARB_texture_compression_bptc,
|
GL_ARB_texture_compression_bptc,
|
||||||
GL_ARM_shader_framebuffer_fetch,
|
GL_ARM_shader_framebuffer_fetch,
|
||||||
@ -17,16 +18,18 @@
|
|||||||
GL_EXT_clip_cull_distance,
|
GL_EXT_clip_cull_distance,
|
||||||
GL_EXT_shader_framebuffer_fetch,
|
GL_EXT_shader_framebuffer_fetch,
|
||||||
GL_EXT_texture_compression_s3tc,
|
GL_EXT_texture_compression_s3tc,
|
||||||
GL_NV_blend_minmax_factor
|
GL_INTEL_fragment_shader_ordering,
|
||||||
|
GL_NV_blend_minmax_factor,
|
||||||
|
GL_NV_fragment_shader_interlock
|
||||||
Loader: True
|
Loader: True
|
||||||
Local files: False
|
Local files: False
|
||||||
Omit khrplatform: False
|
Omit khrplatform: False
|
||||||
Reproducible: False
|
Reproducible: False
|
||||||
|
|
||||||
Commandline:
|
Commandline:
|
||||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||||
Online:
|
Online:
|
||||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@ -3384,6 +3387,10 @@ typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC)(GLuint texture, GLint level,
|
|||||||
GLAPI PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage;
|
GLAPI PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage;
|
||||||
#define glClearTexSubImage glad_glClearTexSubImage
|
#define glClearTexSubImage glad_glClearTexSubImage
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef GL_ARB_fragment_shader_interlock
|
||||||
|
#define GL_ARB_fragment_shader_interlock 1
|
||||||
|
GLAPI int GLAD_GL_ARB_fragment_shader_interlock;
|
||||||
|
#endif
|
||||||
#ifndef GL_ARB_get_texture_sub_image
|
#ifndef GL_ARB_get_texture_sub_image
|
||||||
#define GL_ARB_get_texture_sub_image 1
|
#define GL_ARB_get_texture_sub_image 1
|
||||||
GLAPI int GLAD_GL_ARB_get_texture_sub_image;
|
GLAPI int GLAD_GL_ARB_get_texture_sub_image;
|
||||||
@ -3406,10 +3413,18 @@ GLAPI int GLAD_GL_EXT_shader_framebuffer_fetch;
|
|||||||
#define GL_EXT_texture_compression_s3tc 1
|
#define GL_EXT_texture_compression_s3tc 1
|
||||||
GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef GL_INTEL_fragment_shader_ordering
|
||||||
|
#define GL_INTEL_fragment_shader_ordering 1
|
||||||
|
GLAPI int GLAD_GL_INTEL_fragment_shader_ordering;
|
||||||
|
#endif
|
||||||
#ifndef GL_NV_blend_minmax_factor
|
#ifndef GL_NV_blend_minmax_factor
|
||||||
#define GL_NV_blend_minmax_factor 1
|
#define GL_NV_blend_minmax_factor 1
|
||||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef GL_NV_fragment_shader_interlock
|
||||||
|
#define GL_NV_fragment_shader_interlock 1
|
||||||
|
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||||
|
#endif
|
||||||
#ifndef GL_ARM_shader_framebuffer_fetch
|
#ifndef GL_ARM_shader_framebuffer_fetch
|
||||||
#define GL_ARM_shader_framebuffer_fetch 1
|
#define GL_ARM_shader_framebuffer_fetch 1
|
||||||
GLAPI int GLAD_GL_ARM_shader_framebuffer_fetch;
|
GLAPI int GLAD_GL_ARM_shader_framebuffer_fetch;
|
||||||
@ -3437,6 +3452,10 @@ GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
|||||||
#define GL_NV_blend_minmax_factor 1
|
#define GL_NV_blend_minmax_factor 1
|
||||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef GL_NV_fragment_shader_interlock
|
||||||
|
#define GL_NV_fragment_shader_interlock 1
|
||||||
|
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
18
externals/glad/src/glad.c
vendored
18
externals/glad/src/glad.c
vendored
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||||
|
|
||||||
Language/Generator: C/C++
|
Language/Generator: C/C++
|
||||||
Specification: gl
|
Specification: gl
|
||||||
@ -10,6 +10,7 @@
|
|||||||
GL_AMD_blend_minmax_factor,
|
GL_AMD_blend_minmax_factor,
|
||||||
GL_ARB_buffer_storage,
|
GL_ARB_buffer_storage,
|
||||||
GL_ARB_clear_texture,
|
GL_ARB_clear_texture,
|
||||||
|
GL_ARB_fragment_shader_interlock,
|
||||||
GL_ARB_get_texture_sub_image,
|
GL_ARB_get_texture_sub_image,
|
||||||
GL_ARB_texture_compression_bptc,
|
GL_ARB_texture_compression_bptc,
|
||||||
GL_ARM_shader_framebuffer_fetch,
|
GL_ARM_shader_framebuffer_fetch,
|
||||||
@ -17,16 +18,18 @@
|
|||||||
GL_EXT_clip_cull_distance,
|
GL_EXT_clip_cull_distance,
|
||||||
GL_EXT_shader_framebuffer_fetch,
|
GL_EXT_shader_framebuffer_fetch,
|
||||||
GL_EXT_texture_compression_s3tc,
|
GL_EXT_texture_compression_s3tc,
|
||||||
GL_NV_blend_minmax_factor
|
GL_INTEL_fragment_shader_ordering,
|
||||||
|
GL_NV_blend_minmax_factor,
|
||||||
|
GL_NV_fragment_shader_interlock
|
||||||
Loader: True
|
Loader: True
|
||||||
Local files: False
|
Local files: False
|
||||||
Omit khrplatform: False
|
Omit khrplatform: False
|
||||||
Reproducible: False
|
Reproducible: False
|
||||||
|
|
||||||
Commandline:
|
Commandline:
|
||||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||||
Online:
|
Online:
|
||||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@ -860,6 +863,7 @@ PFNGLWAITSYNCPROC glad_glWaitSync = NULL;
|
|||||||
int GLAD_GL_AMD_blend_minmax_factor = 0;
|
int GLAD_GL_AMD_blend_minmax_factor = 0;
|
||||||
int GLAD_GL_ARB_buffer_storage = 0;
|
int GLAD_GL_ARB_buffer_storage = 0;
|
||||||
int GLAD_GL_ARB_clear_texture = 0;
|
int GLAD_GL_ARB_clear_texture = 0;
|
||||||
|
int GLAD_GL_ARB_fragment_shader_interlock = 0;
|
||||||
int GLAD_GL_ARB_get_texture_sub_image = 0;
|
int GLAD_GL_ARB_get_texture_sub_image = 0;
|
||||||
int GLAD_GL_ARB_texture_compression_bptc = 0;
|
int GLAD_GL_ARB_texture_compression_bptc = 0;
|
||||||
int GLAD_GL_ARM_shader_framebuffer_fetch = 0;
|
int GLAD_GL_ARM_shader_framebuffer_fetch = 0;
|
||||||
@ -867,7 +871,9 @@ int GLAD_GL_EXT_buffer_storage = 0;
|
|||||||
int GLAD_GL_EXT_clip_cull_distance = 0;
|
int GLAD_GL_EXT_clip_cull_distance = 0;
|
||||||
int GLAD_GL_EXT_shader_framebuffer_fetch = 0;
|
int GLAD_GL_EXT_shader_framebuffer_fetch = 0;
|
||||||
int GLAD_GL_EXT_texture_compression_s3tc = 0;
|
int GLAD_GL_EXT_texture_compression_s3tc = 0;
|
||||||
|
int GLAD_GL_INTEL_fragment_shader_ordering = 0;
|
||||||
int GLAD_GL_NV_blend_minmax_factor = 0;
|
int GLAD_GL_NV_blend_minmax_factor = 0;
|
||||||
|
int GLAD_GL_NV_fragment_shader_interlock = 0;
|
||||||
PFNGLBUFFERSTORAGEPROC glad_glBufferStorage = NULL;
|
PFNGLBUFFERSTORAGEPROC glad_glBufferStorage = NULL;
|
||||||
PFNGLCLEARTEXIMAGEPROC glad_glClearTexImage = NULL;
|
PFNGLCLEARTEXIMAGEPROC glad_glClearTexImage = NULL;
|
||||||
PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage = NULL;
|
PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage = NULL;
|
||||||
@ -1509,11 +1515,14 @@ static int find_extensionsGL(void) {
|
|||||||
GLAD_GL_AMD_blend_minmax_factor = has_ext("GL_AMD_blend_minmax_factor");
|
GLAD_GL_AMD_blend_minmax_factor = has_ext("GL_AMD_blend_minmax_factor");
|
||||||
GLAD_GL_ARB_buffer_storage = has_ext("GL_ARB_buffer_storage");
|
GLAD_GL_ARB_buffer_storage = has_ext("GL_ARB_buffer_storage");
|
||||||
GLAD_GL_ARB_clear_texture = has_ext("GL_ARB_clear_texture");
|
GLAD_GL_ARB_clear_texture = has_ext("GL_ARB_clear_texture");
|
||||||
|
GLAD_GL_ARB_fragment_shader_interlock = has_ext("GL_ARB_fragment_shader_interlock");
|
||||||
GLAD_GL_ARB_get_texture_sub_image = has_ext("GL_ARB_get_texture_sub_image");
|
GLAD_GL_ARB_get_texture_sub_image = has_ext("GL_ARB_get_texture_sub_image");
|
||||||
GLAD_GL_ARB_texture_compression_bptc = has_ext("GL_ARB_texture_compression_bptc");
|
GLAD_GL_ARB_texture_compression_bptc = has_ext("GL_ARB_texture_compression_bptc");
|
||||||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||||
|
GLAD_GL_INTEL_fragment_shader_ordering = has_ext("GL_INTEL_fragment_shader_ordering");
|
||||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||||
|
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||||
free_exts();
|
free_exts();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -1988,6 +1997,7 @@ static int find_extensionsGLES2(void) {
|
|||||||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||||
|
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||||
free_exts();
|
free_exts();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
1
externals/oaknut
vendored
Submodule
1
externals/oaknut
vendored
Submodule
Submodule externals/oaknut added at e6eecc3f94
BIN
keys.tar.enc
BIN
keys.tar.enc
Binary file not shown.
@ -102,7 +102,13 @@ if (MSVC)
|
|||||||
else()
|
else()
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
-Wall
|
-Wall
|
||||||
-Wno-attributes
|
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||||
|
-Wno-unused-command-line-argument
|
||||||
|
# Build fortification options
|
||||||
|
-Wp,-D_FORTIFY_SOURCE=2
|
||||||
|
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||||
|
-fstack-protector-strong
|
||||||
|
-fstack-clash-protection
|
||||||
)
|
)
|
||||||
|
|
||||||
if (CITRA_WARNINGS_AS_ERRORS)
|
if (CITRA_WARNINGS_AS_ERRORS)
|
||||||
|
|||||||
@ -2,15 +2,18 @@
|
|||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
import android.databinding.tool.ext.capitalizeUS
|
||||||
|
import de.undercouch.gradle.tasks.download.Download
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("de.undercouch.download") version "5.5.0"
|
id("de.undercouch.download") version "5.5.0"
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
kotlin("plugin.serialization") version "1.8.21"
|
||||||
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
import android.databinding.tool.ext.capitalizeUS
|
|
||||||
import de.undercouch.gradle.tasks.download.Download
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the number of seconds/10 since Jan 1 2016 as the versionCode.
|
* Use the number of seconds/10 since Jan 1 2016 as the versionCode.
|
||||||
* This lets us upload a new build at most every 10 seconds for the
|
* This lets us upload a new build at most every 10 seconds for the
|
||||||
@ -25,7 +28,7 @@ val downloadedJniLibsPath = "${buildDir}/downloadedJniLibs"
|
|||||||
android {
|
android {
|
||||||
namespace = "org.citra.citra_emu"
|
namespace = "org.citra.citra_emu"
|
||||||
|
|
||||||
compileSdkVersion = "android-33"
|
compileSdkVersion = "android-34"
|
||||||
ndkVersion = "25.2.9519653"
|
ndkVersion = "25.2.9519653"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -37,6 +40,11 @@ android {
|
|||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
// This is necessary for libadrenotools custom driver loading
|
||||||
|
jniLibs.useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
@ -51,7 +59,7 @@ android {
|
|||||||
// TODO If this is ever modified, change application_id in strings.xml
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId = "org.citra.citra_emu"
|
applicationId = "org.citra.citra_emu"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 33
|
targetSdk = 34
|
||||||
versionCode = autoVersion
|
versionCode = autoVersion
|
||||||
versionName = getGitVersion()
|
versionName = getGitVersion()
|
||||||
|
|
||||||
@ -69,6 +77,9 @@ android {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||||
|
buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
||||||
@ -92,6 +103,12 @@ android {
|
|||||||
} else {
|
} else {
|
||||||
signingConfigs.getByName("debug")
|
signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// builds a release build that doesn't need signing
|
// builds a release build that doesn't need signing
|
||||||
@ -101,9 +118,15 @@ android {
|
|||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
versionNameSuffix = "-debug"
|
versionNameSuffix = "-debug"
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
isJniDebuggable = true
|
isJniDebuggable = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
isDefault = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signed by debug key disallowing distribution on Play Store.
|
// Signed by debug key disallowing distribution on Play Store.
|
||||||
@ -145,8 +168,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.activity:activity-ktx:1.7.2")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.6.0")
|
implementation("androidx.activity:activity-ktx:1.8.0")
|
||||||
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||||
@ -158,15 +182,14 @@ dependencies {
|
|||||||
// For loading huge screenshots from the disk.
|
// For loading huge screenshots from the disk.
|
||||||
implementation("com.squareup.picasso:picasso:2.71828")
|
implementation("com.squareup.picasso:picasso:2.71828")
|
||||||
|
|
||||||
// Allows FRP-style asynchronous operations in Android.
|
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
|
||||||
|
|
||||||
implementation("org.ini4j:ini4j:0.5.4")
|
implementation("org.ini4j:ini4j:0.5.4")
|
||||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
|
||||||
// Please don't upgrade the billing library as the newer version is not GPL-compatible
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
|
||||||
implementation("com.android.billingclient:billing:2.0.3")
|
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("io.coil-kt:coil:2.2.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||||
@ -216,6 +239,34 @@ fun getGitVersion(): String {
|
|||||||
return versionName
|
return versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getGitHash(): String =
|
||||||
|
runGitCommand(ProcessBuilder("git", "rev-parse", "--short", "HEAD")) ?: "dummy-hash"
|
||||||
|
|
||||||
|
fun getBranch(): String =
|
||||||
|
runGitCommand(ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")) ?: "dummy-branch"
|
||||||
|
|
||||||
|
fun runGitCommand(command: ProcessBuilder) : String? {
|
||||||
|
try {
|
||||||
|
command.directory(project.rootDir)
|
||||||
|
val process = command.start()
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val errorStream = process.errorStream
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
return if (process.exitValue() == 0) {
|
||||||
|
inputStream.bufferedReader()
|
||||||
|
.use { it.readText().trim() } // return the value of gitHash
|
||||||
|
} else {
|
||||||
|
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
|
||||||
|
logger.error("Error running git command: $errorMessage")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("$e: Cannot find git")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android.applicationVariants.configureEach {
|
android.applicationVariants.configureEach {
|
||||||
val variant = this
|
val variant = this
|
||||||
val capitalizedName = variant.name.capitalizeUS()
|
val capitalizedName = variant.name.capitalizeUS()
|
||||||
|
|||||||
40
src/android/app/proguard-rules.pro
vendored
40
src/android/app/proguard-rules.pro
vendored
@ -1,21 +1,25 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Copyright 2023 Citra Emulator Project
|
||||||
# You can control the set of applied configuration files using the
|
# Licensed under GPLv2 or any later version
|
||||||
# proguardFiles setting in build.gradle.
|
# Refer to the license.txt file included.
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
# To get usable stack traces
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
-dontobfuscate
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Prevents crashing when using Wini
|
||||||
# debugging stack traces.
|
-keep class org.ini4j.spi.IniParser
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
-keep class org.ini4j.spi.IniBuilder
|
||||||
|
-keep class org.ini4j.spi.IniFormatter
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# Suppress warnings for R8
|
||||||
# hide the original source file name.
|
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||||
#-renamesourcefileattribute SourceFile
|
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||||
|
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||||
|
-dontwarn org.conscrypt.Conscrypt$Version
|
||||||
|
-dontwarn org.conscrypt.Conscrypt
|
||||||
|
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||||
|
-dontwarn java.beans.Introspector
|
||||||
|
-dontwarn java.beans.VetoableChangeListener
|
||||||
|
-dontwarn java.beans.VetoableChangeSupport
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -44,8 +45,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||||
android:theme="@style/Theme.Citra.Splash.Main"
|
android:theme="@style/Theme.Citra.Splash.Main"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
android:resizeableActivity="false">
|
|
||||||
|
|
||||||
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -68,21 +68,15 @@
|
|||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Main"
|
||||||
android:launchMode="singleTop"/>
|
android:launchMode="singleTop"/>
|
||||||
|
|
||||||
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
|
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
||||||
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||||
|
</service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Main"
|
||||||
android:label="@string/cheats"/>
|
android:label="@string/cheats"/>
|
||||||
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="org.citra.citra_emu.model.GameProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
</provider>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
package org.citra.citra_emu;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
|
||||||
import org.citra.citra_emu.utils.DocumentsTree;
|
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
|
||||||
|
|
||||||
public class CitraApplication extends Application {
|
|
||||||
public static GameDatabase databaseHelper;
|
|
||||||
public static DocumentsTree documentsTree;
|
|
||||||
private static CitraApplication application;
|
|
||||||
|
|
||||||
private void createNotificationChannel() {
|
|
||||||
// Create the NotificationChannel, but only on API 26+ because
|
|
||||||
// the NotificationChannel class is new and not in the support library
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
|
||||||
{
|
|
||||||
// General notification
|
|
||||||
CharSequence name = getString(R.string.app_notification_channel_name);
|
|
||||||
String description = getString(R.string.app_notification_channel_description);
|
|
||||||
NotificationChannel channel = new NotificationChannel(
|
|
||||||
getString(R.string.app_notification_channel_id), name,
|
|
||||||
NotificationManager.IMPORTANCE_LOW);
|
|
||||||
channel.setDescription(description);
|
|
||||||
channel.setSound(null, null);
|
|
||||||
channel.setVibrationPattern(null);
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// CIA Install notifications
|
|
||||||
NotificationChannel channel = new NotificationChannel(
|
|
||||||
getString(R.string.cia_install_notification_channel_id),
|
|
||||||
getString(R.string.cia_install_notification_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT);
|
|
||||||
channel.setDescription(getString(R.string.cia_install_notification_channel_description));
|
|
||||||
channel.setSound(null, null);
|
|
||||||
channel.setVibrationPattern(null);
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
application = this;
|
|
||||||
documentsTree = new DocumentsTree();
|
|
||||||
|
|
||||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
|
||||||
DirectoryInitialization.start(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.LogDeviceInfo();
|
|
||||||
createNotificationChannel();
|
|
||||||
|
|
||||||
databaseHelper = new GameDatabase(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Context getAppContext() {
|
|
||||||
return application.getApplicationContext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||||
|
import org.citra.citra_emu.utils.DocumentsTree
|
||||||
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
|
||||||
|
class CitraApplication : Application() {
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
with(getSystemService(NotificationManager::class.java)) {
|
||||||
|
// General notification
|
||||||
|
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
||||||
|
val description = getString(R.string.app_notification_channel_description)
|
||||||
|
val generalChannel = NotificationChannel(
|
||||||
|
getString(R.string.app_notification_channel_id),
|
||||||
|
name,
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
generalChannel.description = description
|
||||||
|
generalChannel.setSound(null, null)
|
||||||
|
generalChannel.vibrationPattern = null
|
||||||
|
createNotificationChannel(generalChannel)
|
||||||
|
|
||||||
|
// CIA Install notifications
|
||||||
|
val ciaChannel = NotificationChannel(
|
||||||
|
getString(R.string.cia_install_notification_channel_id),
|
||||||
|
getString(R.string.cia_install_notification_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
ciaChannel.description =
|
||||||
|
getString(R.string.cia_install_notification_channel_description)
|
||||||
|
ciaChannel.setSound(null, null)
|
||||||
|
ciaChannel.vibrationPattern = null
|
||||||
|
createNotificationChannel(ciaChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
application = this
|
||||||
|
documentsTree = DocumentsTree()
|
||||||
|
if (PermissionsHandler.hasWriteAccess(applicationContext)) {
|
||||||
|
DirectoryInitialization.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.logDeviceInfo()
|
||||||
|
createNotificationChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var application: CitraApplication? = null
|
||||||
|
|
||||||
|
val appContext: Context get() = application!!.applicationContext
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
lateinit var documentsTree: DocumentsTree
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,720 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2013 Dolphin Emulator Project
|
|
||||||
* Licensed under GPLv2+
|
|
||||||
* Refer to the license.txt file included.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.citra.citra_emu;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static android.Manifest.permission.CAMERA;
|
|
||||||
import static android.Manifest.permission.RECORD_AUDIO;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class which contains methods that interact
|
|
||||||
* with the native side of the Citra code.
|
|
||||||
*/
|
|
||||||
public final class NativeLibrary {
|
|
||||||
/**
|
|
||||||
* Default touchscreen device
|
|
||||||
*/
|
|
||||||
public static final String TouchScreenDevice = "Touchscreen";
|
|
||||||
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
|
|
||||||
|
|
||||||
private static boolean alertResult = false;
|
|
||||||
private static String alertPromptResult = "";
|
|
||||||
private static int alertPromptButton = 0;
|
|
||||||
private static final Object alertPromptLock = new Object();
|
|
||||||
private static boolean alertPromptInProgress = false;
|
|
||||||
private static String alertPromptCaption = "";
|
|
||||||
private static int alertPromptButtonConfig = 0;
|
|
||||||
private static EditText alertPromptEditText = null;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
System.loadLibrary("citra-android");
|
|
||||||
} catch (UnsatisfiedLinkError ex) {
|
|
||||||
Log.error("[NativeLibrary] " + ex.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NativeLibrary() {
|
|
||||||
// Disallows instantiation.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles button press events for a gamepad.
|
|
||||||
*
|
|
||||||
* @param Device The input descriptor of the gamepad.
|
|
||||||
* @param Button Key code identifying which button was pressed.
|
|
||||||
* @param Action Mask identifying which action is happening (button pressed down, or button released).
|
|
||||||
* @return If we handled the button press.
|
|
||||||
*/
|
|
||||||
public static native boolean onGamePadEvent(String Device, int Button, int Action);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles gamepad movement events.
|
|
||||||
*
|
|
||||||
* @param Device The device ID of the gamepad.
|
|
||||||
* @param Axis The axis ID
|
|
||||||
* @param x_axis The value of the x-axis represented by the given ID.
|
|
||||||
* @param y_axis The value of the y-axis represented by the given ID
|
|
||||||
*/
|
|
||||||
public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles gamepad movement events.
|
|
||||||
*
|
|
||||||
* @param Device The device ID of the gamepad.
|
|
||||||
* @param Axis_id The axis ID
|
|
||||||
* @param axis_val The value of the axis represented by the given ID.
|
|
||||||
*/
|
|
||||||
public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch events.
|
|
||||||
*
|
|
||||||
* @param x_axis The value of the x-axis.
|
|
||||||
* @param y_axis The value of the y-axis
|
|
||||||
* @param pressed To identify if the touch held down or released.
|
|
||||||
* @return true if the pointer is within the touchscreen
|
|
||||||
*/
|
|
||||||
public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch movement.
|
|
||||||
*
|
|
||||||
* @param x_axis The value of the instantaneous x-axis.
|
|
||||||
* @param y_axis The value of the instantaneous y-axis.
|
|
||||||
*/
|
|
||||||
public static native void onTouchMoved(float x_axis, float y_axis);
|
|
||||||
|
|
||||||
public static native void ReloadSettings();
|
|
||||||
|
|
||||||
public static native String GetUserSetting(String gameID, String Section, String Key);
|
|
||||||
|
|
||||||
public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
|
|
||||||
|
|
||||||
public static native void InitGameIni(String gameID);
|
|
||||||
|
|
||||||
public static native long GetTitleId(String filename);
|
|
||||||
|
|
||||||
public static native String GetGitRevision();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current working user directory
|
|
||||||
* If not set, it auto-detects a location
|
|
||||||
*/
|
|
||||||
public static native void SetUserDirectory(String directory);
|
|
||||||
|
|
||||||
public static native String[] GetInstalledGamePaths();
|
|
||||||
|
|
||||||
// Create the config.ini file.
|
|
||||||
public static native void CreateConfigFile();
|
|
||||||
|
|
||||||
public static native void CreateLogFile();
|
|
||||||
|
|
||||||
public static native void LogUserDirectory(String directory);
|
|
||||||
|
|
||||||
public static native int DefaultCPUCore();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begins emulation.
|
|
||||||
*/
|
|
||||||
public static native void Run(String path);
|
|
||||||
|
|
||||||
public static native String[] GetTextureFilterNames();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begins emulation from the specified savestate.
|
|
||||||
*/
|
|
||||||
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
|
|
||||||
|
|
||||||
// Surface Handling
|
|
||||||
public static native void SurfaceChanged(Surface surf);
|
|
||||||
|
|
||||||
public static native void SurfaceDestroyed();
|
|
||||||
|
|
||||||
public static native void DoFrame();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unpauses emulation from a paused state.
|
|
||||||
*/
|
|
||||||
public static native void UnPauseEmulation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses emulation.
|
|
||||||
*/
|
|
||||||
public static native void PauseEmulation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops emulation.
|
|
||||||
*/
|
|
||||||
public static native void StopEmulation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if emulation is running (or is paused).
|
|
||||||
*/
|
|
||||||
public static native boolean IsRunning();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the title ID of the currently running title, or 0 on failure.
|
|
||||||
*/
|
|
||||||
public static native long GetRunningTitleId();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the performance stats for the current game
|
|
||||||
**/
|
|
||||||
public static native double[] GetPerfStats();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the core emulation that the orientation has changed.
|
|
||||||
*/
|
|
||||||
public static native void NotifyOrientationChange(int layout_option, int rotation);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swaps the top and bottom screens.
|
|
||||||
*/
|
|
||||||
public static native void SwapScreens(boolean swap_screens, int rotation);
|
|
||||||
|
|
||||||
public enum CoreError {
|
|
||||||
ErrorSystemFiles,
|
|
||||||
ErrorSavestate,
|
|
||||||
ErrorUnknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean coreErrorAlertResult = false;
|
|
||||||
private static final Object coreErrorAlertLock = new Object();
|
|
||||||
|
|
||||||
public static class CoreErrorDialogFragment extends DialogFragment {
|
|
||||||
static CoreErrorDialogFragment newInstance(String title, String message) {
|
|
||||||
CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString("title", title);
|
|
||||||
args.putString("message", message);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
|
||||||
|
|
||||||
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
|
|
||||||
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
|
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(title)
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(R.string.continue_button, (dialog, which) -> {
|
|
||||||
coreErrorAlertResult = true;
|
|
||||||
synchronized (coreErrorAlertLock) {
|
|
||||||
coreErrorAlertLock.notify();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.abort_button, (dialog, which) -> {
|
|
||||||
coreErrorAlertResult = false;
|
|
||||||
synchronized (coreErrorAlertLock) {
|
|
||||||
coreErrorAlertLock.notify();
|
|
||||||
}
|
|
||||||
}).setOnDismissListener(dialog -> {
|
|
||||||
coreErrorAlertResult = true;
|
|
||||||
synchronized (coreErrorAlertLock) {
|
|
||||||
coreErrorAlertLock.notify();
|
|
||||||
}
|
|
||||||
}).create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnCoreErrorImpl(String title, String message) {
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
|
|
||||||
fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a core error.
|
|
||||||
* @return true: continue; false: abort
|
|
||||||
*/
|
|
||||||
public static boolean OnCoreError(CoreError error, String details) {
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String title, message;
|
|
||||||
switch (error) {
|
|
||||||
case ErrorSystemFiles: {
|
|
||||||
title = emulationActivity.getString(R.string.system_archive_not_found);
|
|
||||||
message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorSavestate: {
|
|
||||||
title = emulationActivity.getString(R.string.save_load_error);
|
|
||||||
message = details;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorUnknown: {
|
|
||||||
title = emulationActivity.getString(R.string.fatal_error);
|
|
||||||
message = emulationActivity.getString(R.string.fatal_error_message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the AlertDialog on the main thread.
|
|
||||||
emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));
|
|
||||||
|
|
||||||
// Wait for the lock to notify that it is complete.
|
|
||||||
synchronized (coreErrorAlertLock) {
|
|
||||||
try {
|
|
||||||
coreErrorAlertLock.wait();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return coreErrorAlertResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPortraitMode() {
|
|
||||||
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
|
|
||||||
Configuration.ORIENTATION_PORTRAIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int landscapeScreenLayout() {
|
|
||||||
return EmulationMenuSettings.getLandscapeScreenLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean displayAlertMsg(final String caption, final String text,
|
|
||||||
final boolean yesNo) {
|
|
||||||
Log.error("[NativeLibrary] Alert: " + text);
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
boolean result = false;
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
|
|
||||||
} else {
|
|
||||||
// Create object used for waiting.
|
|
||||||
final Object lock = new Object();
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(caption)
|
|
||||||
.setMessage(text);
|
|
||||||
|
|
||||||
// If not yes/no dialog just have one button that dismisses modal,
|
|
||||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
|
||||||
if (!yesNo) {
|
|
||||||
builder
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
|
|
||||||
{
|
|
||||||
dialog.dismiss();
|
|
||||||
synchronized (lock) {
|
|
||||||
lock.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alertResult = false;
|
|
||||||
|
|
||||||
builder
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
|
|
||||||
{
|
|
||||||
alertResult = true;
|
|
||||||
dialog.dismiss();
|
|
||||||
synchronized (lock) {
|
|
||||||
lock.notify();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.no, (dialog, whichButton) ->
|
|
||||||
{
|
|
||||||
alertResult = false;
|
|
||||||
dialog.dismiss();
|
|
||||||
synchronized (lock) {
|
|
||||||
lock.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the AlertDialog on the main thread.
|
|
||||||
emulationActivity.runOnUiThread(builder::show);
|
|
||||||
|
|
||||||
// Wait for the lock to notify that it is complete.
|
|
||||||
synchronized (lock) {
|
|
||||||
try {
|
|
||||||
lock.wait();
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yesNo)
|
|
||||||
result = alertResult;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void retryDisplayAlertPrompt() {
|
|
||||||
if (!alertPromptInProgress) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
|
|
||||||
alertPromptCaption = caption;
|
|
||||||
alertPromptButtonConfig = buttonConfig;
|
|
||||||
alertPromptInProgress = true;
|
|
||||||
|
|
||||||
// Show the AlertDialog on the main thread
|
|
||||||
sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
|
|
||||||
|
|
||||||
// Wait for the lock to notify that it is complete
|
|
||||||
synchronized (alertPromptLock) {
|
|
||||||
try {
|
|
||||||
alertPromptLock.wait();
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alertPromptInProgress = false;
|
|
||||||
|
|
||||||
return alertPromptResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
alertPromptResult = "";
|
|
||||||
alertPromptButton = 0;
|
|
||||||
|
|
||||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
|
||||||
|
|
||||||
// Set up the input
|
|
||||||
alertPromptEditText = new EditText(CitraApplication.getAppContext());
|
|
||||||
alertPromptEditText.setText(text);
|
|
||||||
alertPromptEditText.setSingleLine();
|
|
||||||
alertPromptEditText.setLayoutParams(params);
|
|
||||||
|
|
||||||
FrameLayout container = new FrameLayout(emulationActivity);
|
|
||||||
container.addView(alertPromptEditText);
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(caption)
|
|
||||||
.setView(container)
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
|
||||||
{
|
|
||||||
alertPromptButton = buttonConfig;
|
|
||||||
alertPromptResult = alertPromptEditText.getText().toString();
|
|
||||||
synchronized (alertPromptLock) {
|
|
||||||
alertPromptLock.notifyAll();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setOnDismissListener(dialogInterface ->
|
|
||||||
{
|
|
||||||
alertPromptResult = "";
|
|
||||||
synchronized (alertPromptLock) {
|
|
||||||
alertPromptLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (buttonConfig > 0) {
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
|
|
||||||
{
|
|
||||||
alertPromptResult = "";
|
|
||||||
synchronized (alertPromptLock) {
|
|
||||||
alertPromptLock.notifyAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int alertPromptButton() {
|
|
||||||
return alertPromptButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void exitEmulationActivity(int resultCode) {
|
|
||||||
final int Success = 0;
|
|
||||||
final int ErrorNotInitialized = 1;
|
|
||||||
final int ErrorGetLoader = 2;
|
|
||||||
final int ErrorSystemMode = 3;
|
|
||||||
final int ErrorLoader = 4;
|
|
||||||
final int ErrorLoader_ErrorEncrypted = 5;
|
|
||||||
final int ErrorLoader_ErrorInvalidFormat = 6;
|
|
||||||
final int ErrorSystemFiles = 7;
|
|
||||||
final int ShutdownRequested = 11;
|
|
||||||
final int ErrorUnknown = 12;
|
|
||||||
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int captionId = R.string.loader_error_invalid_format;
|
|
||||||
if (resultCode == ErrorLoader_ErrorEncrypted) {
|
|
||||||
captionId = R.string.loader_error_encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
|
||||||
.setTitle(captionId)
|
|
||||||
.setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
|
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
|
|
||||||
.setOnDismissListener(dialogInterface -> emulationActivity.finish());
|
|
||||||
emulationActivity.runOnUiThread(() -> {
|
|
||||||
AlertDialog alert = builder.create();
|
|
||||||
alert.show();
|
|
||||||
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setEmulationActivity(EmulationActivity emulationActivity) {
|
|
||||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
|
|
||||||
sEmulationActivity = new WeakReference<>(emulationActivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearEmulationActivity() {
|
|
||||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
|
|
||||||
|
|
||||||
sEmulationActivity.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Object cameraPermissionLock = new Object();
|
|
||||||
private static boolean cameraPermissionGranted = false;
|
|
||||||
public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
|
|
||||||
|
|
||||||
public static boolean RequestCameraPermission() {
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
// Permission already granted
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
|
|
||||||
|
|
||||||
// Wait until result is returned
|
|
||||||
synchronized (cameraPermissionLock) {
|
|
||||||
try {
|
|
||||||
cameraPermissionLock.wait();
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cameraPermissionGranted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CameraPermissionResult(boolean granted) {
|
|
||||||
cameraPermissionGranted = granted;
|
|
||||||
synchronized (cameraPermissionLock) {
|
|
||||||
cameraPermissionLock.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Object micPermissionLock = new Object();
|
|
||||||
private static boolean micPermissionGranted = false;
|
|
||||||
public static final int REQUEST_CODE_NATIVE_MIC = 900;
|
|
||||||
|
|
||||||
public static boolean RequestMicPermission() {
|
|
||||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
// Permission already granted
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
|
|
||||||
|
|
||||||
// Wait until result is returned
|
|
||||||
synchronized (micPermissionLock) {
|
|
||||||
try {
|
|
||||||
micPermissionLock.wait();
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return micPermissionGranted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void MicPermissionResult(boolean granted) {
|
|
||||||
micPermissionGranted = granted;
|
|
||||||
synchronized (micPermissionLock) {
|
|
||||||
micPermissionLock.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
|
||||||
public static native void ReloadCameraDevices();
|
|
||||||
|
|
||||||
public static native boolean LoadAmiibo(String path);
|
|
||||||
|
|
||||||
public static native void RemoveAmiibo();
|
|
||||||
|
|
||||||
public static final int SAVESTATE_SLOT_COUNT = 10;
|
|
||||||
|
|
||||||
public static final class SavestateInfo {
|
|
||||||
public int slot;
|
|
||||||
public Date time;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static native SavestateInfo[] GetSavestateInfo();
|
|
||||||
|
|
||||||
public static native void SaveState(int slot);
|
|
||||||
public static native void LoadState(int slot);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs the Citra version, Android version and, CPU.
|
|
||||||
*/
|
|
||||||
public static native void LogDeviceInfo();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button type for use in onTouchEvent
|
|
||||||
*/
|
|
||||||
public static final class ButtonType {
|
|
||||||
public static final int BUTTON_A = 700;
|
|
||||||
public static final int BUTTON_B = 701;
|
|
||||||
public static final int BUTTON_X = 702;
|
|
||||||
public static final int BUTTON_Y = 703;
|
|
||||||
public static final int BUTTON_START = 704;
|
|
||||||
public static final int BUTTON_SELECT = 705;
|
|
||||||
public static final int BUTTON_HOME = 706;
|
|
||||||
public static final int BUTTON_ZL = 707;
|
|
||||||
public static final int BUTTON_ZR = 708;
|
|
||||||
public static final int DPAD_UP = 709;
|
|
||||||
public static final int DPAD_DOWN = 710;
|
|
||||||
public static final int DPAD_LEFT = 711;
|
|
||||||
public static final int DPAD_RIGHT = 712;
|
|
||||||
public static final int STICK_LEFT = 713;
|
|
||||||
public static final int STICK_LEFT_UP = 714;
|
|
||||||
public static final int STICK_LEFT_DOWN = 715;
|
|
||||||
public static final int STICK_LEFT_LEFT = 716;
|
|
||||||
public static final int STICK_LEFT_RIGHT = 717;
|
|
||||||
public static final int STICK_C = 718;
|
|
||||||
public static final int STICK_C_UP = 719;
|
|
||||||
public static final int STICK_C_DOWN = 720;
|
|
||||||
public static final int STICK_C_LEFT = 771;
|
|
||||||
public static final int STICK_C_RIGHT = 772;
|
|
||||||
public static final int TRIGGER_L = 773;
|
|
||||||
public static final int TRIGGER_R = 774;
|
|
||||||
public static final int DPAD = 780;
|
|
||||||
public static final int BUTTON_DEBUG = 781;
|
|
||||||
public static final int BUTTON_GPIO14 = 782;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button states
|
|
||||||
*/
|
|
||||||
public static final class ButtonState {
|
|
||||||
public static final int RELEASED = 0;
|
|
||||||
public static final int PRESSED = 1;
|
|
||||||
}
|
|
||||||
public static boolean createFile(String directory, String filename) {
|
|
||||||
if (FileUtil.isNativePath(directory)) {
|
|
||||||
return CitraApplication.documentsTree.createFile(directory, filename);
|
|
||||||
}
|
|
||||||
return FileUtil.createFile(CitraApplication.getAppContext(), directory, filename) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean createDir(String directory, String directoryName) {
|
|
||||||
if (FileUtil.isNativePath(directory)) {
|
|
||||||
return CitraApplication.documentsTree.createDir(directory, directoryName);
|
|
||||||
}
|
|
||||||
return FileUtil.createDir(CitraApplication.getAppContext(), directory, directoryName) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int openContentUri(String path, String openMode) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.openContentUri(path, openMode);
|
|
||||||
}
|
|
||||||
return FileUtil.openContentUri(CitraApplication.getAppContext(), path, openMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] getFilesName(String path) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.getFilesName(path);
|
|
||||||
}
|
|
||||||
return FileUtil.getFilesName(CitraApplication.getAppContext(), path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long getSize(String path) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.getFileSize(path);
|
|
||||||
}
|
|
||||||
return FileUtil.getFileSize(CitraApplication.getAppContext(), path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean fileExists(String path) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.Exists(path);
|
|
||||||
}
|
|
||||||
return FileUtil.Exists(CitraApplication.getAppContext(), path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isDirectory(String path) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.isDirectory(path);
|
|
||||||
}
|
|
||||||
return FileUtil.isDirectory(CitraApplication.getAppContext(), path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
|
||||||
if (FileUtil.isNativePath(sourcePath) && FileUtil.isNativePath(destinationParentPath)) {
|
|
||||||
return CitraApplication.documentsTree.copyFile(sourcePath, destinationParentPath, destinationFilename);
|
|
||||||
}
|
|
||||||
return FileUtil.copyFile(CitraApplication.getAppContext(), sourcePath, destinationParentPath, destinationFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean renameFile(String path, String destinationFilename) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.renameFile(path, destinationFilename);
|
|
||||||
}
|
|
||||||
return FileUtil.renameFile(CitraApplication.getAppContext(), path, destinationFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean deleteDocument(String path) {
|
|
||||||
if (FileUtil.isNativePath(path)) {
|
|
||||||
return CitraApplication.documentsTree.deleteDocument(path);
|
|
||||||
}
|
|
||||||
return FileUtil.deleteDocument(CitraApplication.getAppContext(), path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,728 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu
|
||||||
|
|
||||||
|
import android.Manifest.permission
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.Surface
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
|
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||||
|
import org.citra.citra_emu.utils.FileUtil
|
||||||
|
import org.citra.citra_emu.utils.Log
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class which contains methods that interact
|
||||||
|
* with the native side of the Citra code.
|
||||||
|
*/
|
||||||
|
object NativeLibrary {
|
||||||
|
/**
|
||||||
|
* Default touchscreen device
|
||||||
|
*/
|
||||||
|
const val TouchScreenDevice = "Touchscreen"
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
|
||||||
|
private var alertResult = false
|
||||||
|
val alertLock = Object()
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
System.loadLibrary("citra-android")
|
||||||
|
} catch (ex: UnsatisfiedLinkError) {
|
||||||
|
Log.error("[NativeLibrary] $ex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles button press events for a gamepad.
|
||||||
|
*
|
||||||
|
* @param device The input descriptor of the gamepad.
|
||||||
|
* @param button Key code identifying which button was pressed.
|
||||||
|
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
||||||
|
* @return If we handled the button press.
|
||||||
|
*/
|
||||||
|
external fun onGamePadEvent(device: String, button: Int, action: Int): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles gamepad movement events.
|
||||||
|
*
|
||||||
|
* @param device The device ID of the gamepad.
|
||||||
|
* @param axis The axis ID
|
||||||
|
* @param xAxis The value of the x-axis represented by the given ID.
|
||||||
|
* @param yAxis The value of the y-axis represented by the given ID
|
||||||
|
*/
|
||||||
|
external fun onGamePadMoveEvent(device: String, axis: Int, xAxis: Float, yAxis: Float): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles gamepad movement events.
|
||||||
|
*
|
||||||
|
* @param device The device ID of the gamepad.
|
||||||
|
* @param axisId The axis ID
|
||||||
|
* @param axisVal The value of the axis represented by the given ID.
|
||||||
|
*/
|
||||||
|
external fun onGamePadAxisEvent(device: String?, axisId: Int, axisVal: Float): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch events.
|
||||||
|
*
|
||||||
|
* @param xAxis The value of the x-axis.
|
||||||
|
* @param yAxis The value of the y-axis
|
||||||
|
* @param pressed To identify if the touch held down or released.
|
||||||
|
* @return true if the pointer is within the touchscreen
|
||||||
|
*/
|
||||||
|
external fun onTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch movement.
|
||||||
|
*
|
||||||
|
* @param xAxis The value of the instantaneous x-axis.
|
||||||
|
* @param yAxis The value of the instantaneous y-axis.
|
||||||
|
*/
|
||||||
|
external fun onTouchMoved(xAxis: Float, yAxis: Float)
|
||||||
|
|
||||||
|
external fun reloadSettings()
|
||||||
|
|
||||||
|
external fun getTitleId(filename: String): Long
|
||||||
|
|
||||||
|
external fun getIsSystemTitle(path: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current working user directory
|
||||||
|
* If not set, it auto-detects a location
|
||||||
|
*/
|
||||||
|
external fun setUserDirectory(directory: String)
|
||||||
|
external fun getInstalledGamePaths(): Array<String?>
|
||||||
|
|
||||||
|
// Create the config.ini file.
|
||||||
|
external fun createConfigFile()
|
||||||
|
external fun createLogFile()
|
||||||
|
external fun logUserDirectory(directory: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins emulation.
|
||||||
|
*/
|
||||||
|
external fun run(path: String)
|
||||||
|
|
||||||
|
// Surface Handling
|
||||||
|
external fun surfaceChanged(surf: Surface)
|
||||||
|
external fun surfaceDestroyed()
|
||||||
|
external fun doFrame()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpauses emulation from a paused state.
|
||||||
|
*/
|
||||||
|
external fun unPauseEmulation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses emulation.
|
||||||
|
*/
|
||||||
|
external fun pauseEmulation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops emulation.
|
||||||
|
*/
|
||||||
|
external fun stopEmulation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if emulation is running (or is paused).
|
||||||
|
*/
|
||||||
|
external fun isRunning(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the title ID of the currently running title, or 0 on failure.
|
||||||
|
*/
|
||||||
|
external fun getRunningTitleId(): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the performance stats for the current game
|
||||||
|
*/
|
||||||
|
external fun getPerfStats(): DoubleArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the core emulation that the orientation has changed.
|
||||||
|
*/
|
||||||
|
external fun notifyOrientationChange(layoutOption: Int, rotation: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the top and bottom screens.
|
||||||
|
*/
|
||||||
|
external fun swapScreens(swapScreens: Boolean, rotation: Int)
|
||||||
|
|
||||||
|
external fun initializeGpuDriver(
|
||||||
|
hookLibDir: String?,
|
||||||
|
customDriverDir: String?,
|
||||||
|
customDriverName: String?,
|
||||||
|
fileRedirectDir: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
external fun areKeysAvailable(): Boolean
|
||||||
|
|
||||||
|
external fun getHomeMenuPath(region: Int): String
|
||||||
|
|
||||||
|
external fun getSystemTitleIds(systemType: Int, region: Int): LongArray
|
||||||
|
|
||||||
|
external fun downloadTitleFromNus(title: Long): InstallStatus
|
||||||
|
|
||||||
|
private var coreErrorAlertResult = false
|
||||||
|
private val coreErrorAlertLock = Object()
|
||||||
|
|
||||||
|
private fun onCoreErrorImpl(title: String, message: String) {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val fragment = CoreErrorDialogFragment.newInstance(title, message)
|
||||||
|
fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a core error.
|
||||||
|
* @return true: continue; false: abort
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun onCoreError(error: CoreError?, details: String): Boolean {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val title: String
|
||||||
|
val message: String
|
||||||
|
when (error) {
|
||||||
|
CoreError.ErrorSystemFiles -> {
|
||||||
|
title = emulationActivity.getString(R.string.system_archive_not_found)
|
||||||
|
message = emulationActivity.getString(
|
||||||
|
R.string.system_archive_not_found_message,
|
||||||
|
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreError.ErrorSavestate -> {
|
||||||
|
title = emulationActivity.getString(R.string.save_load_error)
|
||||||
|
message = details
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreError.ErrorUnknown -> {
|
||||||
|
title = emulationActivity.getString(R.string.fatal_error)
|
||||||
|
message = emulationActivity.getString(R.string.fatal_error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the AlertDialog on the main thread.
|
||||||
|
emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
|
||||||
|
|
||||||
|
// Wait for the lock to notify that it is complete.
|
||||||
|
synchronized(coreErrorAlertLock) {
|
||||||
|
try {
|
||||||
|
coreErrorAlertLock.wait()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coreErrorAlertResult
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Keep
|
||||||
|
@get:JvmStatic
|
||||||
|
val isPortraitMode: Boolean
|
||||||
|
get() = CitraApplication.appContext.resources.configuration.orientation ==
|
||||||
|
Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun displayAlertMsg(title: String, message: String, yesNo: Boolean): Boolean {
|
||||||
|
Log.error("[NativeLibrary] Alert: $message")
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
var result = false
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.")
|
||||||
|
} else {
|
||||||
|
// Show the AlertDialog on the main thread.
|
||||||
|
emulationActivity.runOnUiThread {
|
||||||
|
AlertMessageDialogFragment.newInstance(title, message, yesNo).showNow(
|
||||||
|
emulationActivity.supportFragmentManager,
|
||||||
|
AlertMessageDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the lock to notify that it is complete.
|
||||||
|
synchronized(alertLock) {
|
||||||
|
try {
|
||||||
|
alertLock.wait()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (yesNo) result = alertResult
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlertMessageDialogFragment : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create object used for waiting.
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(requireArguments().getString(TITLE))
|
||||||
|
.setMessage(requireArguments().getString(MESSAGE))
|
||||||
|
|
||||||
|
// If not yes/no dialog just have one button that dismisses modal,
|
||||||
|
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||||
|
if (!requireArguments().getBoolean(YES_NO)) {
|
||||||
|
builder
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
synchronized(alertLock) { alertLock.notify() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alertResult = false
|
||||||
|
builder
|
||||||
|
.setPositiveButton(android.R.string.yes) { _: DialogInterface, _: Int ->
|
||||||
|
alertResult = true
|
||||||
|
synchronized(alertLock) { alertLock.notify() }
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _: DialogInterface, _: Int ->
|
||||||
|
alertResult = false
|
||||||
|
synchronized(alertLock) { alertLock.notify() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "AlertMessageDialogFragment"
|
||||||
|
|
||||||
|
const val TITLE = "title"
|
||||||
|
const val MESSAGE = "message"
|
||||||
|
const val YES_NO = "yesNo"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
yesNo: Boolean
|
||||||
|
): AlertMessageDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(TITLE, title)
|
||||||
|
args.putString(MESSAGE, message)
|
||||||
|
args.putBoolean(YES_NO, yesNo)
|
||||||
|
val fragment = AlertMessageDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun exitEmulationActivity(resultCode: Int) {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emulationActivity.runOnUiThread {
|
||||||
|
EmulationErrorDialogFragment.newInstance(resultCode).showNow(
|
||||||
|
emulationActivity.supportFragmentManager,
|
||||||
|
EmulationErrorDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmulationErrorDialogFragment : DialogFragment() {
|
||||||
|
private lateinit var emulationActivity: EmulationActivity
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
emulationActivity = requireActivity() as EmulationActivity
|
||||||
|
|
||||||
|
var captionId = R.string.loader_error_invalid_format
|
||||||
|
if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) {
|
||||||
|
captionId = R.string.loader_error_encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(captionId)
|
||||||
|
.setMessage(
|
||||||
|
Html.fromHtml(
|
||||||
|
CitraApplication.appContext.resources.getString(R.string.redump_games),
|
||||||
|
Html.FROM_HTML_MODE_LEGACY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
emulationActivity.finish()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
alert.show()
|
||||||
|
|
||||||
|
val alertMessage = alert.findViewById<View>(android.R.id.message) as TextView
|
||||||
|
alertMessage.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return alert
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "EmulationErrorDialogFragment"
|
||||||
|
|
||||||
|
const val RESULT_CODE = "resultcode"
|
||||||
|
|
||||||
|
const val Success = 0
|
||||||
|
const val ErrorNotInitialized = 1
|
||||||
|
const val ErrorGetLoader = 2
|
||||||
|
const val ErrorSystemMode = 3
|
||||||
|
const val ErrorLoader = 4
|
||||||
|
const val ErrorLoader_ErrorEncrypted = 5
|
||||||
|
const val ErrorLoader_ErrorInvalidFormat = 6
|
||||||
|
const val ErrorSystemFiles = 7
|
||||||
|
const val ShutdownRequested = 11
|
||||||
|
const val ErrorUnknown = 12
|
||||||
|
|
||||||
|
fun newInstance(resultCode: Int): EmulationErrorDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(RESULT_CODE, resultCode)
|
||||||
|
val fragment = EmulationErrorDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||||
|
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
|
||||||
|
sEmulationActivity = WeakReference(emulationActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearEmulationActivity() {
|
||||||
|
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
||||||
|
sEmulationActivity.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraPermissionLock = Object()
|
||||||
|
private var cameraPermissionGranted = false
|
||||||
|
const val REQUEST_CODE_NATIVE_CAMERA = 800
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun requestCameraPermission(): Boolean {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ContextCompat.checkSelfPermission(emulationActivity, permission.CAMERA) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
// Permission already granted
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
emulationActivity.requestPermissions(arrayOf(permission.CAMERA), REQUEST_CODE_NATIVE_CAMERA)
|
||||||
|
|
||||||
|
// Wait until result is returned
|
||||||
|
synchronized(cameraPermissionLock) {
|
||||||
|
try {
|
||||||
|
cameraPermissionLock.wait()
|
||||||
|
} catch (ignored: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cameraPermissionGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cameraPermissionResult(granted: Boolean) {
|
||||||
|
cameraPermissionGranted = granted
|
||||||
|
synchronized(cameraPermissionLock) { cameraPermissionLock.notify() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val micPermissionLock = Object()
|
||||||
|
private var micPermissionGranted = false
|
||||||
|
const val REQUEST_CODE_NATIVE_MIC = 900
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun requestMicPermission(): Boolean {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity == null) {
|
||||||
|
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ContextCompat.checkSelfPermission(emulationActivity, permission.RECORD_AUDIO) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
// Permission already granted
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
emulationActivity.requestPermissions(
|
||||||
|
arrayOf(permission.RECORD_AUDIO),
|
||||||
|
REQUEST_CODE_NATIVE_MIC
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait until result is returned
|
||||||
|
synchronized(micPermissionLock) {
|
||||||
|
try {
|
||||||
|
micPermissionLock.wait()
|
||||||
|
} catch (ignored: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return micPermissionGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun micPermissionResult(granted: Boolean) {
|
||||||
|
micPermissionGranted = granted
|
||||||
|
synchronized(micPermissionLock) { micPermissionLock.notify() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||||
|
external fun reloadCameraDevices()
|
||||||
|
|
||||||
|
external fun loadAmiibo(path: String?): Boolean
|
||||||
|
|
||||||
|
external fun removeAmiibo()
|
||||||
|
|
||||||
|
const val SAVESTATE_SLOT_COUNT = 10
|
||||||
|
|
||||||
|
external fun getSavestateInfo(): Array<SaveStateInfo>?
|
||||||
|
|
||||||
|
external fun saveState(slot: Int)
|
||||||
|
|
||||||
|
external fun loadState(slot: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the Citra version, Android version and, CPU.
|
||||||
|
*/
|
||||||
|
external fun logDeviceInfo()
|
||||||
|
|
||||||
|
external fun loadSystemConfig()
|
||||||
|
|
||||||
|
external fun saveSystemConfig()
|
||||||
|
|
||||||
|
external fun setSystemSetupNeeded(needed: Boolean)
|
||||||
|
|
||||||
|
external fun getIsSystemSetupNeeded(): Boolean
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun createFile(directory: String, filename: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(directory)) {
|
||||||
|
CitraApplication.documentsTree.createFile(directory, filename)
|
||||||
|
} else {
|
||||||
|
FileUtil.createFile(directory, filename) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun createDir(directory: String, directoryName: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(directory)) {
|
||||||
|
CitraApplication.documentsTree.createDir(directory, directoryName)
|
||||||
|
} else {
|
||||||
|
FileUtil.createDir(directory, directoryName) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun openContentUri(path: String, openMode: String): Int =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.openContentUri(path, openMode)
|
||||||
|
} else {
|
||||||
|
FileUtil.openContentUri(path, openMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun getFilesName(path: String): Array<String?> =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.getFilesName(path)
|
||||||
|
} else {
|
||||||
|
FileUtil.getFilesName(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun getSize(path: String): Long =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.getFileSize(path)
|
||||||
|
} else {
|
||||||
|
FileUtil.getFileSize(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun fileExists(path: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.exists(path)
|
||||||
|
} else {
|
||||||
|
FileUtil.exists(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun isDirectory(path: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.isDirectory(path)
|
||||||
|
} else {
|
||||||
|
FileUtil.isDirectory(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun copyFile(
|
||||||
|
sourcePath: String,
|
||||||
|
destinationParentPath: String,
|
||||||
|
destinationFilename: String
|
||||||
|
): Boolean =
|
||||||
|
if (FileUtil.isNativePath(sourcePath) &&
|
||||||
|
FileUtil.isNativePath(destinationParentPath)
|
||||||
|
) {
|
||||||
|
CitraApplication.documentsTree
|
||||||
|
.copyFile(sourcePath, destinationParentPath, destinationFilename)
|
||||||
|
} else {
|
||||||
|
FileUtil.copyFile(
|
||||||
|
Uri.parse(sourcePath),
|
||||||
|
Uri.parse(destinationParentPath),
|
||||||
|
destinationFilename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun renameFile(path: String, destinationFilename: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.renameFile(path, destinationFilename)
|
||||||
|
} else {
|
||||||
|
FileUtil.renameFile(path, destinationFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun deleteDocument(path: String): Boolean =
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
CitraApplication.documentsTree.deleteDocument(path)
|
||||||
|
} else {
|
||||||
|
FileUtil.deleteDocument(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CoreError {
|
||||||
|
ErrorSystemFiles,
|
||||||
|
ErrorSavestate,
|
||||||
|
ErrorUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class InstallStatus {
|
||||||
|
Success,
|
||||||
|
ErrorFailedToOpenFile,
|
||||||
|
ErrorFileNotFound,
|
||||||
|
ErrorAborted,
|
||||||
|
ErrorInvalid,
|
||||||
|
ErrorEncrypted,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoreErrorDialogFragment : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val title = requireArguments().getString(TITLE)
|
||||||
|
val message = requireArguments().getString(MESSAGE)
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
|
||||||
|
coreErrorAlertResult = true
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
|
||||||
|
coreErrorAlertResult = false
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
coreErrorAlertResult = true
|
||||||
|
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "CoreErrorDialogFragment"
|
||||||
|
|
||||||
|
const val TITLE = "title"
|
||||||
|
const val MESSAGE = "message"
|
||||||
|
|
||||||
|
fun newInstance(title: String, message: String): CoreErrorDialogFragment {
|
||||||
|
val frag = CoreErrorDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(TITLE, title)
|
||||||
|
args.putString(MESSAGE, message)
|
||||||
|
frag.arguments = args
|
||||||
|
return frag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
class SaveStateInfo {
|
||||||
|
var slot = 0
|
||||||
|
var time: Date? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button type for use in onTouchEvent
|
||||||
|
*/
|
||||||
|
object ButtonType {
|
||||||
|
const val BUTTON_A = 700
|
||||||
|
const val BUTTON_B = 701
|
||||||
|
const val BUTTON_X = 702
|
||||||
|
const val BUTTON_Y = 703
|
||||||
|
const val BUTTON_START = 704
|
||||||
|
const val BUTTON_SELECT = 705
|
||||||
|
const val BUTTON_HOME = 706
|
||||||
|
const val BUTTON_ZL = 707
|
||||||
|
const val BUTTON_ZR = 708
|
||||||
|
const val DPAD_UP = 709
|
||||||
|
const val DPAD_DOWN = 710
|
||||||
|
const val DPAD_LEFT = 711
|
||||||
|
const val DPAD_RIGHT = 712
|
||||||
|
const val STICK_LEFT = 713
|
||||||
|
const val STICK_LEFT_UP = 714
|
||||||
|
const val STICK_LEFT_DOWN = 715
|
||||||
|
const val STICK_LEFT_LEFT = 716
|
||||||
|
const val STICK_LEFT_RIGHT = 717
|
||||||
|
const val STICK_C = 718
|
||||||
|
const val STICK_C_UP = 719
|
||||||
|
const val STICK_C_DOWN = 720
|
||||||
|
const val STICK_C_LEFT = 771
|
||||||
|
const val STICK_C_RIGHT = 772
|
||||||
|
const val TRIGGER_L = 773
|
||||||
|
const val TRIGGER_R = 774
|
||||||
|
const val DPAD = 780
|
||||||
|
const val BUTTON_DEBUG = 781
|
||||||
|
const val BUTTON_GPIO14 = 782
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button states
|
||||||
|
*/
|
||||||
|
object ButtonState {
|
||||||
|
const val RELEASED = 0
|
||||||
|
const val PRESSED = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import android.view.MenuItem;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SubMenu;
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -48,6 +49,7 @@ import org.citra.citra_emu.utils.EmulationMenuSettings;
|
|||||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
import org.citra.citra_emu.utils.FileUtil;
|
||||||
import org.citra.citra_emu.utils.ForegroundService;
|
import org.citra.citra_emu.utils.ForegroundService;
|
||||||
|
import org.citra.citra_emu.utils.Log;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -169,8 +171,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
ThemeUtil.applyTheme(this);
|
Log.gameLaunched = true;
|
||||||
|
ThemeUtil.INSTANCE.setTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
@ -210,7 +212,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
startForegroundService(foregroundService);
|
startForegroundService(foregroundService);
|
||||||
|
|
||||||
// Override Citra core INI with the one set by our in game menu
|
// Override Citra core INI with the one set by our in game menu
|
||||||
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
|
NativeLibrary.INSTANCE.swapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||||
getWindowManager().getDefaultDisplay().getRotation());
|
getWindowManager().getDefaultDisplay().getRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,15 +226,12 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
protected void restoreState(Bundle savedInstanceState) {
|
protected void restoreState(Bundle savedInstanceState) {
|
||||||
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
||||||
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
||||||
|
|
||||||
// If an alert prompt was in progress when state was restored, retry displaying it
|
|
||||||
NativeLibrary.retryDisplayAlertPrompt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRestart() {
|
public void onRestart() {
|
||||||
super.onRestart();
|
super.onRestart();
|
||||||
NativeLibrary.ReloadCameraDevices();
|
NativeLibrary.INSTANCE.reloadCameraDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -257,7 +256,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
NativeLibrary.INSTANCE.cameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||||
break;
|
break;
|
||||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
||||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||||
@ -268,7 +267,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
NativeLibrary.INSTANCE.micPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
@ -281,6 +280,10 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void enableFullscreenImmersive() {
|
private void enableFullscreenImmersive() {
|
||||||
|
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
|
||||||
|
|
||||||
getWindow().getDecorView().setSystemUiVisibility(
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
@ -323,7 +326,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void DisplaySavestateWarning() {
|
private void DisplaySavestateWarning() {
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
if (preferences.getBoolean("savestateWarningShown", false)) {
|
if (preferences.getBoolean("savestateWarningShown", false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -350,7 +353,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateSavestateMenuOptions(Menu menu) {
|
private void updateSavestateMenuOptions(Menu menu) {
|
||||||
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
|
final NativeLibrary.SaveStateInfo[] savestates = NativeLibrary.INSTANCE.getSavestateInfo();
|
||||||
if (savestates == null) {
|
if (savestates == null) {
|
||||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
||||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
||||||
@ -370,18 +373,18 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
||||||
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
||||||
DisplaySavestateWarning();
|
DisplaySavestateWarning();
|
||||||
NativeLibrary.SaveState(slot);
|
NativeLibrary.INSTANCE.saveState(slot);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
||||||
NativeLibrary.LoadState(slot);
|
NativeLibrary.INSTANCE.loadState(slot);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (final NativeLibrary.SavestateInfo info : savestates) {
|
for (final NativeLibrary.SaveStateInfo info : savestates) {
|
||||||
final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time);
|
final String text = getString(R.string.emulation_occupied_state_slot, info.getSlot(), info.getTime());
|
||||||
saveStateMenu.getItem(info.slot - 1).setTitle(text);
|
saveStateMenu.getItem(info.getSlot() - 1).setTitle(text);
|
||||||
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
|
loadStateMenu.getItem(info.getSlot() - 1).setTitle(text).setEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +444,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
EmulationMenuSettings.setSwapScreens(isEnabled);
|
||||||
item.setChecked(isEnabled);
|
item.setChecked(isEnabled);
|
||||||
|
|
||||||
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
NativeLibrary.INSTANCE.swapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||||
.getRotation());
|
.getRotation());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -491,11 +494,11 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_OPEN_CHEATS:
|
case MENU_ACTION_OPEN_CHEATS:
|
||||||
CheatsActivity.launch(this, NativeLibrary.GetRunningTitleId());
|
CheatsActivity.launch(this, NativeLibrary.INSTANCE.getRunningTitleId());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_CLOSE_GAME:
|
case MENU_ACTION_CLOSE_GAME:
|
||||||
NativeLibrary.PauseEmulation();
|
NativeLibrary.INSTANCE.pauseEmulation();
|
||||||
new MaterialAlertDialogBuilder(this)
|
new MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.emulation_close_game)
|
.setTitle(R.string.emulation_close_game)
|
||||||
.setMessage(R.string.emulation_close_game_message)
|
.setMessage(R.string.emulation_close_game_message)
|
||||||
@ -504,8 +507,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
mEmulationFragment.stopEmulation();
|
mEmulationFragment.stopEmulation();
|
||||||
finish();
|
finish();
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.UnPauseEmulation())
|
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.INSTANCE.unPauseEmulation())
|
||||||
.setOnCancelListener(dialogInterface -> NativeLibrary.UnPauseEmulation())
|
.setOnCancelListener(dialogInterface -> NativeLibrary.INSTANCE.unPauseEmulation())
|
||||||
.show();
|
.show();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -515,7 +518,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
NativeLibrary.INSTANCE.notifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||||
.getRotation());
|
.getRotation());
|
||||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
||||||
}
|
}
|
||||||
@ -558,7 +561,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
|
return NativeLibrary.INSTANCE.onGamePadEvent(input.getDescriptor(), button, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -570,7 +573,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onAmiiboSelected(String selectedFile) {
|
private void onAmiiboSelected(String selectedFile) {
|
||||||
boolean success = NativeLibrary.LoadAmiibo(selectedFile);
|
boolean success = NativeLibrary.INSTANCE.loadAmiibo(selectedFile);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
new MaterialAlertDialogBuilder(this)
|
new MaterialAlertDialogBuilder(this)
|
||||||
@ -582,7 +585,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveAmiibo() {
|
private void RemoveAmiibo() {
|
||||||
NativeLibrary.RemoveAmiibo();
|
NativeLibrary.INSTANCE.removeAmiibo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleControls() {
|
private void toggleControls() {
|
||||||
@ -725,47 +728,47 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Circle-Pad and C-Stick status
|
// Circle-Pad and C-Stick status
|
||||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||||
|
|
||||||
// Triggers L/R and ZL/ZR
|
// Triggers L/R and ZL/ZR
|
||||||
if (isTriggerPressedLMapped) {
|
if (isTriggerPressedLMapped) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (isTriggerPressedRMapped) {
|
if (isTriggerPressedRMapped) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (isTriggerPressedZLMapped) {
|
if (isTriggerPressedZLMapped) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (isTriggerPressedZRMapped) {
|
if (isTriggerPressedZRMapped) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||||
if (axisValuesDPad[0] == 0.f) {
|
if (axisValuesDPad[0] == 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (axisValuesDPad[0] < 0.f) {
|
if (axisValuesDPad[0] < 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (axisValuesDPad[0] > 0.f) {
|
if (axisValuesDPad[0] > 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||||
}
|
}
|
||||||
if (axisValuesDPad[1] == 0.f) {
|
if (axisValuesDPad[1] == 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (axisValuesDPad[1] < 0.f) {
|
if (axisValuesDPad[1] < 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||||
}
|
}
|
||||||
if (axisValuesDPad[1] > 0.f) {
|
if (axisValuesDPad[1] > 0.f) {
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.CardDriverOptionBinding
|
||||||
|
import org.citra.citra_emu.utils.GpuDriverMetadata
|
||||||
|
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||||
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
|
|
||||||
|
class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||||
|
ListAdapter<Pair<Uri, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
|
||||||
|
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
|
||||||
|
val binding =
|
||||||
|
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return DriverViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = currentList.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
|
||||||
|
holder.bind(currentList[position])
|
||||||
|
|
||||||
|
private fun onSelectDriver(position: Int) {
|
||||||
|
driverViewModel.setSelectedDriverIndex(position)
|
||||||
|
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||||
|
notifyItemChanged(driverViewModel.selectedDriver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDeleteDriver(driverData: Pair<Uri, GpuDriverMetadata>, position: Int) {
|
||||||
|
if (driverViewModel.selectedDriver > position) {
|
||||||
|
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||||
|
}
|
||||||
|
if (GpuDriverHelper.customDriverData == driverData.second) {
|
||||||
|
driverViewModel.setSelectedDriverIndex(0)
|
||||||
|
}
|
||||||
|
driverViewModel.driversToDelete.add(driverData.first)
|
||||||
|
driverViewModel.removeDriver(driverData)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
notifyItemChanged(driverViewModel.selectedDriver)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private lateinit var driverData: Pair<Uri, GpuDriverMetadata>
|
||||||
|
|
||||||
|
fun bind(driverData: Pair<Uri, GpuDriverMetadata>) {
|
||||||
|
this.driverData = driverData
|
||||||
|
val driver = driverData.second
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
|
||||||
|
root.setOnClickListener {
|
||||||
|
onSelectDriver(bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
buttonDelete.setOnClickListener {
|
||||||
|
onDeleteDriver(driverData, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay marquee by 3s
|
||||||
|
title.postDelayed(
|
||||||
|
{
|
||||||
|
title.isSelected = true
|
||||||
|
title.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
version.isSelected = true
|
||||||
|
version.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
description.isSelected = true
|
||||||
|
description.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
if (driver.name == null) {
|
||||||
|
title.setText(R.string.system_gpu_driver)
|
||||||
|
description.text = ""
|
||||||
|
version.text = ""
|
||||||
|
version.visibility = View.GONE
|
||||||
|
description.visibility = View.GONE
|
||||||
|
buttonDelete.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
title.text = driver.name
|
||||||
|
version.text = driver.version
|
||||||
|
description.text = driver.description
|
||||||
|
version.visibility = View.VISIBLE
|
||||||
|
description.visibility = View.VISIBLE
|
||||||
|
buttonDelete.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<Pair<Uri, GpuDriverMetadata>>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||||
|
newItem: Pair<Uri, GpuDriverMetadata>
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.first == newItem.first
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||||
|
newItem: Pair<Uri, GpuDriverMetadata>
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.second == newItem.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,261 +0,0 @@
|
|||||||
package org.citra.citra_emu.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DataSetObserver;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.color.MaterialColors;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
|
||||||
import org.citra.citra_emu.viewholders.GameViewHolder;
|
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
|
|
||||||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
|
|
||||||
* large dataset.
|
|
||||||
*/
|
|
||||||
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> {
|
|
||||||
private Cursor mCursor;
|
|
||||||
private GameDataSetObserver mObserver;
|
|
||||||
|
|
||||||
private boolean mDatasetValid;
|
|
||||||
private long mLastClickTime = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
|
||||||
* display no data until a Cursor is supplied by a CursorLoader.
|
|
||||||
*/
|
|
||||||
public GameAdapter() {
|
|
||||||
mDatasetValid = false;
|
|
||||||
mObserver = new GameDataSetObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager when it is necessary to create a new view.
|
|
||||||
*
|
|
||||||
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
|
||||||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
|
||||||
* @return The created ViewHolder with references to all the child view's members.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
// Create a new view.
|
|
||||||
View gameCard = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.card_game, parent, false);
|
|
||||||
|
|
||||||
gameCard.setOnClickListener(this::onClick);
|
|
||||||
gameCard.setOnLongClickListener(this::onLongClick);
|
|
||||||
|
|
||||||
// Use that view to create a ViewHolder.
|
|
||||||
return new GameViewHolder(gameCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
|
||||||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
|
||||||
* can use the view that just scrolled off the top instead of inflating a new one.)
|
|
||||||
*
|
|
||||||
* @param holder A ViewHolder representing the view we're recycling.
|
|
||||||
* @param position The position of the 'new' view in the dataset.
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
|
|
||||||
if (mDatasetValid) {
|
|
||||||
if (mCursor.moveToPosition(position)) {
|
|
||||||
PicassoUtils.loadGameIcon(holder.imageIcon,
|
|
||||||
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
|
||||||
|
|
||||||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
|
||||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
|
||||||
|
|
||||||
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
|
||||||
String filename;
|
|
||||||
if (FileUtil.isNativePath(filepath)) {
|
|
||||||
filename = CitraApplication.documentsTree.getFilename(filepath);
|
|
||||||
} else {
|
|
||||||
filename = FileUtil.getFilename(CitraApplication.getAppContext(), filepath);
|
|
||||||
}
|
|
||||||
holder.textFileName.setText(filename);
|
|
||||||
|
|
||||||
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
|
||||||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
|
||||||
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
|
||||||
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
|
|
||||||
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
|
|
||||||
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
|
|
||||||
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
|
|
||||||
|
|
||||||
final int backgroundColorId = isValidGame(holder.path) ? R.attr.colorSurface : R.attr.colorErrorContainer;
|
|
||||||
View itemView = holder.getItemView();
|
|
||||||
itemView.setBackgroundColor(MaterialColors.getColor(itemView, backgroundColorId));
|
|
||||||
} else {
|
|
||||||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the LayoutManager to find out how much data we have.
|
|
||||||
*
|
|
||||||
* @return Size of the dataset.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
if (mDatasetValid && mCursor != null) {
|
|
||||||
return mCursor.getCount();
|
|
||||||
}
|
|
||||||
Log.error("[GameAdapter] Dataset is not valid.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the contents of the _id column for a given row.
|
|
||||||
*
|
|
||||||
* @param position The row for which Android wants an ID.
|
|
||||||
* @return A valid ID from the database, or 0 if not available.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
if (mDatasetValid && mCursor != null) {
|
|
||||||
if (mCursor.moveToPosition(position)) {
|
|
||||||
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.error("[GameAdapter] Dataset is not valid.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tell Android whether or not each item in the dataset has a stable identifier.
|
|
||||||
* Which it does, because it's a database, so always tell Android 'true'.
|
|
||||||
*
|
|
||||||
* @param hasStableIds ignored.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setHasStableIds(boolean hasStableIds) {
|
|
||||||
super.setHasStableIds(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a load is finished, call this to replace the existing data with the newly-loaded
|
|
||||||
* data.
|
|
||||||
*
|
|
||||||
* @param cursor The newly-loaded Cursor.
|
|
||||||
*/
|
|
||||||
public void swapCursor(Cursor cursor) {
|
|
||||||
// Sanity check.
|
|
||||||
if (cursor == mCursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before getting rid of the old cursor, disassociate it from the Observer.
|
|
||||||
final Cursor oldCursor = mCursor;
|
|
||||||
if (oldCursor != null && mObserver != null) {
|
|
||||||
oldCursor.unregisterDataSetObserver(mObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
mCursor = cursor;
|
|
||||||
if (mCursor != null) {
|
|
||||||
// Attempt to associate the new Cursor with the Observer.
|
|
||||||
if (mObserver != null) {
|
|
||||||
mCursor.registerDataSetObserver(mObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatasetValid = true;
|
|
||||||
} else {
|
|
||||||
mDatasetValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches the game that was clicked on.
|
|
||||||
*
|
|
||||||
* @param view The view representing the game the user wants to play.
|
|
||||||
*/
|
|
||||||
private void onClick(View view) {
|
|
||||||
// Double-click prevention, using threshold of 1000 ms
|
|
||||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mLastClickTime = SystemClock.elapsedRealtime();
|
|
||||||
|
|
||||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
|
||||||
|
|
||||||
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the cheats settings for the game that was clicked on.
|
|
||||||
*
|
|
||||||
* @param view The view representing the game the user wants to play.
|
|
||||||
*/
|
|
||||||
private boolean onLongClick(View view) {
|
|
||||||
Context context = view.getContext();
|
|
||||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
|
||||||
|
|
||||||
final long titleId = NativeLibrary.GetTitleId(holder.path);
|
|
||||||
|
|
||||||
if (titleId == 0) {
|
|
||||||
new MaterialAlertDialogBuilder(context)
|
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
|
||||||
.setTitle(R.string.properties)
|
|
||||||
.setMessage(R.string.properties_not_loaded)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
} else {
|
|
||||||
CheatsActivity.launch(context, titleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidGame(String path) {
|
|
||||||
return Stream.of(
|
|
||||||
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class GameDataSetObserver extends DataSetObserver {
|
|
||||||
@Override
|
|
||||||
public void onChanged() {
|
|
||||||
super.onChanged();
|
|
||||||
|
|
||||||
mDatasetValid = true;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInvalidated() {
|
|
||||||
super.onInvalidated();
|
|
||||||
|
|
||||||
mDatasetValid = false;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,203 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
|
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||||
|
import org.citra.citra_emu.databinding.CardGameBinding
|
||||||
|
import org.citra.citra_emu.features.cheats.ui.CheatsActivity
|
||||||
|
import org.citra.citra_emu.model.Game
|
||||||
|
import org.citra.citra_emu.utils.GameIconUtils
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
|
||||||
|
class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
|
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||||
|
View.OnClickListener, View.OnLongClickListener {
|
||||||
|
private var lastClickTime = 0L
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||||
|
// Create a new view.
|
||||||
|
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
binding.cardGame.setOnClickListener(this)
|
||||||
|
binding.cardGame.setOnLongClickListener(this)
|
||||||
|
|
||||||
|
// Use that view to create a ViewHolder.
|
||||||
|
return GameViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||||
|
holder.bind(currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = currentList.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the game that was clicked on.
|
||||||
|
*
|
||||||
|
* @param view The card representing the game the user wants to play.
|
||||||
|
*/
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
// Double-click prevention, using threshold of 1000 ms
|
||||||
|
if (SystemClock.elapsedRealtime() - lastClickTime < 1000) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastClickTime = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
|
val holder = view.tag as GameViewHolder
|
||||||
|
gameExists(holder)
|
||||||
|
|
||||||
|
val preferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
preferences.edit()
|
||||||
|
.putLong(
|
||||||
|
holder.game.keyLastPlayedTime,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
EmulationActivity.launch(activity, holder.game.path, holder.game.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the cheats settings for the game that was clicked on.
|
||||||
|
*
|
||||||
|
* @param view The view representing the game the user wants to play.
|
||||||
|
*/
|
||||||
|
override fun onLongClick(view: View): Boolean {
|
||||||
|
val context = view.context
|
||||||
|
val holder = view.tag as GameViewHolder
|
||||||
|
gameExists(holder)
|
||||||
|
|
||||||
|
if (holder.game.titleId == 0L) {
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.properties)
|
||||||
|
.setMessage(R.string.properties_not_loaded)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
CheatsActivity.launch(view.context, holder.game.titleId)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triggers a library refresh if the user clicks on stale data
|
||||||
|
private fun gameExists(holder: GameViewHolder): Boolean {
|
||||||
|
if (holder.game.isInstalled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val gameExists = DocumentFile.fromSingleUri(
|
||||||
|
CitraApplication.appContext,
|
||||||
|
Uri.parse(holder.game.path)
|
||||||
|
)?.exists() == true
|
||||||
|
return if (!gameExists) {
|
||||||
|
Toast.makeText(
|
||||||
|
CitraApplication.appContext,
|
||||||
|
R.string.loader_error_file_not_found,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
lateinit var game: Game
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.cardGame.tag = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(game: Game) {
|
||||||
|
this.game = game
|
||||||
|
|
||||||
|
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
GameIconUtils.loadGameIcon(activity, game, binding.imageGameScreen)
|
||||||
|
|
||||||
|
binding.textGameTitle.visibility = if (game.title.isEmpty()) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.textCompany.visibility = if (game.company.isEmpty()) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textGameTitle.text = game.title
|
||||||
|
binding.textCompany.text = game.company
|
||||||
|
binding.textFilename.text = game.filename
|
||||||
|
|
||||||
|
val backgroundColorId =
|
||||||
|
if (
|
||||||
|
isValidGame(game.filename.substring(game.filename.lastIndexOf(".") + 1).lowercase())
|
||||||
|
) {
|
||||||
|
R.attr.colorSurface
|
||||||
|
} else {
|
||||||
|
R.attr.colorErrorContainer
|
||||||
|
}
|
||||||
|
binding.cardContents.setBackgroundColor(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.cardContents,
|
||||||
|
backgroundColorId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.textGameTitle.postDelayed(
|
||||||
|
{
|
||||||
|
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.textGameTitle.isSelected = true
|
||||||
|
|
||||||
|
binding.textCompany.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.textCompany.isSelected = true
|
||||||
|
|
||||||
|
binding.textFilename.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.textFilename.isSelected = true
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidGame(extension: String): Boolean {
|
||||||
|
return Game.badExtensions.stream()
|
||||||
|
.noneMatch { extension == it.lowercase() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
|
return oldItem.titleId == newItem.titleId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.CardHomeOptionBinding
|
||||||
|
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||||
|
import org.citra.citra_emu.model.HomeSetting
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
|
||||||
|
class HomeSettingAdapter(
|
||||||
|
private val activity: AppCompatActivity,
|
||||||
|
private val viewLifecycle: LifecycleOwner,
|
||||||
|
var options: List<HomeSetting>
|
||||||
|
) : RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), View.OnClickListener {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
|
||||||
|
val binding =
|
||||||
|
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
binding.root.setOnClickListener(this)
|
||||||
|
return HomeOptionViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return options.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
|
||||||
|
holder.bind(options[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
val holder = view.tag as HomeOptionViewHolder
|
||||||
|
if (holder.option.isEnabled.invoke()) {
|
||||||
|
holder.option.onClick.invoke()
|
||||||
|
} else {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
holder.option.disabledTitleId,
|
||||||
|
holder.option.disabledMessageId
|
||||||
|
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
lateinit var option: HomeSetting
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.tag = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(option: HomeSetting) {
|
||||||
|
this.option = option
|
||||||
|
|
||||||
|
binding.optionTitle.text = activity.resources.getString(option.titleId)
|
||||||
|
binding.optionDescription.text = activity.resources.getString(option.descriptionId)
|
||||||
|
binding.optionIcon.setImageDrawable(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
activity.resources,
|
||||||
|
option.iconId,
|
||||||
|
activity.theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
viewLifecycle.lifecycleScope.launch {
|
||||||
|
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
option.details.collect { updateOptionDetails(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.optionDetail.postDelayed(
|
||||||
|
{
|
||||||
|
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.optionDetail.isSelected = true
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
if (option.isEnabled.invoke()) {
|
||||||
|
binding.optionTitle.alpha = 1f
|
||||||
|
binding.optionDescription.alpha = 1f
|
||||||
|
binding.optionIcon.alpha = 1f
|
||||||
|
} else {
|
||||||
|
binding.optionTitle.alpha = 0.5f
|
||||||
|
binding.optionDescription.alpha = 0.5f
|
||||||
|
binding.optionIcon.alpha = 0.5f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOptionDetails(detailString: String) {
|
||||||
|
if (detailString != "") {
|
||||||
|
binding.optionDetail.text = detailString
|
||||||
|
binding.optionDetail.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.databinding.ListItemSettingBinding
|
||||||
|
import org.citra.citra_emu.fragments.LicenseBottomSheetDialogFragment
|
||||||
|
import org.citra.citra_emu.model.License
|
||||||
|
|
||||||
|
class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
|
||||||
|
RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
|
||||||
|
View.OnClickListener {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
|
||||||
|
val binding =
|
||||||
|
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
binding.root.setOnClickListener(this)
|
||||||
|
return LicenseViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = licenses.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
|
||||||
|
holder.bind(licenses[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
val license = (view.tag as LicenseViewHolder).license
|
||||||
|
LicenseBottomSheetDialogFragment.newInstance(license)
|
||||||
|
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
|
||||||
|
lateinit var license: License
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.tag = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(license: License) {
|
||||||
|
this.license = license
|
||||||
|
|
||||||
|
val context = CitraApplication.appContext
|
||||||
|
binding.textSettingName.text = context.getString(license.titleId)
|
||||||
|
binding.textSettingDescription.text = context.getString(license.descriptionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.adapters
|
||||||
|
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import org.citra.citra_emu.databinding.PageSetupBinding
|
||||||
|
import org.citra.citra_emu.model.SetupCallback
|
||||||
|
import org.citra.citra_emu.model.SetupPage
|
||||||
|
import org.citra.citra_emu.model.StepState
|
||||||
|
import org.citra.citra_emu.utils.ViewUtils
|
||||||
|
|
||||||
|
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||||
|
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
|
||||||
|
val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return SetupPageViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = pages.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
|
||||||
|
holder.bind(pages[position])
|
||||||
|
|
||||||
|
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root), SetupCallback {
|
||||||
|
lateinit var page: SetupPage
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.tag = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(page: SetupPage) {
|
||||||
|
this.page = page
|
||||||
|
|
||||||
|
if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) {
|
||||||
|
onStepCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.icon.setImageDrawable(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
activity.resources,
|
||||||
|
page.iconId,
|
||||||
|
activity.theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.textTitle.text = activity.resources.getString(page.titleId)
|
||||||
|
binding.textDescription.text =
|
||||||
|
Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
|
||||||
|
binding.textDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
|
binding.buttonAction.apply {
|
||||||
|
text = activity.resources.getString(page.buttonTextId)
|
||||||
|
if (page.buttonIconId != 0) {
|
||||||
|
icon = ResourcesCompat.getDrawable(
|
||||||
|
activity.resources,
|
||||||
|
page.buttonIconId,
|
||||||
|
activity.theme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconGravity =
|
||||||
|
if (page.leftAlignedIcon) {
|
||||||
|
MaterialButton.ICON_GRAVITY_START
|
||||||
|
} else {
|
||||||
|
MaterialButton.ICON_GRAVITY_END
|
||||||
|
}
|
||||||
|
setOnClickListener {
|
||||||
|
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStepCompleted() {
|
||||||
|
ViewUtils.hideView(binding.buttonAction, 200)
|
||||||
|
ViewUtils.showView(binding.textConfirmation, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,13 +18,16 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
@Keep
|
||||||
public final class MiiSelector {
|
public final class MiiSelector {
|
||||||
|
@Keep
|
||||||
public static class MiiSelectorConfig implements java.io.Serializable {
|
public static class MiiSelectorConfig implements java.io.Serializable {
|
||||||
public boolean enable_cancel_button;
|
public boolean enable_cancel_button;
|
||||||
public String title;
|
public String title;
|
||||||
|
|||||||
@ -7,13 +7,17 @@ package org.citra.citra_emu.applets;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.InputFilter;
|
import android.text.InputFilter;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.Keep;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
@ -29,6 +33,7 @@ import org.citra.citra_emu.utils.Log;
|
|||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Keep
|
||||||
public final class SoftwareKeyboard {
|
public final class SoftwareKeyboard {
|
||||||
/// Corresponds to Frontend::ButtonConfig
|
/// Corresponds to Frontend::ButtonConfig
|
||||||
private interface ButtonConfig {
|
private interface ButtonConfig {
|
||||||
@ -57,6 +62,7 @@ public final class SoftwareKeyboard {
|
|||||||
EmptyInputNotAllowed,
|
EmptyInputNotAllowed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
public static class KeyboardConfig implements java.io.Serializable {
|
public static class KeyboardConfig implements java.io.Serializable {
|
||||||
public int button_config;
|
public int button_config;
|
||||||
public int max_text_length;
|
public int max_text_length;
|
||||||
@ -109,20 +115,27 @@ public final class SoftwareKeyboard {
|
|||||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
params.leftMargin = params.rightMargin =
|
params.leftMargin = params.rightMargin =
|
||||||
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
|
CitraApplication.Companion.getAppContext().getResources().getDimensionPixelSize(
|
||||||
R.dimen.dialog_margin);
|
R.dimen.dialog_margin);
|
||||||
|
|
||||||
KeyboardConfig config = Objects.requireNonNull(
|
KeyboardConfig config = Objects.requireNonNull(
|
||||||
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
|
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
|
||||||
|
|
||||||
// Set up the input
|
// Set up the input
|
||||||
EditText editText = new EditText(CitraApplication.getAppContext());
|
EditText editText = new EditText(CitraApplication.Companion.getAppContext());
|
||||||
editText.setHint(config.hint_text);
|
editText.setHint(config.hint_text);
|
||||||
editText.setSingleLine(!config.multiline_mode);
|
editText.setSingleLine(!config.multiline_mode);
|
||||||
editText.setLayoutParams(params);
|
editText.setLayoutParams(params);
|
||||||
editText.setFilters(new InputFilter[]{
|
editText.setFilters(new InputFilter[]{
|
||||||
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
|
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
|
||||||
|
|
||||||
|
TypedValue typedValue = new TypedValue();
|
||||||
|
Resources.Theme theme = requireContext().getTheme();
|
||||||
|
theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true);
|
||||||
|
@ColorInt int color = typedValue.data;
|
||||||
|
editText.setHintTextColor(color);
|
||||||
|
editText.setTextColor(color);
|
||||||
|
|
||||||
FrameLayout container = new FrameLayout(emulationActivity);
|
FrameLayout container = new FrameLayout(emulationActivity);
|
||||||
container.addView(editText);
|
container.addView(editText);
|
||||||
|
|
||||||
@ -256,7 +269,7 @@ public final class SoftwareKeyboard {
|
|||||||
|
|
||||||
public static void ShowError(String error) {
|
public static void ShowError(String error) {
|
||||||
NativeLibrary.displayAlertMsg(
|
NativeLibrary.displayAlertMsg(
|
||||||
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
|
CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||||
error, false);
|
error, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import org.citra.citra_emu.R;
|
|||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
import org.citra.citra_emu.utils.PicassoUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
// Used in native code.
|
// Used in native code.
|
||||||
@ -23,6 +24,7 @@ public final class StillImageCameraHelper {
|
|||||||
String filePickerPath;
|
String filePickerPath;
|
||||||
|
|
||||||
// Opens file picker for camera.
|
// Opens file picker for camera.
|
||||||
|
@Keep
|
||||||
public static @Nullable
|
public static @Nullable
|
||||||
String OpenFilePicker() {
|
String OpenFilePicker() {
|
||||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||||
@ -58,6 +60,7 @@ public final class StillImageCameraHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||||
|
@Keep
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
||||||
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
package org.citra.citra_emu.dialogs;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
import java.util.Objects;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
|
||||||
|
|
||||||
public class CitraDirectoryDialog extends DialogFragment {
|
|
||||||
public static final String TAG = "citra_directory_dialog_fragment";
|
|
||||||
|
|
||||||
private static final String MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE";
|
|
||||||
|
|
||||||
TextView pathView;
|
|
||||||
|
|
||||||
TextView spaceView;
|
|
||||||
|
|
||||||
CheckBox checkBox;
|
|
||||||
|
|
||||||
AlertDialog dialog;
|
|
||||||
|
|
||||||
Listener listener;
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void onPressPositiveButton(boolean moveData, Uri path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CitraDirectoryDialog newInstance(String path, Listener listener) {
|
|
||||||
CitraDirectoryDialog frag = new CitraDirectoryDialog();
|
|
||||||
frag.listener = listener;
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString("path", path);
|
|
||||||
frag.setArguments(args);
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final FragmentActivity activity = requireActivity();
|
|
||||||
final Uri path = Uri.parse(Objects.requireNonNull(requireArguments().getString("path")));
|
|
||||||
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
String freeSpaceText =
|
|
||||||
getResources().getString(R.string.free_space, FileUtil.getFreeSpace(activity, path));
|
|
||||||
|
|
||||||
LayoutInflater inflater = getLayoutInflater();
|
|
||||||
View view = inflater.inflate(R.layout.dialog_citra_directory, null);
|
|
||||||
|
|
||||||
checkBox = view.findViewById(R.id.checkBox);
|
|
||||||
pathView = view.findViewById(R.id.path);
|
|
||||||
spaceView = view.findViewById(R.id.space);
|
|
||||||
|
|
||||||
checkBox.setChecked(mPreferences.getBoolean(MOVE_DATE_ENABLE, true));
|
|
||||||
if (!PermissionsHandler.hasWriteAccess(activity)) {
|
|
||||||
checkBox.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
checkBox.setOnCheckedChangeListener(
|
|
||||||
(v, isChecked)
|
|
||||||
// record move data selection with SharedPreferences
|
|
||||||
-> mPreferences.edit().putBoolean(MOVE_DATE_ENABLE, checkBox.isChecked()).apply());
|
|
||||||
|
|
||||||
pathView.setText(path.getPath());
|
|
||||||
spaceView.setText(freeSpaceText);
|
|
||||||
|
|
||||||
setCancelable(false);
|
|
||||||
|
|
||||||
dialog = new MaterialAlertDialogBuilder(activity)
|
|
||||||
.setView(view)
|
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
|
||||||
.setTitle(R.string.app_name)
|
|
||||||
.setPositiveButton(
|
|
||||||
android.R.string.ok,
|
|
||||||
(d, v) -> listener.onPressPositiveButton(checkBox.isChecked(), path))
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create();
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package org.citra.citra_emu.dialogs;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
|
|
||||||
public class CopyDirProgressDialog extends DialogFragment {
|
|
||||||
public static final String TAG = "copy_dir_progress_dialog";
|
|
||||||
ProgressBar progressBar;
|
|
||||||
|
|
||||||
TextView progressText;
|
|
||||||
|
|
||||||
AlertDialog dialog;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final FragmentActivity activity = requireActivity();
|
|
||||||
|
|
||||||
LayoutInflater inflater = getLayoutInflater();
|
|
||||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
|
||||||
|
|
||||||
progressBar = view.findViewById(R.id.progress_bar);
|
|
||||||
progressText = view.findViewById(R.id.progress_text);
|
|
||||||
progressText.setText("");
|
|
||||||
|
|
||||||
setCancelable(false);
|
|
||||||
|
|
||||||
dialog = new MaterialAlertDialogBuilder(activity)
|
|
||||||
.setView(view)
|
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
|
||||||
.setTitle(R.string.move_data)
|
|
||||||
.setMessage("")
|
|
||||||
.create();
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdateSearchProgress(String msg) {
|
|
||||||
requireActivity().runOnUiThread(() -> {
|
|
||||||
dialog.setMessage(getResources().getString(R.string.searching_direcotry, msg));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdateCopyProgress(String msg, int progress, int max) {
|
|
||||||
requireActivity().runOnUiThread(() -> {
|
|
||||||
progressBar.setProgress(progress);
|
|
||||||
progressBar.setMax(max);
|
|
||||||
progressText.setText(String.format("%d/%d", progress, max));
|
|
||||||
dialog.setMessage(getResources().getString(R.string.copy_file_name, msg));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -51,8 +51,7 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
ThemeUtil.applyTheme(this);
|
ThemeUtil.INSTANCE.setTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|||||||
@ -14,7 +14,12 @@ import java.util.Map;
|
|||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
public static final String SECTION_PREMIUM = "Premium";
|
public static final String PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch";
|
||||||
|
public static final String PREF_MATERIAL_YOU = "MaterialYouTheme";
|
||||||
|
public static final String PREF_THEME_MODE = "ThemeMode";
|
||||||
|
public static final String PREF_BLACK_BACKGROUNDS = "BlackBackgrounds";
|
||||||
|
public static final String PREF_SHOW_HOME_APPS = "ShowHomeApps";
|
||||||
|
|
||||||
public static final String SECTION_CORE = "Core";
|
public static final String SECTION_CORE = "Core";
|
||||||
public static final String SECTION_SYSTEM = "System";
|
public static final String SECTION_SYSTEM = "System";
|
||||||
public static final String SECTION_CAMERA = "Camera";
|
public static final String SECTION_CAMERA = "Camera";
|
||||||
@ -30,7 +35,7 @@ public class Settings {
|
|||||||
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
|
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
|
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +114,7 @@ public class Settings {
|
|||||||
|
|
||||||
public void saveSettings(SettingsActivityView view) {
|
public void saveSettings(SettingsActivityView view) {
|
||||||
if (TextUtils.isEmpty(gameId)) {
|
if (TextUtils.isEmpty(gameId)) {
|
||||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false);
|
view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.ini_saved), false);
|
||||||
|
|
||||||
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
|
||||||
String fileName = entry.getKey();
|
String fileName = entry.getKey();
|
||||||
@ -121,12 +126,6 @@ public class Settings {
|
|||||||
|
|
||||||
SettingsFile.saveFile(fileName, iniSections, view);
|
SettingsFile.saveFile(fileName, iniSections, view);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// custom game settings
|
|
||||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false);
|
|
||||||
|
|
||||||
SettingsFile.saveCustomGameSettings(gameId, sections);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,7 +59,7 @@ public final class CheckBoxSetting extends SettingsItem {
|
|||||||
public IntSetting setChecked(boolean checked) {
|
public IntSetting setChecked(boolean checked) {
|
||||||
// Show a performance warning if the setting has been disabled
|
// Show a performance warning if the setting has been disabled
|
||||||
if (mShowPerformanceWarning && !checked) {
|
if (mShowPerformanceWarning && !checked) {
|
||||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true);
|
mView.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.performance_warning), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSetting() == null) {
|
if (getSetting() == null) {
|
||||||
|
|||||||
@ -201,7 +201,7 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
*/
|
*/
|
||||||
public void removeOldMapping() {
|
public void removeOldMapping() {
|
||||||
// Get preferences editor
|
// Get preferences editor
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
|
||||||
// Try remove all possible keys we wrote for this setting
|
// Try remove all possible keys we wrote for this setting
|
||||||
@ -250,7 +250,7 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
*/
|
*/
|
||||||
private void WriteButtonMapping(String key) {
|
private void WriteButtonMapping(String key) {
|
||||||
// Get preferences editor
|
// Get preferences editor
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
|
||||||
// Remove mapping for another setting using this input
|
// Remove mapping for another setting using this input
|
||||||
@ -278,7 +278,7 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
*/
|
*/
|
||||||
private void WriteAxisMapping(int axis, int value) {
|
private void WriteAxisMapping(int axis, int value) {
|
||||||
// Get preferences editor
|
// Get preferences editor
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
|
||||||
// Cleanup old mapping
|
// Cleanup old mapping
|
||||||
@ -302,7 +302,7 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
*/
|
*/
|
||||||
public void onKeyInput(KeyEvent keyEvent) {
|
public void onKeyInput(KeyEvent keyEvent) {
|
||||||
if (!IsButtonMappingSupported()) {
|
if (!IsButtonMappingSupported()) {
|
||||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
|
Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,11 +324,11 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
|
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
|
||||||
char axisDir) {
|
char axisDir) {
|
||||||
if (!IsAxisMappingSupported()) {
|
if (!IsAxisMappingSupported()) {
|
||||||
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
|
Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
|
||||||
int button;
|
int button;
|
||||||
@ -354,7 +354,7 @@ public final class InputBindingSetting extends SettingsItem {
|
|||||||
* Sets the string to use in the configuration UI for the gamepad input.
|
* Sets the string to use in the configuration UI for the gamepad input.
|
||||||
*/
|
*/
|
||||||
private StringSetting setUiString(String ui) {
|
private StringSetting setUiString(String ui) {
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
|
||||||
if (getSetting() == null) {
|
if (getSetting() == null) {
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
package org.citra.citra_emu.features.settings.model.view;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.features.settings.model.Setting;
|
|
||||||
|
|
||||||
public final class PremiumHeader extends SettingsItem {
|
|
||||||
public PremiumHeader() {
|
|
||||||
super(null, null, null, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getType() {
|
|
||||||
return SettingsItem.TYPE_PREMIUM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package org.citra.citra_emu.features.settings.model.view;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.settings.model.Setting;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
|
||||||
|
|
||||||
public final class PremiumSingleChoiceSetting extends SettingsItem {
|
|
||||||
private int mDefaultValue;
|
|
||||||
|
|
||||||
private int mChoicesId;
|
|
||||||
private int mValuesId;
|
|
||||||
private SettingsFragmentView mView;
|
|
||||||
|
|
||||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
|
||||||
|
|
||||||
public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
|
|
||||||
int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) {
|
|
||||||
super(key, section, setting, titleId, descriptionId);
|
|
||||||
mValuesId = valuesId;
|
|
||||||
mChoicesId = choicesId;
|
|
||||||
mDefaultValue = defaultValue;
|
|
||||||
mView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getChoicesId() {
|
|
||||||
return mChoicesId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getValuesId() {
|
|
||||||
return mValuesId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedValue() {
|
|
||||||
return mPreferences.getInt(getKey(), mDefaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a value to the backing int. If that int was previously null,
|
|
||||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
|
||||||
*
|
|
||||||
* @param selection New value of the int.
|
|
||||||
* @return null if overwritten successfully otherwise; a newly created IntSetting.
|
|
||||||
*/
|
|
||||||
public void setSelectedValue(int selection) {
|
|
||||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
|
||||||
editor.putInt(getKey(), selection);
|
|
||||||
editor.apply();
|
|
||||||
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getType() {
|
|
||||||
return TYPE_SINGLE_CHOICE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,7 +20,6 @@ public abstract class SettingsItem {
|
|||||||
public static final int TYPE_INPUT_BINDING = 5;
|
public static final int TYPE_INPUT_BINDING = 5;
|
||||||
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
|
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
|
||||||
public static final int TYPE_DATETIME_SETTING = 7;
|
public static final int TYPE_DATETIME_SETTING = 7;
|
||||||
public static final int TYPE_PREMIUM = 8;
|
|
||||||
|
|
||||||
private String mKey;
|
private String mKey;
|
||||||
private String mSection;
|
private String mSection;
|
||||||
@ -29,7 +28,6 @@ public abstract class SettingsItem {
|
|||||||
|
|
||||||
private int mNameId;
|
private int mNameId;
|
||||||
private int mDescriptionId;
|
private int mDescriptionId;
|
||||||
private boolean mIsPremium;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
|
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
|
||||||
@ -48,7 +46,6 @@ public abstract class SettingsItem {
|
|||||||
mSetting = setting;
|
mSetting = setting;
|
||||||
mNameId = nameId;
|
mNameId = nameId;
|
||||||
mDescriptionId = descriptionId;
|
mDescriptionId = descriptionId;
|
||||||
mIsPremium = (section == Settings.SECTION_PREMIUM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,10 +90,6 @@ public abstract class SettingsItem {
|
|||||||
return mDescriptionId;
|
return mDescriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPremium() {
|
|
||||||
return mIsPremium;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
|
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
|
||||||
* method to determine which type of ViewHolder should be created.
|
* method to determine which type of ViewHolder should be created.
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import com.google.android.material.appbar.MaterialToolbar;
|
|||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||||
import org.citra.citra_emu.utils.InsetsHelper;
|
import org.citra.citra_emu.utils.InsetsHelper;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
@ -48,8 +47,7 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
ThemeUtil.applyTheme(this);
|
ThemeUtil.INSTANCE.setTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
|
|
||||||
@ -109,7 +107,7 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
mPresenter.onStop(isFinishing());
|
mPresenter.onStop(isFinishing());
|
||||||
|
|
||||||
// Update framebuffer layout when closing the settings
|
// Update framebuffer layout when closing the settings
|
||||||
NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
|
NativeLibrary.INSTANCE.notifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
|
||||||
getWindowManager().getDefaultDisplay().getRotation());
|
getWindowManager().getDefaultDisplay().getRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,19 +145,6 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
return duration != 0 && transition != 0;
|
return duration != 0 && transition != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) {
|
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
|
||||||
receiver,
|
|
||||||
filter);
|
|
||||||
DirectoryInitialization.start(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) {
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
if (dialog == null) {
|
if (dialog == null) {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import org.citra.citra_emu.features.settings.model.Settings;
|
|||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
@ -24,8 +23,6 @@ public final class SettingsActivityPresenter {
|
|||||||
|
|
||||||
private boolean mShouldSave;
|
private boolean mShouldSave;
|
||||||
|
|
||||||
private DirectoryStateReceiver directoryStateReceiver;
|
|
||||||
|
|
||||||
private String menuTag;
|
private String menuTag;
|
||||||
private String gameId;
|
private String gameId;
|
||||||
|
|
||||||
@ -64,30 +61,7 @@ public final class SettingsActivityPresenter {
|
|||||||
if (configFile == null || !configFile.exists()) {
|
if (configFile == null || !configFile.exists()) {
|
||||||
Log.error("Citra config file could not be found!");
|
Log.error("Citra config file could not be found!");
|
||||||
}
|
}
|
||||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
loadSettingsUI();
|
||||||
loadSettingsUI();
|
|
||||||
} else {
|
|
||||||
mView.showLoading();
|
|
||||||
IntentFilter statusIntentFilter = new IntentFilter(
|
|
||||||
DirectoryInitialization.BROADCAST_ACTION);
|
|
||||||
|
|
||||||
directoryStateReceiver =
|
|
||||||
new DirectoryStateReceiver(directoryInitializationState ->
|
|
||||||
{
|
|
||||||
if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
|
||||||
mView.hideLoading();
|
|
||||||
loadSettingsUI();
|
|
||||||
} else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
|
||||||
mView.showPermissionNeededHint();
|
|
||||||
mView.hideLoading();
|
|
||||||
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
|
||||||
mView.showExternalStorageNotMountedHint();
|
|
||||||
mView.hideLoading();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSettings(Settings settings) {
|
public void setSettings(Settings settings) {
|
||||||
@ -99,17 +73,12 @@ public final class SettingsActivityPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onStop(boolean finishing) {
|
public void onStop(boolean finishing) {
|
||||||
if (directoryStateReceiver != null) {
|
|
||||||
mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
|
|
||||||
directoryStateReceiver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mSettings != null && finishing && mShouldSave) {
|
if (mSettings != null && finishing && mShouldSave) {
|
||||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
|
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
|
||||||
mSettings.saveSettings(mView);
|
mSettings.saveSettings(mView);
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.ReloadSettings();
|
NativeLibrary.INSTANCE.reloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSettingChanged() {
|
public void onSettingChanged() {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package org.citra.citra_emu.features.settings.ui;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
|
||||||
import org.citra.citra_emu.features.settings.model.Settings;
|
import org.citra.citra_emu.features.settings.model.Settings;
|
||||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction for the Activity that manages SettingsFragments.
|
* Abstraction for the Activity that manages SettingsFragments.
|
||||||
@ -85,19 +84,4 @@ public interface SettingsActivityView {
|
|||||||
* Show a hint to the user that the app needs the external storage to be mounted
|
* Show a hint to the user that the app needs the external storage to be mounted
|
||||||
*/
|
*/
|
||||||
void showExternalStorageNotMountedHint();
|
void showExternalStorageNotMountedHint();
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the DirectoryInitialization and listen for the result.
|
|
||||||
*
|
|
||||||
* @param receiver the broadcast receiver for the DirectoryInitialization
|
|
||||||
* @param filter the Intent broadcasts to be received.
|
|
||||||
*/
|
|
||||||
void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop listening to the DirectoryInitialization.
|
|
||||||
*
|
|
||||||
* @param receiver The broadcast receiver to unregister.
|
|
||||||
*/
|
|
||||||
void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import org.citra.citra_emu.features.settings.model.StringSetting;
|
|||||||
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||||
@ -34,12 +33,10 @@ import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHo
|
|||||||
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
|
||||||
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
|
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
|
||||||
import org.citra.citra_emu.ui.main.MainActivity;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -97,10 +94,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||||
return new DateTimeViewHolder(view, this);
|
return new DateTimeViewHolder(view, this);
|
||||||
|
|
||||||
case SettingsItem.TYPE_PREMIUM:
|
|
||||||
view = inflater.inflate(R.layout.premium_item_setting, parent, false);
|
|
||||||
return new PremiumViewHolder(view, this, mView);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
|
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
|
||||||
return null;
|
return null;
|
||||||
@ -146,17 +139,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
mView.onSettingChanged();
|
mView.onSettingChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item) {
|
|
||||||
mClickedItem = item;
|
|
||||||
|
|
||||||
int value = getSelectionForSingleChoiceValue(item);
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
|
|
||||||
.setTitle(item.getNameId())
|
|
||||||
.setSingleChoiceItems(item.getChoicesId(), value, this);
|
|
||||||
mDialog = builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onSingleChoiceClick(SingleChoiceSetting item) {
|
public void onSingleChoiceClick(SingleChoiceSetting item) {
|
||||||
mClickedItem = item;
|
mClickedItem = item;
|
||||||
|
|
||||||
@ -170,28 +152,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
|
|
||||||
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
||||||
mClickedPosition = position;
|
mClickedPosition = position;
|
||||||
|
onSingleChoiceClick(item);
|
||||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
|
||||||
// Setting is either not Premium, or the user has Premium
|
|
||||||
onSingleChoiceClick(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User needs Premium, invoke the billing flow
|
|
||||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) {
|
|
||||||
mClickedPosition = position;
|
|
||||||
|
|
||||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
|
||||||
// Setting is either not Premium, or the user has Premium
|
|
||||||
onSingleChoiceClick(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User needs Premium, invoke the billing flow
|
|
||||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
|
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
|
||||||
@ -205,15 +166,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
|
|
||||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
|
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
|
||||||
mClickedPosition = position;
|
mClickedPosition = position;
|
||||||
|
onStringSingleChoiceClick(item);
|
||||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
|
||||||
// Setting is either not Premium, or the user has Premium
|
|
||||||
onStringSingleChoiceClick(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User needs Premium, invoke the billing flow
|
|
||||||
MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
|
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
|
||||||
@ -351,10 +304,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
mView.putSetting(setting);
|
mView.putSetting(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDialog();
|
|
||||||
} else if (mClickedItem instanceof PremiumSingleChoiceSetting) {
|
|
||||||
PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem;
|
|
||||||
scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which));
|
|
||||||
closeDialog();
|
closeDialog();
|
||||||
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
|
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
|
||||||
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
|
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
|
||||||
@ -417,17 +366,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) {
|
|
||||||
int valuesId = item.getValuesId();
|
|
||||||
|
|
||||||
if (valuesId > 0) {
|
|
||||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
|
||||||
return valuesArray[which];
|
|
||||||
} else {
|
|
||||||
return which;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
|
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
|
||||||
int value = item.getSelectedValue();
|
int value = item.getSelectedValue();
|
||||||
int valuesId = item.getValuesId();
|
int valuesId = item.getValuesId();
|
||||||
@ -447,25 +385,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) {
|
|
||||||
int value = item.getSelectedValue();
|
|
||||||
int valuesId = item.getValuesId();
|
|
||||||
|
|
||||||
if (valuesId > 0) {
|
|
||||||
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
|
|
||||||
for (int index = 0; index < valuesArray.length; index++) {
|
|
||||||
int current = valuesArray[index];
|
|
||||||
if (current == value) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
|
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
|
||||||
mSliderProgress = (int) value;
|
mSliderProgress = (int) value;
|
||||||
|
|||||||
@ -17,8 +17,6 @@ import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
|
|||||||
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
|
import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||||
@ -107,9 +105,6 @@ public final class SettingsFragmentPresenter {
|
|||||||
case SettingsFile.FILE_NAME_CONFIG:
|
case SettingsFile.FILE_NAME_CONFIG:
|
||||||
addConfigSettings(sl);
|
addConfigSettings(sl);
|
||||||
break;
|
break;
|
||||||
case Settings.SECTION_PREMIUM:
|
|
||||||
addPremiumSettings(sl);
|
|
||||||
break;
|
|
||||||
case Settings.SECTION_CORE:
|
case Settings.SECTION_CORE:
|
||||||
addGeneralSettings(sl);
|
addGeneralSettings(sl);
|
||||||
break;
|
break;
|
||||||
@ -143,7 +138,6 @@ public final class SettingsFragmentPresenter {
|
|||||||
private void addConfigSettings(ArrayList<SettingsItem> sl) {
|
private void addConfigSettings(ArrayList<SettingsItem> sl) {
|
||||||
mView.getActivity().setTitle(R.string.preferences_settings);
|
mView.getActivity().setTitle(R.string.preferences_settings);
|
||||||
|
|
||||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM));
|
|
||||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
|
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
|
||||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
|
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
|
||||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
|
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
|
||||||
@ -153,25 +147,6 @@ public final class SettingsFragmentPresenter {
|
|||||||
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
|
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPremiumSettings(ArrayList<SettingsItem> sl) {
|
|
||||||
mView.getActivity().setTitle(R.string.preferences_premium);
|
|
||||||
|
|
||||||
SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
|
|
||||||
Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
|
|
||||||
|
|
||||||
sl.add(new PremiumHeader());
|
|
||||||
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
|
||||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView));
|
|
||||||
} else {
|
|
||||||
// Pre-Android 10 does not support System Default
|
|
||||||
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView));
|
|
||||||
}
|
|
||||||
|
|
||||||
Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
|
|
||||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
|
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
|
||||||
mView.getActivity().setTitle(R.string.preferences_general);
|
mView.getActivity().setTitle(R.string.preferences_general);
|
||||||
|
|
||||||
@ -367,6 +342,7 @@ public final class SettingsFragmentPresenter {
|
|||||||
Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
|
Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
|
||||||
Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
|
Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
|
||||||
Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
|
Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
|
||||||
|
Setting textureFilterName = rendererSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
|
||||||
SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
|
SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
|
||||||
Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
|
Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
|
||||||
Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
|
Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
|
||||||
@ -385,6 +361,7 @@ public final class SettingsFragmentPresenter {
|
|||||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
|
sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
|
||||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
|
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
|
||||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
|
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
|
||||||
|
sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_RENDERER, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
|
||||||
|
|
||||||
sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
|
sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
|
||||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
|
sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
|
||||||
import org.citra.citra_emu.ui.main.MainActivity;
|
|
||||||
|
|
||||||
public final class PremiumViewHolder extends SettingViewHolder {
|
|
||||||
private TextView mHeaderName;
|
|
||||||
private TextView mTextDescription;
|
|
||||||
private SettingsFragmentView mView;
|
|
||||||
|
|
||||||
public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) {
|
|
||||||
super(itemView, adapter);
|
|
||||||
mView = view;
|
|
||||||
itemView.setOnClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void findViews(View root) {
|
|
||||||
mHeaderName = root.findViewById(R.id.text_setting_name);
|
|
||||||
mTextDescription = root.findViewById(R.id.text_setting_description);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bind(SettingsItem item) {
|
|
||||||
updateText();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View clicked) {
|
|
||||||
if (MainActivity.isPremiumActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke billing flow if Premium is not already active, then refresh the UI to indicate
|
|
||||||
// the purchase has completed.
|
|
||||||
MainActivity.invokePremiumBilling(() -> updateText());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the text shown to the user, based on whether Premium is active
|
|
||||||
*/
|
|
||||||
private void updateText() {
|
|
||||||
if (MainActivity.isPremiumActive()) {
|
|
||||||
mHeaderName.setText(R.string.premium_settings_welcome);
|
|
||||||
mTextDescription.setText(R.string.premium_settings_welcome_description);
|
|
||||||
} else {
|
|
||||||
mHeaderName.setText(R.string.premium_settings_upsell);
|
|
||||||
mTextDescription.setText(R.string.premium_settings_upsell_description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,7 +5,6 @@ import android.view.View;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
|
|
||||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||||
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||||
@ -46,17 +45,6 @@ public final class SingleChoiceViewHolder extends SettingViewHolder {
|
|||||||
mTextSettingDescription.setText(choices[i]);
|
mTextSettingDescription.setText(choices[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (item instanceof PremiumSingleChoiceSetting) {
|
|
||||||
PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item;
|
|
||||||
int selected = setting.getSelectedValue();
|
|
||||||
Resources resMgr = mTextSettingDescription.getContext().getResources();
|
|
||||||
String[] choices = resMgr.getStringArray(setting.getChoicesId());
|
|
||||||
int[] values = resMgr.getIntArray(setting.getValuesId());
|
|
||||||
for (int i = 0; i < values.length; ++i) {
|
|
||||||
if (values[i] == selected) {
|
|
||||||
mTextSettingDescription.setText(choices[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mTextSettingDescription.setVisibility(View.GONE);
|
mTextSettingDescription.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@ -67,8 +55,6 @@ public final class SingleChoiceViewHolder extends SettingViewHolder {
|
|||||||
int position = getAdapterPosition();
|
int position = getAdapterPosition();
|
||||||
if (mItem instanceof SingleChoiceSetting) {
|
if (mItem instanceof SingleChoiceSetting) {
|
||||||
getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
|
getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
|
||||||
} else if (mItem instanceof PremiumSingleChoiceSetting) {
|
|
||||||
getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position);
|
|
||||||
} else if (mItem instanceof StringSingleChoiceSetting) {
|
} else if (mItem instanceof StringSingleChoiceSetting) {
|
||||||
getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
|
getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,6 @@ public final class SettingsFile {
|
|||||||
|
|
||||||
public static final String KEY_DESIGN = "design";
|
public static final String KEY_DESIGN = "design";
|
||||||
|
|
||||||
public static final String KEY_PREMIUM = "premium";
|
|
||||||
|
|
||||||
public static final String KEY_GRAPHICS_API = "graphics_api";
|
public static final String KEY_GRAPHICS_API = "graphics_api";
|
||||||
public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen";
|
public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen";
|
||||||
@ -160,7 +159,7 @@ public final class SettingsFile {
|
|||||||
BufferedReader reader = null;
|
BufferedReader reader = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Context context = CitraApplication.getAppContext();
|
Context context = CitraApplication.Companion.getAppContext();
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
|
||||||
@ -226,7 +225,7 @@ public final class SettingsFile {
|
|||||||
DocumentFile ini = getSettingsFile(fileName);
|
DocumentFile ini = getSettingsFile(fileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Context context = CitraApplication.getAppContext();
|
Context context = CitraApplication.Companion.getAppContext();
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||||
Wini writer = new Wini(inputStream);
|
Wini writer = new Wini(inputStream);
|
||||||
|
|
||||||
@ -242,24 +241,7 @@ public final class SettingsFile {
|
|||||||
outputStream.close();
|
outputStream.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
|
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
|
||||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
|
view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) {
|
|
||||||
Set<String> sortedSections = new TreeSet<>(sections.keySet());
|
|
||||||
|
|
||||||
for (String sectionKey : sortedSections) {
|
|
||||||
SettingSection section = sections.get(sectionKey);
|
|
||||||
|
|
||||||
HashMap<String, Setting> settings = section.getSettings();
|
|
||||||
Set<String> sortedKeySet = new TreeSet<>(settings.keySet());
|
|
||||||
|
|
||||||
for (String settingKey : sortedKeySet) {
|
|
||||||
Setting setting = settings.get(settingKey);
|
|
||||||
NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,13 +262,13 @@ public final class SettingsFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static DocumentFile getSettingsFile(String fileName) {
|
public static DocumentFile getSettingsFile(String fileName) {
|
||||||
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
|
||||||
DocumentFile configDirectory = root.findFile("config");
|
DocumentFile configDirectory = root.findFile("config");
|
||||||
return configDirectory.findFile(fileName + ".ini");
|
return configDirectory.findFile(fileName + ".ini");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DocumentFile getCustomGameSettingsFile(String gameId) {
|
private static DocumentFile getCustomGameSettingsFile(String gameId) {
|
||||||
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
|
||||||
DocumentFile configDirectory = root.findFile("GameSettings");
|
DocumentFile configDirectory = root.findFile("GameSettings");
|
||||||
return configDirectory.findFile(gameId + ".ini");
|
return configDirectory.findFile(gameId + ".ini");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import org.citra.citra_emu.BuildConfig
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.FragmentAboutBinding
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class AboutFragment : Fragment() {
|
||||||
|
private var _binding: FragmentAboutBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarAbout.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonContributors.setOnClickListener {
|
||||||
|
openLink(
|
||||||
|
getString(R.string.contributors_link)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.buttonLicenses.setOnClickListener {
|
||||||
|
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textBuildHash.text = BuildConfig.VERSION_NAME
|
||||||
|
binding.buttonBuildHash.setOnClickListener {
|
||||||
|
val clipBoard =
|
||||||
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH)
|
||||||
|
clipBoard.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.copied_to_clipboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
|
||||||
|
binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
|
||||||
|
binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLink(link: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarAbout.layoutParams as MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarAbout.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
val mlpScrollAbout = binding.scrollAbout.layoutParams as MarginLayoutParams
|
||||||
|
mlpScrollAbout.leftMargin = leftInsets
|
||||||
|
mlpScrollAbout.rightMargin = rightInsets
|
||||||
|
binding.scrollAbout.layoutParams = mlpScrollAbout
|
||||||
|
|
||||||
|
binding.contentAbout.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogCitraDirectoryBinding
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class CitraDirectoryDialogFragment : DialogFragment() {
|
||||||
|
private lateinit var binding: DialogCitraDirectoryBinding
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
fun interface Listener {
|
||||||
|
fun onPressPositiveButton(moveData: Boolean, path: Uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
binding = DialogCitraDirectoryBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
val path = Uri.parse(requireArguments().getString(PATH))
|
||||||
|
|
||||||
|
binding.checkBox.isChecked = savedInstanceState?.getBoolean(MOVE_DATE_ENABLE) ?: false
|
||||||
|
val oldPath = PermissionsHandler.citraDirectory
|
||||||
|
if (!PermissionsHandler.hasWriteAccess(requireActivity()) ||
|
||||||
|
oldPath.toString() == path.toString()
|
||||||
|
) {
|
||||||
|
binding.checkBox.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.path.text = path.path
|
||||||
|
binding.path.isSelected = true
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.select_citra_user_folder)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
homeViewModel.directoryListener?.onPressPositiveButton(
|
||||||
|
if (binding.checkBox.visibility != View.GONE) {
|
||||||
|
binding.checkBox.isChecked
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
},
|
||||||
|
path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
|
||||||
|
if (!PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||||
|
(requireActivity() as MainActivity).openCitraDirectory.launch(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(MOVE_DATE_ENABLE, binding.checkBox.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "citra_directory_dialog_fragment"
|
||||||
|
private const val MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE"
|
||||||
|
private const val PATH = "path"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
path: String,
|
||||||
|
listener: Listener
|
||||||
|
): CitraDirectoryDialogFragment {
|
||||||
|
val dialog = CitraDirectoryDialogFragment()
|
||||||
|
ViewModelProvider(activity)[HomeViewModel::class.java].directoryListener = listener
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(PATH, path)
|
||||||
|
dialog.arguments = args
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogCopyDirBinding
|
||||||
|
import org.citra.citra_emu.model.SetupCallback
|
||||||
|
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||||
|
import org.citra.citra_emu.utils.FileUtil
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class CopyDirProgressDialog : DialogFragment() {
|
||||||
|
private var _binding: DialogCopyDirBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
_binding = DialogCopyDirBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.moving_data)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.messageText.collectLatest { binding.messageText.text = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.dirProgress.collectLatest {
|
||||||
|
binding.progressBar.max = homeViewModel.maxDirProgress.value
|
||||||
|
binding.progressBar.progress = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.copyComplete.collect {
|
||||||
|
if (it) {
|
||||||
|
homeViewModel.setUserDir(
|
||||||
|
requireActivity(),
|
||||||
|
PermissionsHandler.citraDirectory.path!!
|
||||||
|
)
|
||||||
|
homeViewModel.copyInProgress = false
|
||||||
|
homeViewModel.setPickingUserDir(false)
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.copy_complete,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "CopyDirProgressDialog"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
previous: Uri,
|
||||||
|
path: Uri,
|
||||||
|
callback: SetupCallback? = null
|
||||||
|
): CopyDirProgressDialog? {
|
||||||
|
val viewModel = ViewModelProvider(activity)[HomeViewModel::class.java]
|
||||||
|
if (viewModel.copyInProgress) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
viewModel.clearCopyInfo()
|
||||||
|
viewModel.copyInProgress = true
|
||||||
|
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
FileUtil.copyDir(
|
||||||
|
previous.toString(),
|
||||||
|
path.toString(),
|
||||||
|
object : FileUtil.CopyDirListener {
|
||||||
|
override fun onSearchProgress(directoryName: String) {
|
||||||
|
viewModel.onUpdateSearchProgress(
|
||||||
|
CitraApplication.appContext.resources,
|
||||||
|
directoryName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCopyProgress(filename: String, progress: Int, max: Int) {
|
||||||
|
viewModel.onUpdateCopyProgress(
|
||||||
|
CitraApplication.appContext.resources,
|
||||||
|
filename,
|
||||||
|
progress,
|
||||||
|
max
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete() {
|
||||||
|
CitraDirectoryHelper.initializeCitraDirectory(path)
|
||||||
|
callback?.onStepCompleted()
|
||||||
|
viewModel.setCopyComplete(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CopyDirProgressDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.NativeLibrary.InstallStatus
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogProgressBarBinding
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||||
|
|
||||||
|
class DownloadSystemFilesDialogFragment : DialogFragment() {
|
||||||
|
private var _binding: DialogProgressBarBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val downloadViewModel: SystemFilesViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var titles: LongArray
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
_binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
titles = requireArguments().getLongArray(TITLES)!!
|
||||||
|
|
||||||
|
binding.progressText.visibility = View.GONE
|
||||||
|
|
||||||
|
binding.progressBar.min = 0
|
||||||
|
binding.progressBar.max = titles.size
|
||||||
|
if (downloadViewModel.isDownloading.value != true) {
|
||||||
|
binding.progressBar.progress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.downloading_files)
|
||||||
|
.setMessage(R.string.downloading_files_description)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
downloadViewModel.progress.collectLatest { binding.progressBar.progress = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
downloadViewModel.result.collect {
|
||||||
|
when (it) {
|
||||||
|
InstallStatus.Success -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(R.string.download_success, 0)
|
||||||
|
.show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
gamesViewModel.setShouldSwapData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallStatus.ErrorFailedToOpenFile,
|
||||||
|
InstallStatus.ErrorEncrypted,
|
||||||
|
InstallStatus.ErrorFileNotFound,
|
||||||
|
InstallStatus.ErrorInvalid,
|
||||||
|
InstallStatus.ErrorAborted -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.download_failed,
|
||||||
|
R.string.download_failed_description
|
||||||
|
).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
gamesViewModel.setShouldSwapData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallStatus.Cancelled -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.download_cancelled,
|
||||||
|
R.string.download_cancelled_description
|
||||||
|
).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing on null
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider using WorkManager here. While the home menu can only really amount to
|
||||||
|
// about 150MBs, this could be a problem on inconsistent networks
|
||||||
|
downloadViewModel.download(titles)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val alertDialog = dialog as AlertDialog
|
||||||
|
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
downloadViewModel.cancel()
|
||||||
|
dialog?.setTitle(R.string.cancelling)
|
||||||
|
binding.progressBar.isIndeterminate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DownloadSystemFilesDialogFragment"
|
||||||
|
|
||||||
|
const val TITLES = "Titles"
|
||||||
|
|
||||||
|
fun newInstance(titles: LongArray): DownloadSystemFilesDialogFragment {
|
||||||
|
val dialog = DownloadSystemFilesDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putLongArray(TITLES, titles)
|
||||||
|
dialog.arguments = args
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.DriverAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentDriverManagerBinding
|
||||||
|
import org.citra.citra_emu.utils.FileUtil.asDocumentFile
|
||||||
|
import org.citra.citra_emu.utils.FileUtil.inputStream
|
||||||
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class DriverManagerFragment : Fragment() {
|
||||||
|
private var _binding: FragmentDriverManagerBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentDriverManagerBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
if (!driverViewModel.isInteractionAllowed) {
|
||||||
|
DriversLoadingDialogFragment().show(
|
||||||
|
childFragmentManager,
|
||||||
|
DriversLoadingDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarDrivers.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonInstall.setOnClickListener {
|
||||||
|
getDriver.launch(arrayOf("application/zip"))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.listDrivers.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
|
)
|
||||||
|
adapter = DriverAdapter(driverViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
driverViewModel.driverList.collectLatest {
|
||||||
|
(binding.listDrivers.adapter as DriverAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
driverViewModel.newDriverInstalled.collect {
|
||||||
|
if (_binding != null && it) {
|
||||||
|
(binding.listDrivers.adapter as DriverAdapter).apply {
|
||||||
|
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||||
|
notifyItemChanged(driverViewModel.selectedDriver)
|
||||||
|
driverViewModel.setNewDriverInstalled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start installing requested driver
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
driverViewModel.onCloseDriverManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarDrivers.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlplistDrivers.leftMargin = leftInsets
|
||||||
|
mlplistDrivers.rightMargin = rightInsets
|
||||||
|
binding.listDrivers.layoutParams = mlplistDrivers
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonInstall.layoutParams = mlpFab
|
||||||
|
|
||||||
|
binding.listDrivers.updatePadding(
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getDriver =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.installing_driver,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
// Ignore file exceptions when a user selects an invalid zip
|
||||||
|
val driverFile: DocumentFile
|
||||||
|
try {
|
||||||
|
driverFile = GpuDriverHelper.copyDriverToExternalStorage(result)
|
||||||
|
?: throw IOException("Driver failed validation!")
|
||||||
|
} catch (_: IOException) {
|
||||||
|
return@newInstance getString(R.string.select_gpu_driver_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
val driverData = GpuDriverHelper.getMetadataFromZip(driverFile.inputStream())
|
||||||
|
val driverInList =
|
||||||
|
driverViewModel.driverList.value.firstOrNull { it.second == driverData }
|
||||||
|
if (driverInList != null) {
|
||||||
|
driverFile.delete()
|
||||||
|
return@newInstance getString(R.string.driver_already_installed)
|
||||||
|
} else {
|
||||||
|
driverViewModel.addDriver(Pair(driverFile.uri, driverData))
|
||||||
|
driverViewModel.setNewDriverInstalled(true)
|
||||||
|
}
|
||||||
|
return@newInstance Any()
|
||||||
|
}.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogProgressBarBinding
|
||||||
|
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||||
|
|
||||||
|
class DriversLoadingDialogFragment : DialogFragment() {
|
||||||
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var binding: DialogProgressBarBinding
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||||
|
binding.progressBar.isIndeterminate = true
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.loading)
|
||||||
|
.setView(binding.root)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = binding.root
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
driverViewModel.areDriversLoading.collect { checkForDismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
driverViewModel.isDriverReady.collect { checkForDismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForDismiss() {
|
||||||
|
if (driverViewModel.isInteractionAllowed) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DriversLoadingDialogFragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,7 +27,6 @@ import org.citra.citra_emu.activities.EmulationActivity;
|
|||||||
import org.citra.citra_emu.overlay.InputOverlay;
|
import org.citra.citra_emu.overlay.InputOverlay;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
||||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
|
|
||||||
@ -42,8 +41,6 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
|
|
||||||
private EmulationState mEmulationState;
|
private EmulationState mEmulationState;
|
||||||
|
|
||||||
private DirectoryStateReceiver directoryStateReceiver;
|
|
||||||
|
|
||||||
private EmulationActivity activity;
|
private EmulationActivity activity;
|
||||||
|
|
||||||
private TextView mPerfStats;
|
private TextView mPerfStats;
|
||||||
@ -65,7 +62,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
|
|
||||||
if (context instanceof EmulationActivity) {
|
if (context instanceof EmulationActivity) {
|
||||||
activity = (EmulationActivity) context;
|
activity = (EmulationActivity) context;
|
||||||
NativeLibrary.setEmulationActivity((EmulationActivity) context);
|
NativeLibrary.INSTANCE.setEmulationActivity((EmulationActivity) context);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
|
throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
|
||||||
}
|
}
|
||||||
@ -116,20 +113,11 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Choreographer.getInstance().postFrameCallback(this);
|
Choreographer.getInstance().postFrameCallback(this);
|
||||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
mEmulationState.run(activity.isActivityRecreated());
|
||||||
mEmulationState.run(activity.isActivityRecreated());
|
|
||||||
} else {
|
|
||||||
setupCitraDirectoriesThenStartEmulation();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
if (directoryStateReceiver != null) {
|
|
||||||
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
|
|
||||||
directoryStateReceiver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mEmulationState.isRunning()) {
|
if (mEmulationState.isRunning()) {
|
||||||
mEmulationState.pause();
|
mEmulationState.pause();
|
||||||
}
|
}
|
||||||
@ -140,39 +128,10 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDetach() {
|
public void onDetach() {
|
||||||
NativeLibrary.clearEmulationActivity();
|
NativeLibrary.INSTANCE.clearEmulationActivity();
|
||||||
super.onDetach();
|
super.onDetach();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCitraDirectoriesThenStartEmulation() {
|
|
||||||
IntentFilter statusIntentFilter = new IntentFilter(
|
|
||||||
DirectoryInitialization.BROADCAST_ACTION);
|
|
||||||
|
|
||||||
directoryStateReceiver =
|
|
||||||
new DirectoryStateReceiver(directoryInitializationState ->
|
|
||||||
{
|
|
||||||
if (directoryInitializationState ==
|
|
||||||
DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
|
||||||
mEmulationState.run(activity.isActivityRecreated());
|
|
||||||
} else if (directoryInitializationState ==
|
|
||||||
DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
|
||||||
Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
} else if (directoryInitializationState ==
|
|
||||||
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
|
||||||
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
|
|
||||||
Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Registers the DirectoryStateReceiver and its intent filters
|
|
||||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
|
|
||||||
directoryStateReceiver,
|
|
||||||
statusIntentFilter);
|
|
||||||
DirectoryInitialization.start(getActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshInputOverlay() {
|
public void refreshInputOverlay() {
|
||||||
mInputOverlay.refreshControls();
|
mInputOverlay.refreshControls();
|
||||||
}
|
}
|
||||||
@ -195,7 +154,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
|
|
||||||
perfStatsUpdater = () ->
|
perfStatsUpdater = () ->
|
||||||
{
|
{
|
||||||
final double[] perfStats = NativeLibrary.GetPerfStats();
|
final double[] perfStats = NativeLibrary.INSTANCE.getPerfStats();
|
||||||
if (perfStats[FPS] > 0) {
|
if (perfStats[FPS] > 0) {
|
||||||
mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5),
|
mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5),
|
||||||
(int) (perfStats[SPEED] * 100.0 + 0.5)));
|
(int) (perfStats[SPEED] * 100.0 + 0.5)));
|
||||||
@ -235,7 +194,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
@Override
|
@Override
|
||||||
public void doFrame(long frameTimeNanos) {
|
public void doFrame(long frameTimeNanos) {
|
||||||
Choreographer.getInstance().postFrameCallback(this);
|
Choreographer.getInstance().postFrameCallback(this);
|
||||||
NativeLibrary.DoFrame();
|
NativeLibrary.INSTANCE.doFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopEmulation() {
|
public void stopEmulation() {
|
||||||
@ -286,7 +245,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
if (state != State.STOPPED) {
|
if (state != State.STOPPED) {
|
||||||
Log.debug("[EmulationFragment] Stopping emulation.");
|
Log.debug("[EmulationFragment] Stopping emulation.");
|
||||||
state = State.STOPPED;
|
state = State.STOPPED;
|
||||||
NativeLibrary.StopEmulation();
|
NativeLibrary.INSTANCE.stopEmulation();
|
||||||
} else {
|
} else {
|
||||||
Log.warning("[EmulationFragment] Stop called while already stopped.");
|
Log.warning("[EmulationFragment] Stop called while already stopped.");
|
||||||
}
|
}
|
||||||
@ -300,8 +259,8 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
Log.debug("[EmulationFragment] Pausing emulation.");
|
Log.debug("[EmulationFragment] Pausing emulation.");
|
||||||
|
|
||||||
// Release the surface before pausing, since emulation has to be running for that.
|
// Release the surface before pausing, since emulation has to be running for that.
|
||||||
NativeLibrary.SurfaceDestroyed();
|
NativeLibrary.INSTANCE.surfaceDestroyed();
|
||||||
NativeLibrary.PauseEmulation();
|
NativeLibrary.INSTANCE.pauseEmulation();
|
||||||
} else {
|
} else {
|
||||||
Log.warning("[EmulationFragment] Pause called while already paused.");
|
Log.warning("[EmulationFragment] Pause called while already paused.");
|
||||||
}
|
}
|
||||||
@ -309,7 +268,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
|
|
||||||
public synchronized void run(boolean isActivityRecreated) {
|
public synchronized void run(boolean isActivityRecreated) {
|
||||||
if (isActivityRecreated) {
|
if (isActivityRecreated) {
|
||||||
if (NativeLibrary.IsRunning()) {
|
if (NativeLibrary.INSTANCE.isRunning()) {
|
||||||
state = State.PAUSED;
|
state = State.PAUSED;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -340,7 +299,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
Log.debug("[EmulationFragment] Surface destroyed.");
|
Log.debug("[EmulationFragment] Surface destroyed.");
|
||||||
|
|
||||||
if (state == State.RUNNING) {
|
if (state == State.RUNNING) {
|
||||||
NativeLibrary.SurfaceDestroyed();
|
NativeLibrary.INSTANCE.surfaceDestroyed();
|
||||||
state = State.PAUSED;
|
state = State.PAUSED;
|
||||||
} else if (state == State.PAUSED) {
|
} else if (state == State.PAUSED) {
|
||||||
Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
|
Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
|
||||||
@ -353,18 +312,18 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
|
|||||||
private void runWithValidSurface() {
|
private void runWithValidSurface() {
|
||||||
mRunWhenSurfaceIsValid = false;
|
mRunWhenSurfaceIsValid = false;
|
||||||
if (state == State.STOPPED) {
|
if (state == State.STOPPED) {
|
||||||
NativeLibrary.SurfaceChanged(mSurface);
|
NativeLibrary.INSTANCE.surfaceChanged(mSurface);
|
||||||
Thread mEmulationThread = new Thread(() ->
|
Thread mEmulationThread = new Thread(() ->
|
||||||
{
|
{
|
||||||
Log.debug("[EmulationFragment] Starting emulation thread.");
|
Log.debug("[EmulationFragment] Starting emulation thread.");
|
||||||
NativeLibrary.Run(mGamePath);
|
NativeLibrary.INSTANCE.run(mGamePath);
|
||||||
}, "NativeEmulation");
|
}, "NativeEmulation");
|
||||||
mEmulationThread.start();
|
mEmulationThread.start();
|
||||||
|
|
||||||
} else if (state == State.PAUSED) {
|
} else if (state == State.PAUSED) {
|
||||||
Log.debug("[EmulationFragment] Resuming emulation.");
|
Log.debug("[EmulationFragment] Resuming emulation.");
|
||||||
NativeLibrary.SurfaceChanged(mSurface);
|
NativeLibrary.INSTANCE.surfaceChanged(mSurface);
|
||||||
NativeLibrary.UnPauseEmulation();
|
NativeLibrary.INSTANCE.unPauseEmulation();
|
||||||
} else {
|
} else {
|
||||||
Log.debug("[EmulationFragment] Bug, run called while already running.");
|
Log.debug("[EmulationFragment] Bug, run called while already running.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,202 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.GameAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentGamesBinding
|
||||||
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
|
import org.citra.citra_emu.model.Game
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class GamesFragment : Fragment() {
|
||||||
|
private var _binding: FragmentGamesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialFadeThrough()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentGamesBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||||
|
|
||||||
|
binding.gridGames.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
|
)
|
||||||
|
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.swipeRefresh.apply {
|
||||||
|
// Add swipe down to refresh gesture
|
||||||
|
setOnRefreshListener {
|
||||||
|
gamesViewModel.reloadGames(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set theme color to the refresh animation's background
|
||||||
|
setProgressBackgroundColorSchemeColor(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.swipeRefresh,
|
||||||
|
com.google.android.material.R.attr.colorPrimary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setColorSchemeColors(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.swipeRefresh,
|
||||||
|
com.google.android.material.R.attr.colorOnPrimary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn
|
||||||
|
post {
|
||||||
|
if (_binding == null) {
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
gamesViewModel.isReloading.collect { isReloading ->
|
||||||
|
binding.swipeRefresh.isRefreshing = isReloading
|
||||||
|
if (gamesViewModel.games.value.isEmpty() && !isReloading) {
|
||||||
|
binding.noticeText.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.noticeText.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
gamesViewModel.games.collectLatest { setAdapter(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
gamesViewModel.shouldSwapData.collect {
|
||||||
|
if (it) {
|
||||||
|
setAdapter(gamesViewModel.games.value)
|
||||||
|
gamesViewModel.setShouldSwapData(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
gamesViewModel.shouldScrollToTop.collect {
|
||||||
|
if (it) {
|
||||||
|
scrollToTop()
|
||||||
|
gamesViewModel.setShouldScrollToTop(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAdapter(games: List<Game>) {
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
if (preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false)) {
|
||||||
|
(binding.gridGames.adapter as GameAdapter).submitList(games)
|
||||||
|
} else {
|
||||||
|
val filteredList = games.filter { !it.isSystemTitle }
|
||||||
|
(binding.gridGames.adapter as GameAdapter).submitList(filteredList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToTop() {
|
||||||
|
if (_binding != null) {
|
||||||
|
binding.gridGames.smoothScrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
||||||
|
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||||
|
val spacingNavigationRail =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
||||||
|
|
||||||
|
binding.gridGames.updatePadding(
|
||||||
|
top = barInsets.top + extraListSpacing,
|
||||||
|
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.swipeRefresh.setProgressViewEndTarget(
|
||||||
|
false,
|
||||||
|
barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
|
||||||
|
)
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
val mlpSwipe = binding.swipeRefresh.layoutParams as MarginLayoutParams
|
||||||
|
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
|
mlpSwipe.leftMargin = leftInsets + spacingNavigationRail
|
||||||
|
mlpSwipe.rightMargin = rightInsets
|
||||||
|
} else {
|
||||||
|
mlpSwipe.leftMargin = leftInsets
|
||||||
|
mlpSwipe.rightMargin = rightInsets + spacingNavigationRail
|
||||||
|
}
|
||||||
|
binding.swipeRefresh.layoutParams = mlpSwipe
|
||||||
|
|
||||||
|
binding.noticeText.updatePadding(bottom = spacingNavigation)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.HomeSettingAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
|
||||||
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||||
|
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||||
|
import org.citra.citra_emu.model.HomeSetting
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
import org.citra.citra_emu.utils.GameHelper
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
|
import org.citra.citra_emu.utils.Log
|
||||||
|
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||||
|
|
||||||
|
class HomeSettingsFragment : Fragment() {
|
||||||
|
private var _binding: FragmentHomeSettingsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val preferences get() =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentHomeSettingsBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
|
val optionsList = listOf(
|
||||||
|
HomeSetting(
|
||||||
|
R.string.grid_menu_core_settings,
|
||||||
|
R.string.settings_description,
|
||||||
|
R.drawable.ic_settings,
|
||||||
|
{ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") }
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.system_files,
|
||||||
|
R.string.system_files_description,
|
||||||
|
R.drawable.ic_system_update,
|
||||||
|
{
|
||||||
|
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
parentFragmentManager.primaryNavigationFragment?.findNavController()
|
||||||
|
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.install_game_content,
|
||||||
|
R.string.install_game_content_description,
|
||||||
|
R.drawable.ic_install,
|
||||||
|
{ mainActivity.ciaFileInstaller.launch(true) }
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.share_log,
|
||||||
|
R.string.share_log_description,
|
||||||
|
R.drawable.ic_share,
|
||||||
|
{ shareLog() }
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.gpu_driver_manager,
|
||||||
|
R.string.install_gpu_driver_description,
|
||||||
|
R.drawable.ic_install_driver,
|
||||||
|
{
|
||||||
|
binding.root.findNavController()
|
||||||
|
.navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
|
||||||
|
},
|
||||||
|
{ GpuDriverHelper.supportsCustomDriverLoading() },
|
||||||
|
R.string.custom_driver_not_supported,
|
||||||
|
R.string.custom_driver_not_supported_description,
|
||||||
|
driverViewModel.selectedDriverMetadata
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.select_citra_user_folder,
|
||||||
|
R.string.select_citra_user_folder_home_description,
|
||||||
|
R.drawable.ic_home,
|
||||||
|
{ mainActivity.openCitraDirectory.launch(null) },
|
||||||
|
details = homeViewModel.userDir
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.select_games_folder,
|
||||||
|
R.string.select_games_folder_description,
|
||||||
|
R.drawable.ic_add,
|
||||||
|
{ getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
|
||||||
|
details = homeViewModel.gamesDir
|
||||||
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.about,
|
||||||
|
R.string.about_description,
|
||||||
|
R.drawable.ic_info_outline,
|
||||||
|
{
|
||||||
|
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
parentFragmentManager.primaryNavigationFragment?.findNavController()
|
||||||
|
?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.homeSettingsList.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
|
)
|
||||||
|
adapter = HomeSettingAdapter(
|
||||||
|
requireActivity() as AppCompatActivity,
|
||||||
|
viewLifecycleOwner,
|
||||||
|
optionsList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
exitTransition = null
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getGamesDirectory =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
requireContext().contentResolver.takePersistableUriPermission(
|
||||||
|
result,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
// When a new directory is picked, we currently will reset the existing games
|
||||||
|
// database. This effectively means that only one game directory is supported.
|
||||||
|
preferences.edit()
|
||||||
|
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
CitraApplication.appContext,
|
||||||
|
R.string.games_dir_selected,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
homeViewModel.setGamesDir(requireActivity(), result.path!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareLog() {
|
||||||
|
val logDirectory = DocumentFile.fromTreeUri(
|
||||||
|
requireContext(),
|
||||||
|
PermissionsHandler.citraDirectory
|
||||||
|
)?.findFile("log")
|
||||||
|
val currentLog = logDirectory?.findFile("citra_log.txt")
|
||||||
|
val oldLog = logDirectory?.findFile("citra_log.txt.old.txt")
|
||||||
|
|
||||||
|
val intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
if (!Log.gameLaunched && oldLog?.exists() == true) {
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, oldLog.uri)
|
||||||
|
startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
|
||||||
|
} else if (currentLog?.exists() == true) {
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, currentLog.uri)
|
||||||
|
startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
getText(R.string.share_log_not_found),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||||
|
val spacingNavigationRail =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
binding.scrollViewSettings.updatePadding(
|
||||||
|
top = barInsets.top,
|
||||||
|
bottom = barInsets.bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
val mlpScrollSettings = binding.scrollViewSettings.layoutParams as MarginLayoutParams
|
||||||
|
mlpScrollSettings.leftMargin = leftInsets
|
||||||
|
mlpScrollSettings.rightMargin = rightInsets
|
||||||
|
binding.scrollViewSettings.layoutParams = mlpScrollSettings
|
||||||
|
|
||||||
|
binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation)
|
||||||
|
|
||||||
|
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
|
binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail)
|
||||||
|
} else {
|
||||||
|
binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
|
||||||
|
}
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogProgressBarBinding
|
||||||
|
import org.citra.citra_emu.viewmodel.TaskViewModel
|
||||||
|
|
||||||
|
class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
|
private val taskViewModel: TaskViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var binding: DialogProgressBarBinding
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val titleId = requireArguments().getInt(TITLE)
|
||||||
|
val cancellable = requireArguments().getBoolean(CANCELLABLE)
|
||||||
|
|
||||||
|
binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||||
|
binding.progressBar.isIndeterminate = true
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(titleId)
|
||||||
|
.setView(binding.root)
|
||||||
|
|
||||||
|
if (cancellable) {
|
||||||
|
dialog.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val alertDialog = dialog.create()
|
||||||
|
alertDialog.setCanceledOnTouchOutside(false)
|
||||||
|
|
||||||
|
if (!taskViewModel.isRunning.value) {
|
||||||
|
taskViewModel.runTask()
|
||||||
|
}
|
||||||
|
return alertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
taskViewModel.isComplete.collect {
|
||||||
|
if (it) {
|
||||||
|
dismiss()
|
||||||
|
when (val result = taskViewModel.result.value) {
|
||||||
|
is String -> Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
result,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
is MessageDialogFragment -> result.show(
|
||||||
|
requireActivity().supportFragmentManager,
|
||||||
|
MessageDialogFragment.TAG
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskViewModel.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
taskViewModel.cancelled.collect {
|
||||||
|
if (it) {
|
||||||
|
dialog?.setTitle(R.string.cancelling)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, the ProgressDialog will immediately dismiss itself upon a button being pressed.
|
||||||
|
// Setting the OnClickListener again after the dialog is shown overrides this behavior.
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val alertDialog = dialog as AlertDialog
|
||||||
|
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
alertDialog.setTitle(getString(R.string.cancelling))
|
||||||
|
taskViewModel.setCancelled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "IndeterminateProgressDialogFragment"
|
||||||
|
|
||||||
|
private const val TITLE = "Title"
|
||||||
|
private const val CANCELLABLE = "Cancellable"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
titleId: Int,
|
||||||
|
cancellable: Boolean = false,
|
||||||
|
task: () -> Any
|
||||||
|
): IndeterminateProgressDialogFragment {
|
||||||
|
val dialog = IndeterminateProgressDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
||||||
|
args.putInt(TITLE, titleId)
|
||||||
|
args.putBoolean(CANCELLABLE, cancellable)
|
||||||
|
dialog.arguments = args
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import org.citra.citra_emu.databinding.DialogLicenseBinding
|
||||||
|
import org.citra.citra_emu.model.License
|
||||||
|
import org.citra.citra_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
|
class LicenseBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
|
private var _binding: DialogLicenseBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = DialogLicenseBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
BottomSheetBehavior.from<View>(view.parent as View).state =
|
||||||
|
BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
|
||||||
|
val license = requireArguments().parcelable<License>(LICENSE)!!
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
textTitle.setText(license.titleId)
|
||||||
|
textLink.setText(license.linkId)
|
||||||
|
if (license.copyrightId != 0) {
|
||||||
|
textCopyright.setText(license.copyrightId)
|
||||||
|
} else {
|
||||||
|
textCopyright.visibility = View.GONE
|
||||||
|
}
|
||||||
|
if (license.licenseId != 0) {
|
||||||
|
textLicense.setText(license.licenseId)
|
||||||
|
} else {
|
||||||
|
textLicense.setText(license.licenseLinkId)
|
||||||
|
BottomSheetBehavior.from<View>(view.parent as View).state =
|
||||||
|
BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LicenseBottomSheetDialogFragment"
|
||||||
|
|
||||||
|
const val LICENSE = "License"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
license: License
|
||||||
|
): LicenseBottomSheetDialogFragment {
|
||||||
|
val dialog = LicenseBottomSheetDialogFragment()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(LICENSE, license)
|
||||||
|
dialog.arguments = bundle
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.LicenseAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentLicensesBinding
|
||||||
|
import org.citra.citra_emu.model.License
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class LicensesFragment : Fragment() {
|
||||||
|
private var _binding: FragmentLicensesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentLicensesBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarLicenses.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val licenses = listOf(
|
||||||
|
License(
|
||||||
|
R.string.license_adreno_tools,
|
||||||
|
R.string.license_adreno_tools_description,
|
||||||
|
R.string.license_adreno_tools_link,
|
||||||
|
R.string.license_adreno_tools_copyright,
|
||||||
|
R.string.license_adreno_tools_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_cubeb,
|
||||||
|
R.string.license_cubeb_description,
|
||||||
|
R.string.license_cubeb_link,
|
||||||
|
R.string.license_cubeb_copyright,
|
||||||
|
R.string.license_cubeb_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_dynarmic,
|
||||||
|
R.string.license_dynarmic_description,
|
||||||
|
R.string.license_dynarmic_link,
|
||||||
|
R.string.license_dynarmic_copyright,
|
||||||
|
R.string.license_dynarmic_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_sirit,
|
||||||
|
R.string.license_sirit_description,
|
||||||
|
R.string.license_sirit_link,
|
||||||
|
R.string.license_sirit_copyright,
|
||||||
|
R.string.license_sirit_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_cryptopp,
|
||||||
|
R.string.license_cryptopp_description,
|
||||||
|
R.string.license_cryptopp_link,
|
||||||
|
R.string.license_cryptopp_copyright,
|
||||||
|
R.string.license_cryptopp_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
titleId = R.string.license_boost,
|
||||||
|
descriptionId = R.string.license_boost_description,
|
||||||
|
linkId = R.string.license_boost_link,
|
||||||
|
licenseId = R.string.license_boost_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_nihstro,
|
||||||
|
R.string.license_nihstro_description,
|
||||||
|
R.string.license_nihstro_link,
|
||||||
|
R.string.license_nihstro_copyright,
|
||||||
|
R.string.license_nihstro_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_httplib,
|
||||||
|
R.string.license_httplib_description,
|
||||||
|
R.string.license_httplib_link,
|
||||||
|
R.string.license_httplib_copyright,
|
||||||
|
R.string.license_mit
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_teakra,
|
||||||
|
R.string.license_teakra_description,
|
||||||
|
R.string.license_teakra_link,
|
||||||
|
R.string.license_teakra_copyright,
|
||||||
|
R.string.license_mit
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_enet,
|
||||||
|
R.string.license_enet_description,
|
||||||
|
R.string.license_enet_link,
|
||||||
|
R.string.license_enet_copyright,
|
||||||
|
R.string.license_mit
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_glad,
|
||||||
|
R.string.license_glad_description,
|
||||||
|
R.string.license_glad_link,
|
||||||
|
R.string.license_glad_copyright,
|
||||||
|
R.string.license_mit
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
titleId = R.string.license_glslang,
|
||||||
|
descriptionId = R.string.license_glslang_description,
|
||||||
|
linkId = R.string.license_glslang_link,
|
||||||
|
licenseLinkId = R.string.license_glslang_link_license
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_openal,
|
||||||
|
R.string.license_openal_description,
|
||||||
|
R.string.license_openal_link,
|
||||||
|
R.string.license_openal_copyright,
|
||||||
|
R.string.license_openal_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_sdl,
|
||||||
|
R.string.license_sdl_description,
|
||||||
|
R.string.license_sdl_link,
|
||||||
|
R.string.license_sdl_copyright,
|
||||||
|
R.string.license_sdl_text
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_vma,
|
||||||
|
R.string.license_vma_description,
|
||||||
|
R.string.license_vma_link,
|
||||||
|
R.string.license_vma_copyright,
|
||||||
|
R.string.license_mit
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
R.string.license_zstd,
|
||||||
|
R.string.license_zstd_description,
|
||||||
|
R.string.license_zstd_link,
|
||||||
|
R.string.license_zstd_copyright,
|
||||||
|
R.string.license_zstd_text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.listLicenses.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = LicenseAdapter(requireActivity() as AppCompatActivity, licenses)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarLicenses.layoutParams as MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarLicenses.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
val mlpScrollAbout = binding.listLicenses.layoutParams as MarginLayoutParams
|
||||||
|
mlpScrollAbout.leftMargin = leftInsets
|
||||||
|
mlpScrollAbout.rightMargin = rightInsets
|
||||||
|
binding.listLicenses.layoutParams = mlpScrollAbout
|
||||||
|
|
||||||
|
binding.listLicenses.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
|
||||||
|
class MessageDialogFragment : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val titleId = requireArguments().getInt(TITLE_ID)
|
||||||
|
val descriptionId = requireArguments().getInt(DESCRIPTION_ID)
|
||||||
|
val descriptionString = requireArguments().getString(DESCRIPTION_STRING) ?: ""
|
||||||
|
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setPositiveButton(R.string.close, null)
|
||||||
|
.setTitle(titleId)
|
||||||
|
|
||||||
|
if (descriptionString.isNotEmpty()) {
|
||||||
|
dialog.setMessage(descriptionString)
|
||||||
|
} else if (descriptionId != 0) {
|
||||||
|
dialog.setMessage(descriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helpLinkId != 0) {
|
||||||
|
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
|
||||||
|
openLink(getString(helpLinkId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLink(link: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "MessageDialogFragment"
|
||||||
|
|
||||||
|
private const val TITLE_ID = "Title"
|
||||||
|
private const val DESCRIPTION_ID = "Description"
|
||||||
|
private const val DESCRIPTION_STRING = "Description_string"
|
||||||
|
private const val HELP_LINK = "Link"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
titleId: Int,
|
||||||
|
descriptionId: Int,
|
||||||
|
helpLinkId: Int = 0
|
||||||
|
): MessageDialogFragment {
|
||||||
|
val dialog = MessageDialogFragment()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.apply {
|
||||||
|
putInt(TITLE_ID, titleId)
|
||||||
|
putInt(DESCRIPTION_ID, descriptionId)
|
||||||
|
putInt(HELP_LINK, helpLinkId)
|
||||||
|
}
|
||||||
|
dialog.arguments = bundle
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
titleId: Int,
|
||||||
|
description: String,
|
||||||
|
helpLinkId: Int = 0
|
||||||
|
): MessageDialogFragment {
|
||||||
|
val dialog = MessageDialogFragment()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.apply {
|
||||||
|
putInt(TITLE_ID, titleId)
|
||||||
|
putString(DESCRIPTION_STRING, description)
|
||||||
|
putInt(HELP_LINK, helpLinkId)
|
||||||
|
}
|
||||||
|
dialog.arguments = bundle
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import info.debatty.java.stringsimilarity.Jaccard
|
||||||
|
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.GameAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentSearchBinding
|
||||||
|
import org.citra.citra_emu.model.Game
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import java.time.temporal.ChronoField
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class SearchFragment : Fragment() {
|
||||||
|
private var _binding: FragmentSearchBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SEARCH_TEXT = "SearchText"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSearchBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||||
|
|
||||||
|
preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.gridGamesSearch.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.game_grid_columns)
|
||||||
|
)
|
||||||
|
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
||||||
|
|
||||||
|
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||||
|
if (text.toString().isNotEmpty()) {
|
||||||
|
binding.clearButton.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.clearButton.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
filterAndSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
gamesViewModel.searchFocused.collect {
|
||||||
|
if (it) {
|
||||||
|
focusSearch()
|
||||||
|
gamesViewModel.setSearchFocused(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
gamesViewModel.games.collect { filterAndSearch() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
gamesViewModel.searchedGames.collect {
|
||||||
|
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
binding.noResultsView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.noResultsView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
|
||||||
|
|
||||||
|
binding.searchBackground.setOnClickListener { focusSearch() }
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
filterAndSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ScoredGame(val score: Double, val item: Game)
|
||||||
|
|
||||||
|
private fun filterAndSearch() {
|
||||||
|
if (binding.searchText.text.toString().isEmpty() &&
|
||||||
|
binding.chipGroup.checkedChipId == View.NO_ID
|
||||||
|
) {
|
||||||
|
gamesViewModel.setSearchedGames(emptyList())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseList = gamesViewModel.games.value
|
||||||
|
val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
|
||||||
|
R.id.chip_recently_played -> {
|
||||||
|
baseList.filter {
|
||||||
|
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
|
||||||
|
lastPlayedTime > (System.currentTimeMillis() - ChronoField.MILLI_OF_DAY.range().maximum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.chip_recently_added -> {
|
||||||
|
baseList.filter {
|
||||||
|
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
||||||
|
addedTime > (System.currentTimeMillis() - ChronoField.MILLI_OF_DAY.range().maximum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.chip_installed -> baseList.filter { it.isInstalled }
|
||||||
|
|
||||||
|
else -> baseList
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.searchText.text.toString().isEmpty() &&
|
||||||
|
binding.chipGroup.checkedChipId != View.NO_ID
|
||||||
|
) {
|
||||||
|
gamesViewModel.setSearchedGames(filteredList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||||
|
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
|
||||||
|
val sortedList: List<Game> = filteredList.mapNotNull { game ->
|
||||||
|
val title = game.title.lowercase(Locale.getDefault())
|
||||||
|
val score = searchAlgorithm.similarity(searchTerm, title)
|
||||||
|
if (score > 0.03) {
|
||||||
|
ScoredGame(score, game)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.sortedByDescending { it.score }.map { it.item }
|
||||||
|
gamesViewModel.setSearchedGames(sortedList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
if (_binding != null) {
|
||||||
|
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun focusSearch() {
|
||||||
|
if (_binding != null) {
|
||||||
|
binding.searchText.requestFocus()
|
||||||
|
val imm = requireActivity()
|
||||||
|
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||||
|
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
||||||
|
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||||
|
val spacingNavigationRail =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
||||||
|
val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
|
||||||
|
|
||||||
|
binding.constraintSearch.updatePadding(
|
||||||
|
left = barInsets.left + cutoutInsets.left,
|
||||||
|
top = barInsets.top,
|
||||||
|
right = barInsets.right + cutoutInsets.right
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.gridGamesSearch.updatePadding(
|
||||||
|
top = extraListSpacing,
|
||||||
|
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
|
||||||
|
)
|
||||||
|
binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
|
||||||
|
|
||||||
|
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
|
binding.frameSearch.updatePadding(left = spacingNavigationRail)
|
||||||
|
binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
|
||||||
|
binding.noResultsView.updatePadding(left = spacingNavigationRail)
|
||||||
|
binding.chipGroup.updatePadding(
|
||||||
|
left = chipSpacing + spacingNavigationRail,
|
||||||
|
right = chipSpacing
|
||||||
|
)
|
||||||
|
mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
|
||||||
|
mlpDivider.rightMargin = chipSpacing
|
||||||
|
} else {
|
||||||
|
binding.frameSearch.updatePadding(right = spacingNavigationRail)
|
||||||
|
binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
|
||||||
|
binding.noResultsView.updatePadding(right = spacingNavigationRail)
|
||||||
|
binding.chipGroup.updatePadding(
|
||||||
|
left = chipSpacing,
|
||||||
|
right = chipSpacing + spacingNavigationRail
|
||||||
|
)
|
||||||
|
mlpDivider.leftMargin = chipSpacing
|
||||||
|
mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
|
||||||
|
}
|
||||||
|
binding.divider.layoutParams = mlpDivider
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class SelectUserDirectoryDialogFragment : DialogFragment() {
|
||||||
|
private lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.select_citra_user_folder)
|
||||||
|
.setMessage(R.string.cannot_skip_directory_description)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
mainActivity.openCitraDirectory.launch(null)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "SelectUserDirectoryDialogFragment"
|
||||||
|
|
||||||
|
fun newInstance(activity: FragmentActivity): SelectUserDirectoryDialogFragment {
|
||||||
|
ViewModelProvider(activity)[HomeViewModel::class.java].setPickingUserDir(true)
|
||||||
|
return SelectUserDirectoryDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,481 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.adapters.SetupAdapter
|
||||||
|
import org.citra.citra_emu.databinding.FragmentSetupBinding
|
||||||
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
|
import org.citra.citra_emu.model.SetupCallback
|
||||||
|
import org.citra.citra_emu.model.SetupPage
|
||||||
|
import org.citra.citra_emu.model.StepState
|
||||||
|
import org.citra.citra_emu.ui.main.MainActivity
|
||||||
|
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||||
|
import org.citra.citra_emu.utils.GameHelper
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.utils.ViewUtils
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class SetupFragment : Fragment() {
|
||||||
|
private var _binding: FragmentSetupBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
|
private lateinit var hasBeenWarned: BooleanArray
|
||||||
|
|
||||||
|
private lateinit var pages: MutableList<SetupPage>
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences
|
||||||
|
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_NEXT_VISIBILITY = "NextButtonVisibility"
|
||||||
|
const val KEY_BACK_VISIBILITY = "BackButtonVisibility"
|
||||||
|
const val KEY_HAS_BEEN_WARNED = "HasBeenWarned"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
exitTransition = MaterialFadeThrough()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSetupBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||||
|
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (binding.viewPager2.currentItem > 0) {
|
||||||
|
pageBackward()
|
||||||
|
} else {
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
requireActivity().window.navigationBarColor =
|
||||||
|
ContextCompat.getColor(requireContext(), android.R.color.transparent)
|
||||||
|
|
||||||
|
pages = mutableListOf()
|
||||||
|
pages.apply {
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_citra_full,
|
||||||
|
R.string.welcome,
|
||||||
|
R.string.welcome_description,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
R.string.get_started,
|
||||||
|
{ pageForward() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_notification,
|
||||||
|
R.string.notifications,
|
||||||
|
R.string.notifications_description,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
R.string.give_permission,
|
||||||
|
{
|
||||||
|
notificationCallback = it
|
||||||
|
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
if (NotificationManagerCompat.from(requireContext())
|
||||||
|
.areNotificationsEnabled()
|
||||||
|
) {
|
||||||
|
StepState.STEP_COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.STEP_INCOMPLETE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
R.string.notification_warning,
|
||||||
|
R.string.notification_warning_description,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_microphone,
|
||||||
|
R.string.microphone_permission,
|
||||||
|
R.string.microphone_permission_description,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
R.string.give_permission,
|
||||||
|
{
|
||||||
|
microphoneCallback = it
|
||||||
|
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
StepState.STEP_COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.STEP_INCOMPLETE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_camera,
|
||||||
|
R.string.camera_permission,
|
||||||
|
R.string.camera_permission_description,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
R.string.give_permission,
|
||||||
|
{
|
||||||
|
cameraCallback = it
|
||||||
|
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
StepState.STEP_COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.STEP_INCOMPLETE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_home,
|
||||||
|
R.string.select_citra_user_folder,
|
||||||
|
R.string.select_citra_user_folder_description,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
R.string.select,
|
||||||
|
{
|
||||||
|
userDirCallback = it
|
||||||
|
openCitraDirectory.launch(null)
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
if (PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||||
|
StepState.STEP_COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.STEP_INCOMPLETE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
R.string.cannot_skip,
|
||||||
|
R.string.cannot_skip_directory_description,
|
||||||
|
R.string.cannot_skip_directory_help
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_controller,
|
||||||
|
R.string.games,
|
||||||
|
R.string.games_description,
|
||||||
|
R.drawable.ic_add,
|
||||||
|
true,
|
||||||
|
R.string.add_games,
|
||||||
|
{
|
||||||
|
gamesDirCallback = it
|
||||||
|
getGamesDirectory.launch(
|
||||||
|
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||||
|
)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
||||||
|
StepState.STEP_COMPLETE
|
||||||
|
} else {
|
||||||
|
StepState.STEP_INCOMPLETE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
R.string.add_games_warning,
|
||||||
|
R.string.add_games_warning_description,
|
||||||
|
R.string.add_games_warning_help
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SetupPage(
|
||||||
|
R.drawable.ic_check,
|
||||||
|
R.string.done,
|
||||||
|
R.string.done_description,
|
||||||
|
R.drawable.ic_arrow_forward,
|
||||||
|
false,
|
||||||
|
R.string.text_continue,
|
||||||
|
{ finishSetup() }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.viewPager2.apply {
|
||||||
|
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
|
||||||
|
offscreenPageLimit = 2
|
||||||
|
isUserInputEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
|
||||||
|
var previousPosition: Int = 0
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
super.onPageSelected(position)
|
||||||
|
|
||||||
|
if (position == 1 && previousPosition == 0) {
|
||||||
|
ViewUtils.showView(binding.buttonNext)
|
||||||
|
ViewUtils.showView(binding.buttonBack)
|
||||||
|
} else if (position == 0 && previousPosition == 1) {
|
||||||
|
ViewUtils.hideView(binding.buttonBack)
|
||||||
|
ViewUtils.hideView(binding.buttonNext)
|
||||||
|
} else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
|
||||||
|
ViewUtils.hideView(binding.buttonNext)
|
||||||
|
} else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
|
||||||
|
ViewUtils.showView(binding.buttonNext)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPosition = position
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.buttonNext.setOnClickListener {
|
||||||
|
val index = binding.viewPager2.currentItem
|
||||||
|
val currentPage = pages[index]
|
||||||
|
|
||||||
|
// Checks if the user has completed the task on the current page
|
||||||
|
if (currentPage.hasWarning || currentPage.isUnskippable) {
|
||||||
|
val stepState = currentPage.stepCompleted.invoke()
|
||||||
|
if (stepState == StepState.STEP_COMPLETE ||
|
||||||
|
stepState == StepState.STEP_UNDEFINED
|
||||||
|
) {
|
||||||
|
pageForward()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage.isUnskippable) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
currentPage.warningTitleId,
|
||||||
|
currentPage.warningDescriptionId,
|
||||||
|
currentPage.warningHelpLinkId
|
||||||
|
).show(childFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasBeenWarned[index]) {
|
||||||
|
SetupWarningDialogFragment.newInstance(
|
||||||
|
currentPage.warningTitleId,
|
||||||
|
currentPage.warningDescriptionId,
|
||||||
|
currentPage.warningHelpLinkId,
|
||||||
|
index
|
||||||
|
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageForward()
|
||||||
|
}
|
||||||
|
binding.buttonBack.setOnClickListener { pageBackward() }
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
|
||||||
|
val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
|
||||||
|
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
|
||||||
|
|
||||||
|
if (nextIsVisible) {
|
||||||
|
binding.buttonNext.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
if (backIsVisible) {
|
||||||
|
binding.buttonBack.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasBeenWarned = BooleanArray(pages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
|
||||||
|
outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
|
||||||
|
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var notificationCallback: SetupCallback
|
||||||
|
private lateinit var microphoneCallback: SetupCallback
|
||||||
|
private lateinit var cameraCallback: SetupCallback
|
||||||
|
|
||||||
|
private val permissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
val page = pages[binding.viewPager2.currentItem]
|
||||||
|
when (page.titleId) {
|
||||||
|
R.string.notifications -> notificationCallback.onStepCompleted()
|
||||||
|
R.string.microphone_permission -> microphoneCallback.onStepCompleted()
|
||||||
|
R.string.camera_permission -> cameraCallback.onStepCompleted()
|
||||||
|
}
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
|
||||||
|
.setAnchorView(binding.buttonNext)
|
||||||
|
.setAction(R.string.grid_menu_core_settings) {
|
||||||
|
val intent =
|
||||||
|
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
val uri = Uri.fromParts("package", requireActivity().packageName, null)
|
||||||
|
intent.data = uri
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var userDirCallback: SetupCallback
|
||||||
|
|
||||||
|
private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
|
||||||
|
ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { result: Uri? ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, userDirCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var gamesDirCallback: SetupCallback
|
||||||
|
|
||||||
|
private val getGamesDirectory =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
requireActivity().contentResolver.takePersistableUriPermission(
|
||||||
|
result,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
// When a new directory is picked, we currently will reset the existing games
|
||||||
|
// database. This effectively means that only one game directory is supported.
|
||||||
|
preferences.edit()
|
||||||
|
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
homeViewModel.setGamesDir(requireActivity(), result.path!!)
|
||||||
|
|
||||||
|
gamesDirCallback.onStepCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishSetup() {
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
|
||||||
|
.apply()
|
||||||
|
mainActivity.finishSetup(binding.root.findNavController())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pageForward() {
|
||||||
|
binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pageBackward() {
|
||||||
|
binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPageWarned(page: Int) {
|
||||||
|
hasBeenWarned[page] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftPadding = barInsets.left + cutoutInsets.left
|
||||||
|
val topPadding = barInsets.top + cutoutInsets.top
|
||||||
|
val rightPadding = barInsets.right + cutoutInsets.right
|
||||||
|
val bottomPadding = barInsets.bottom + cutoutInsets.bottom
|
||||||
|
|
||||||
|
if (resources.getBoolean(R.bool.small_layout)) {
|
||||||
|
binding.viewPager2
|
||||||
|
.updatePadding(left = leftPadding, top = topPadding, right = rightPadding)
|
||||||
|
binding.constraintButtons
|
||||||
|
.updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding)
|
||||||
|
} else {
|
||||||
|
binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding)
|
||||||
|
binding.constraintButtons
|
||||||
|
.setPadding(
|
||||||
|
leftPadding + rightPadding,
|
||||||
|
topPadding,
|
||||||
|
rightPadding + leftPadding,
|
||||||
|
bottomPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
|
||||||
|
class SetupWarningDialogFragment : DialogFragment() {
|
||||||
|
private var titleId: Int = 0
|
||||||
|
private var descriptionId: Int = 0
|
||||||
|
private var helpLinkId: Int = 0
|
||||||
|
private var page: Int = 0
|
||||||
|
|
||||||
|
private lateinit var setupFragment: SetupFragment
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
titleId = requireArguments().getInt(TITLE)
|
||||||
|
descriptionId = requireArguments().getInt(DESCRIPTION)
|
||||||
|
helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
|
page = requireArguments().getInt(PAGE)
|
||||||
|
|
||||||
|
setupFragment = requireParentFragment() as SetupFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
|
||||||
|
setupFragment.pageForward()
|
||||||
|
setupFragment.setPageWarned(page)
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.warning_cancel, null)
|
||||||
|
|
||||||
|
if (titleId != 0) {
|
||||||
|
builder.setTitle(titleId)
|
||||||
|
} else {
|
||||||
|
builder.setTitle("")
|
||||||
|
}
|
||||||
|
if (descriptionId != 0) {
|
||||||
|
builder.setMessage(descriptionId)
|
||||||
|
}
|
||||||
|
if (helpLinkId != 0) {
|
||||||
|
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
||||||
|
val helpLink = resources.getString(helpLinkId)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "SetupWarningDialogFragment"
|
||||||
|
|
||||||
|
private const val TITLE = "Title"
|
||||||
|
private const val DESCRIPTION = "Description"
|
||||||
|
private const val HELP_LINK = "HelpLink"
|
||||||
|
private const val PAGE = "Page"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
titleId: Int,
|
||||||
|
descriptionId: Int,
|
||||||
|
helpLinkId: Int,
|
||||||
|
page: Int
|
||||||
|
): SetupWarningDialogFragment {
|
||||||
|
val dialog = SetupWarningDialogFragment()
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.apply {
|
||||||
|
putInt(TITLE, titleId)
|
||||||
|
putInt(DESCRIPTION, descriptionId)
|
||||||
|
putInt(HELP_LINK, helpLinkId)
|
||||||
|
putInt(PAGE, page)
|
||||||
|
}
|
||||||
|
dialog.arguments = bundle
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,301 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
|
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
|
||||||
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||||
|
|
||||||
|
class SystemFilesFragment : Fragment() {
|
||||||
|
private var _binding: FragmentSystemFilesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val systemFilesViewModel: SystemFilesViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var regionValues: IntArray
|
||||||
|
|
||||||
|
private val systemTypeDropdown = DropdownItem(R.array.systemFileTypeValues)
|
||||||
|
private val systemRegionDropdown = DropdownItem(R.array.systemFileRegionValues)
|
||||||
|
|
||||||
|
private val SYS_TYPE = "SysType"
|
||||||
|
private val REGION = "Region"
|
||||||
|
private val REGION_START = "RegionStart"
|
||||||
|
|
||||||
|
private val homeMenuMap: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
|
private val WARNING_SHOWN = "SystemFilesWarningShown"
|
||||||
|
|
||||||
|
private class DropdownItem(val valuesId: Int) : AdapterView.OnItemClickListener {
|
||||||
|
var position = 0
|
||||||
|
|
||||||
|
fun getValue(resources: Resources): Int {
|
||||||
|
return resources.getIntArray(valuesId)[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(p0: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
this.position = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
NativeLibrary.loadSystemConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSystemFilesBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
if (!preferences.getBoolean(WARNING_SHOWN, false)) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.home_menu_warning,
|
||||||
|
R.string.home_menu_warning_description
|
||||||
|
).show(childFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(WARNING_SHOWN, true)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarSystemFiles.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove workaround for text filtering issue in material components when fixed
|
||||||
|
// https://github.com/material-components/material-components-android/issues/1464
|
||||||
|
binding.dropdownSystemType.isSaveEnabled = false
|
||||||
|
binding.dropdownSystemRegion.isSaveEnabled = false
|
||||||
|
binding.dropdownSystemRegionStart.isSaveEnabled = false
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
systemFilesViewModel.shouldRefresh.collect {
|
||||||
|
if (it) {
|
||||||
|
reloadUi()
|
||||||
|
systemFilesViewModel.setShouldRefresh(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadUi()
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemType,
|
||||||
|
systemTypeDropdown,
|
||||||
|
savedInstanceState.getInt(SYS_TYPE)
|
||||||
|
)
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
systemRegionDropdown,
|
||||||
|
savedInstanceState.getInt(REGION)
|
||||||
|
)
|
||||||
|
binding.dropdownSystemRegionStart
|
||||||
|
.setText(savedInstanceState.getString(REGION_START), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putInt(SYS_TYPE, systemTypeDropdown.position)
|
||||||
|
outState.putInt(REGION, systemRegionDropdown.position)
|
||||||
|
outState.putString(REGION_START, binding.dropdownSystemRegionStart.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
NativeLibrary.saveSystemConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reloadUi() {
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
|
||||||
|
binding.switchRunSystemSetup.isChecked = NativeLibrary.getIsSystemSetupNeeded()
|
||||||
|
binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
NativeLibrary.setSystemSetupNeeded(isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false)
|
||||||
|
binding.switchShowApps.isChecked = showHomeApps
|
||||||
|
binding.switchShowApps.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(Settings.PREF_SHOW_HOME_APPS, isChecked)
|
||||||
|
.apply()
|
||||||
|
gamesViewModel.setShouldSwapData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NativeLibrary.areKeysAvailable()) {
|
||||||
|
binding.apply {
|
||||||
|
systemType.isEnabled = false
|
||||||
|
systemRegion.isEnabled = false
|
||||||
|
buttonDownloadHomeMenu.isEnabled = false
|
||||||
|
textKeysMissing.visibility = View.VISIBLE
|
||||||
|
textKeysMissingHelp.visibility = View.VISIBLE
|
||||||
|
textKeysMissingHelp.text =
|
||||||
|
Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
|
||||||
|
textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
populateDownloadOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonDownloadHomeMenu.setOnClickListener {
|
||||||
|
val titleIds = NativeLibrary.getSystemTitleIds(
|
||||||
|
systemTypeDropdown.getValue(resources),
|
||||||
|
systemRegionDropdown.getValue(resources)
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
|
||||||
|
childFragmentManager,
|
||||||
|
DownloadSystemFilesDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
populateHomeMenuOptions()
|
||||||
|
binding.buttonStartHomeMenu.setOnClickListener {
|
||||||
|
val menuPath = homeMenuMap[binding.dropdownSystemRegionStart.text.toString()]!!
|
||||||
|
EmulationActivity.launch(requireActivity(), menuPath, getString(R.string.home_menu))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateDropdown(
|
||||||
|
dropdown: MaterialAutoCompleteTextView,
|
||||||
|
valuesId: Int,
|
||||||
|
dropdownItem: DropdownItem
|
||||||
|
) {
|
||||||
|
val valuesAdapter = ArrayAdapter.createFromResource(
|
||||||
|
requireContext(),
|
||||||
|
valuesId,
|
||||||
|
R.layout.support_simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
dropdown.setAdapter(valuesAdapter)
|
||||||
|
dropdown.onItemClickListener = dropdownItem
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDropdownSelection(
|
||||||
|
dropdown: MaterialAutoCompleteTextView,
|
||||||
|
dropdownItem: DropdownItem,
|
||||||
|
selection: Int
|
||||||
|
) {
|
||||||
|
if (dropdown.adapter != null) {
|
||||||
|
dropdown.setText(dropdown.adapter.getItem(selection).toString(), false)
|
||||||
|
}
|
||||||
|
dropdownItem.position = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateDownloadOptions() {
|
||||||
|
populateDropdown(binding.dropdownSystemType, R.array.systemFileTypes, systemTypeDropdown)
|
||||||
|
populateDropdown(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
R.array.systemFileRegions,
|
||||||
|
systemRegionDropdown
|
||||||
|
)
|
||||||
|
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemType,
|
||||||
|
systemTypeDropdown,
|
||||||
|
systemTypeDropdown.position
|
||||||
|
)
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
systemRegionDropdown,
|
||||||
|
systemRegionDropdown.position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateHomeMenuOptions() {
|
||||||
|
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
|
||||||
|
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
|
||||||
|
regionValues.forEachIndexed { i: Int, region: Int ->
|
||||||
|
val regionString = regionEntries[i]
|
||||||
|
val regionPath = NativeLibrary.getHomeMenuPath(region)
|
||||||
|
homeMenuMap[regionString] = regionPath
|
||||||
|
}
|
||||||
|
|
||||||
|
val availableMenus = homeMenuMap.filter { it.value != "" }
|
||||||
|
if (availableMenus.isNotEmpty()) {
|
||||||
|
binding.systemRegionStart.isEnabled = true
|
||||||
|
binding.buttonStartHomeMenu.isEnabled = true
|
||||||
|
|
||||||
|
binding.dropdownSystemRegionStart.setAdapter(
|
||||||
|
ArrayAdapter(
|
||||||
|
requireContext(),
|
||||||
|
R.layout.support_simple_spinner_dropdown_item,
|
||||||
|
availableMenus.keys.toList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarSystemFiles.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
val mlpScrollSystemFiles =
|
||||||
|
binding.scrollSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpScrollSystemFiles.leftMargin = leftInsets
|
||||||
|
mlpScrollSystemFiles.rightMargin = rightInsets
|
||||||
|
binding.scrollSystemFiles.layoutParams = mlpScrollSystemFiles
|
||||||
|
|
||||||
|
binding.scrollSystemFiles.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,76 +0,0 @@
|
|||||||
package org.citra.citra_emu.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
public final class Game {
|
|
||||||
private String mTitle;
|
|
||||||
private String mDescription;
|
|
||||||
private String mPath;
|
|
||||||
private String mGameId;
|
|
||||||
private String mCompany;
|
|
||||||
private String mRegions;
|
|
||||||
|
|
||||||
public Game(String title, String description, String regions, String path,
|
|
||||||
String gameId, String company) {
|
|
||||||
mTitle = title;
|
|
||||||
mDescription = description;
|
|
||||||
mRegions = regions;
|
|
||||||
mPath = path;
|
|
||||||
mGameId = gameId;
|
|
||||||
mCompany = company;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
|
|
||||||
if (gameId.isEmpty()) {
|
|
||||||
// Homebrew, etc. may not have a game ID, use filename as a unique identifier
|
|
||||||
gameId = Paths.get(path).getFileName().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
values.put(GameDatabase.KEY_GAME_TITLE, title);
|
|
||||||
values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
|
|
||||||
values.put(GameDatabase.KEY_GAME_REGIONS, regions);
|
|
||||||
values.put(GameDatabase.KEY_GAME_PATH, path);
|
|
||||||
values.put(GameDatabase.KEY_GAME_ID, gameId);
|
|
||||||
values.put(GameDatabase.KEY_GAME_COMPANY, company);
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Game fromCursor(Cursor cursor) {
|
|
||||||
return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
|
|
||||||
cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
|
|
||||||
cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
|
|
||||||
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
|
|
||||||
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
|
|
||||||
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return mTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return mDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCompany() {
|
|
||||||
return mCompany;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRegions() {
|
|
||||||
return mRegions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPath() {
|
|
||||||
return mPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGameId() {
|
|
||||||
return mGameId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.util.HashSet
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
|
class Game(
|
||||||
|
val title: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val path: String = "",
|
||||||
|
val titleId: Long = 0L,
|
||||||
|
val company: String = "",
|
||||||
|
val regions: String = "",
|
||||||
|
val isInstalled: Boolean = false,
|
||||||
|
val isSystemTitle: Boolean = false,
|
||||||
|
val isVisibleSystemTitle: Boolean = false,
|
||||||
|
val icon: IntArray? = null,
|
||||||
|
val filename: String
|
||||||
|
) : Parcelable {
|
||||||
|
val keyAddedToLibraryTime get() = "${filename}_AddedToLibraryTime"
|
||||||
|
val keyLastPlayedTime get() = "${filename}_LastPlayed"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is Game) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashCode() == other.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = title.hashCode()
|
||||||
|
result = 31 * result + description.hashCode()
|
||||||
|
result = 31 * result + regions.hashCode()
|
||||||
|
result = 31 * result + path.hashCode()
|
||||||
|
result = 31 * result + titleId.hashCode()
|
||||||
|
result = 31 * result + company.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val allExtensions: Set<String> get() = extensions + badExtensions
|
||||||
|
|
||||||
|
val extensions: Set<String> = HashSet(
|
||||||
|
listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
|
||||||
|
)
|
||||||
|
|
||||||
|
val badExtensions: Set<String> = HashSet(
|
||||||
|
listOf("rar", "zip", "7z", "torrent", "tar", "gz")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,279 +0,0 @@
|
|||||||
package org.citra.citra_emu.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.utils.FileUtil;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Array;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import rx.Observable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class that provides several utilities simplifying interaction with
|
|
||||||
* the SQLite database.
|
|
||||||
*/
|
|
||||||
public final class GameDatabase extends SQLiteOpenHelper {
|
|
||||||
public static final int COLUMN_DB_ID = 0;
|
|
||||||
public static final int GAME_COLUMN_PATH = 1;
|
|
||||||
public static final int GAME_COLUMN_TITLE = 2;
|
|
||||||
public static final int GAME_COLUMN_DESCRIPTION = 3;
|
|
||||||
public static final int GAME_COLUMN_REGIONS = 4;
|
|
||||||
public static final int GAME_COLUMN_GAME_ID = 5;
|
|
||||||
public static final int GAME_COLUMN_COMPANY = 6;
|
|
||||||
public static final int FOLDER_COLUMN_PATH = 1;
|
|
||||||
public static final String KEY_DB_ID = "_id";
|
|
||||||
public static final String KEY_GAME_PATH = "path";
|
|
||||||
public static final String KEY_GAME_TITLE = "title";
|
|
||||||
public static final String KEY_GAME_DESCRIPTION = "description";
|
|
||||||
public static final String KEY_GAME_REGIONS = "regions";
|
|
||||||
public static final String KEY_GAME_ID = "game_id";
|
|
||||||
public static final String KEY_GAME_COMPANY = "company";
|
|
||||||
public static final String KEY_FOLDER_PATH = "path";
|
|
||||||
public static final String TABLE_NAME_FOLDERS = "folders";
|
|
||||||
public static final String TABLE_NAME_GAMES = "games";
|
|
||||||
private static final int DB_VERSION = 2;
|
|
||||||
private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
|
|
||||||
private static final String TYPE_INTEGER = " INTEGER";
|
|
||||||
private static final String TYPE_STRING = " TEXT";
|
|
||||||
|
|
||||||
private static final String CONSTRAINT_UNIQUE = " UNIQUE";
|
|
||||||
|
|
||||||
private static final String SEPARATOR = ", ";
|
|
||||||
|
|
||||||
private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
|
|
||||||
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
|
|
||||||
+ KEY_GAME_PATH + TYPE_STRING + SEPARATOR
|
|
||||||
+ KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
|
|
||||||
+ KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
|
|
||||||
+ KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
|
|
||||||
+ KEY_GAME_ID + TYPE_STRING + SEPARATOR
|
|
||||||
+ KEY_GAME_COMPANY + TYPE_STRING + ")";
|
|
||||||
|
|
||||||
private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
|
|
||||||
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
|
|
||||||
+ KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
|
|
||||||
|
|
||||||
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
|
|
||||||
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
public GameDatabase(Context context) {
|
|
||||||
// Superclass constructor builds a database or uses an existing one.
|
|
||||||
super(context, "games.db", null, DB_VERSION);
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(SQLiteDatabase database) {
|
|
||||||
Log.debug("[GameDatabase] GameDatabase - Creating database...");
|
|
||||||
|
|
||||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) {
|
|
||||||
Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..");
|
|
||||||
execSqlAndLog(database, SQL_DELETE_FOLDERS);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
|
||||||
|
|
||||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
|
|
||||||
Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " +
|
|
||||||
newVersion);
|
|
||||||
|
|
||||||
// Delete all the games
|
|
||||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetDatabase(SQLiteDatabase database) {
|
|
||||||
execSqlAndLog(database, SQL_DELETE_FOLDERS);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_FOLDERS);
|
|
||||||
|
|
||||||
execSqlAndLog(database, SQL_DELETE_GAMES);
|
|
||||||
execSqlAndLog(database, SQL_CREATE_GAMES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scanLibrary(SQLiteDatabase database) {
|
|
||||||
// Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
|
|
||||||
Cursor fileCursor = database.query(TABLE_NAME_GAMES,
|
|
||||||
null, // Get all columns.
|
|
||||||
null, // Get all rows.
|
|
||||||
null,
|
|
||||||
null, // No grouping.
|
|
||||||
null,
|
|
||||||
null); // Order of games is irrelevant.
|
|
||||||
|
|
||||||
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
|
|
||||||
fileCursor.moveToPosition(-1);
|
|
||||||
|
|
||||||
while (fileCursor.moveToNext()) {
|
|
||||||
String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
|
|
||||||
|
|
||||||
if (!FileUtil.Exists(mContext, gamePath)) {
|
|
||||||
Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
|
|
||||||
gamePath);
|
|
||||||
database.delete(TABLE_NAME_GAMES,
|
|
||||||
KEY_DB_ID + " = ?",
|
|
||||||
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a cursor listing all the folders the user has added to the library.
|
|
||||||
Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
|
|
||||||
null, // Get all columns.
|
|
||||||
null, // Get all rows.
|
|
||||||
null,
|
|
||||||
null, // No grouping.
|
|
||||||
null,
|
|
||||||
null); // Order of folders is irrelevant.
|
|
||||||
|
|
||||||
Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(
|
|
||||||
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app", ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz"));
|
|
||||||
|
|
||||||
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
|
|
||||||
folderCursor.moveToPosition(-1);
|
|
||||||
|
|
||||||
// Iterate through all results of the DB query (i.e. all folders in the library.)
|
|
||||||
while (folderCursor.moveToNext()) {
|
|
||||||
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
|
|
||||||
|
|
||||||
Uri folder = Uri.parse(folderPath);
|
|
||||||
// If the folder is empty because it no longer exists, remove it from the library.
|
|
||||||
CheapDocument[] files = FileUtil.listFiles(mContext, folder);
|
|
||||||
if (files.length == 0) {
|
|
||||||
Log.error(
|
|
||||||
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
|
||||||
database.delete(TABLE_NAME_FOLDERS,
|
|
||||||
KEY_DB_ID + " = ?",
|
|
||||||
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
|
||||||
}
|
|
||||||
|
|
||||||
addGamesRecursive(database, files, allowedExtensions, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
fileCursor.close();
|
|
||||||
folderCursor.close();
|
|
||||||
|
|
||||||
Arrays.stream(NativeLibrary.GetInstalledGamePaths())
|
|
||||||
.forEach(filePath -> attemptToAddGame(database, filePath));
|
|
||||||
|
|
||||||
database.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGamesRecursive(SQLiteDatabase database, CheapDocument[] files,
|
|
||||||
Set<String> allowedExtensions, int depth) {
|
|
||||||
if (depth <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (CheapDocument file : files) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
|
||||||
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
|
|
||||||
CheapDocument[] children = FileUtil.listFiles(mContext, file.getUri());
|
|
||||||
this.addGamesRecursive(database, children, newExtensions, depth - 1);
|
|
||||||
} else {
|
|
||||||
String filename = file.getUri().toString();
|
|
||||||
|
|
||||||
int extensionStart = filename.lastIndexOf('.');
|
|
||||||
if (extensionStart > 0) {
|
|
||||||
String fileExtension = filename.substring(extensionStart);
|
|
||||||
|
|
||||||
// Check that the file has an extension we care about before trying to read out of it.
|
|
||||||
if (allowedExtensions.contains(fileExtension.toLowerCase())) {
|
|
||||||
attemptToAddGame(database, filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
|
|
||||||
GameInfo gameInfo;
|
|
||||||
try {
|
|
||||||
gameInfo = new GameInfo(filePath);
|
|
||||||
} catch (IOException e) {
|
|
||||||
gameInfo = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = gameInfo != null ? gameInfo.getTitle() : "";
|
|
||||||
|
|
||||||
// If the game's title field is empty, use the filename.
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
name = filePath.substring(filePath.lastIndexOf("/") + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentValues game = Game.asContentValues(name,
|
|
||||||
filePath.replace("\n", " "),
|
|
||||||
gameInfo != null ? gameInfo.getRegions() : "Invalid region",
|
|
||||||
filePath,
|
|
||||||
filePath,
|
|
||||||
gameInfo != null ? gameInfo.getCompany() : "");
|
|
||||||
|
|
||||||
// Try to update an existing game first.
|
|
||||||
int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update.
|
|
||||||
game,
|
|
||||||
// The values to fill the row with.
|
|
||||||
KEY_GAME_ID + " = ?",
|
|
||||||
// The WHERE clause used to find the right row.
|
|
||||||
new String[]{game.getAsString(
|
|
||||||
KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this,
|
|
||||||
// which is provided as an array because there
|
|
||||||
// could potentially be more than one argument.
|
|
||||||
|
|
||||||
// If update fails, insert a new game instead.
|
|
||||||
if (rowsMatched == 0) {
|
|
||||||
Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE));
|
|
||||||
database.insert(TABLE_NAME_GAMES, null, game);
|
|
||||||
} else {
|
|
||||||
Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Observable<Cursor> getGames() {
|
|
||||||
return Observable.create(subscriber ->
|
|
||||||
{
|
|
||||||
Log.info("[GameDatabase] Reading games list...");
|
|
||||||
|
|
||||||
SQLiteDatabase database = getReadableDatabase();
|
|
||||||
Cursor resultCursor = database.query(
|
|
||||||
TABLE_NAME_GAMES,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
KEY_GAME_TITLE + " ASC"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pass the result cursor to the consumer.
|
|
||||||
subscriber.onNext(resultCursor);
|
|
||||||
|
|
||||||
// Tell the consumer we're done; it will unsubscribe implicitly.
|
|
||||||
subscriber.onCompleted();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void execSqlAndLog(SQLiteDatabase database, String sql) {
|
|
||||||
Log.verbose("[GameDatabase] Executing SQL: " + sql);
|
|
||||||
database.execSQL(sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package org.citra.citra_emu.model;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class GameInfo {
|
|
||||||
@Keep
|
|
||||||
private final long mPointer;
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
public GameInfo(String path) throws IOException {
|
|
||||||
mPointer = initialize(path);
|
|
||||||
if (mPointer == 0L) {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native long initialize(String path);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected native void finalize();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getTitle();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getRegions();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public native String getCompany();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public native int[] getIcon();
|
|
||||||
}
|
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class GameInfo(path: String) {
|
||||||
|
@Keep
|
||||||
|
private val pointer: Long
|
||||||
|
|
||||||
|
init {
|
||||||
|
pointer = initialize(path)
|
||||||
|
if (pointer == 0L) {
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected external fun finalize()
|
||||||
|
|
||||||
|
external fun getTitle(): String
|
||||||
|
|
||||||
|
external fun getRegions(): String
|
||||||
|
|
||||||
|
external fun getCompany(): String
|
||||||
|
|
||||||
|
external fun getIcon(): IntArray?
|
||||||
|
|
||||||
|
external fun getIsVisibleSystemTitle(): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
private external fun initialize(path: String): Long
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,138 +0,0 @@
|
|||||||
package org.citra.citra_emu.model;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.BuildConfig;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an interface allowing Activities to interact with the SQLite database.
|
|
||||||
* CRUD methods in this class can be called by Activities using getContentResolver().
|
|
||||||
*/
|
|
||||||
public final class GameProvider extends ContentProvider {
|
|
||||||
public static final String REFRESH_LIBRARY = "refresh";
|
|
||||||
public static final String RESET_LIBRARY = "reset";
|
|
||||||
|
|
||||||
public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
|
|
||||||
public static final Uri URI_FOLDER =
|
|
||||||
Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
|
|
||||||
public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/");
|
|
||||||
public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/");
|
|
||||||
|
|
||||||
public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder";
|
|
||||||
public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game";
|
|
||||||
|
|
||||||
|
|
||||||
private GameDatabase mDbHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
Log.info("[GameProvider] Creating Content Provider...");
|
|
||||||
|
|
||||||
mDbHelper = new GameDatabase(getContext());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs, String sortOrder) {
|
|
||||||
Log.info("[GameProvider] Querying URI: " + uri);
|
|
||||||
|
|
||||||
SQLiteDatabase db = mDbHelper.getReadableDatabase();
|
|
||||||
|
|
||||||
String table = uri.getLastPathSegment();
|
|
||||||
|
|
||||||
if (table == null) {
|
|
||||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
|
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
|
||||||
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(@NonNull Uri uri) {
|
|
||||||
Log.verbose("[GameProvider] Getting MIME type for URI: " + uri);
|
|
||||||
String lastSegment = uri.getLastPathSegment();
|
|
||||||
|
|
||||||
if (lastSegment == null) {
|
|
||||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
|
|
||||||
return MIME_TYPE_FOLDER;
|
|
||||||
} else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) {
|
|
||||||
return MIME_TYPE_GAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.error("[GameProvider] Unknown MIME type for URI: " + uri);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
|
||||||
Log.info("[GameProvider] Inserting row at URI: " + uri);
|
|
||||||
|
|
||||||
SQLiteDatabase database = mDbHelper.getWritableDatabase();
|
|
||||||
String table = uri.getLastPathSegment();
|
|
||||||
|
|
||||||
if (table != null) {
|
|
||||||
if (table.equals(RESET_LIBRARY)) {
|
|
||||||
mDbHelper.resetDatabase(database);
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
if (table.equals(REFRESH_LIBRARY)) {
|
|
||||||
Log.info(
|
|
||||||
"[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents...");
|
|
||||||
mDbHelper.scanLibrary(database);
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
|
||||||
|
|
||||||
// If insertion was successful...
|
|
||||||
if (id > 0) {
|
|
||||||
// If we just added a folder, add its contents to the game list.
|
|
||||||
if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
|
|
||||||
mDbHelper.scanLibrary(database);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the UI that its contents should be refreshed.
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
uri = Uri.withAppendedPath(uri, Long.toString(id));
|
|
||||||
} else {
|
|
||||||
Log.error("[GameProvider] Row already exists: " + uri + " id: " + id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.error("[GameProvider] Badly formatted URI: " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
database.close();
|
|
||||||
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
|
||||||
Log.error("[GameProvider] Delete operations unsupported. URI: " + uri);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(@NonNull Uri uri, ContentValues values, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
Log.error("[GameProvider] Update operations unsupported. URI: " + uri);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.model
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
data class HomeSetting(
|
||||||
|
val titleId: Int,
|
||||||
|
val descriptionId: Int,
|
||||||
|
val iconId: Int,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val isEnabled: () -> Boolean = { true },
|
||||||
|
val disabledTitleId: Int = 0,
|
||||||
|
val disabledMessageId: Int = 0,
|
||||||
|
val details: StateFlow<String> = MutableStateFlow("")
|
||||||
|
)
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class License(
|
||||||
|
@StringRes val titleId: Int,
|
||||||
|
@StringRes val descriptionId: Int,
|
||||||
|
@StringRes val linkId: Int,
|
||||||
|
@StringRes val copyrightId: Int = 0,
|
||||||
|
@StringRes val licenseId: Int = 0,
|
||||||
|
@StringRes val licenseLinkId: Int = 0
|
||||||
|
) : Parcelable
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.model
|
||||||
|
|
||||||
|
data class SetupPage(
|
||||||
|
val iconId: Int,
|
||||||
|
val titleId: Int,
|
||||||
|
val descriptionId: Int,
|
||||||
|
val buttonIconId: Int,
|
||||||
|
val leftAlignedIcon: Boolean,
|
||||||
|
val buttonTextId: Int,
|
||||||
|
val buttonAction: (callback: SetupCallback) -> Unit,
|
||||||
|
val isUnskippable: Boolean = false,
|
||||||
|
val hasWarning: Boolean = false,
|
||||||
|
val stepCompleted: () -> StepState = { StepState.STEP_UNDEFINED },
|
||||||
|
val warningTitleId: Int = 0,
|
||||||
|
val warningDescriptionId: Int = 0,
|
||||||
|
val warningHelpLinkId: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SetupCallback {
|
||||||
|
fun onStepCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class StepState {
|
||||||
|
STEP_COMPLETE,
|
||||||
|
STEP_INCOMPLETE,
|
||||||
|
STEP_UNDEFINED
|
||||||
|
}
|
||||||
@ -347,7 +347,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
|
|||||||
if (!button.updateStatus(event)) {
|
if (!button.updateStatus(event)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), button.getStatus());
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), button.getStatus());
|
||||||
shouldUpdateView = true;
|
shouldUpdateView = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,10 +355,10 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
|
|||||||
if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) {
|
if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus());
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus());
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getDownId(), dpad.getDownStatus());
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getDownId(), dpad.getDownStatus());
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getLeftId(), dpad.getLeftStatus());
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getLeftId(), dpad.getLeftStatus());
|
||||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getRightId(), dpad.getRightStatus());
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getRightId(), dpad.getRightStatus());
|
||||||
shouldUpdateView = true;
|
shouldUpdateView = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,7 +367,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int axisID = joystick.getJoystickId();
|
int axisID = joystick.getJoystickId();
|
||||||
NativeLibrary
|
NativeLibrary.INSTANCE
|
||||||
.onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, joystick.getXAxis(), joystick.getYAxis());
|
.onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, joystick.getXAxis(), joystick.getYAxis());
|
||||||
shouldUpdateView = true;
|
shouldUpdateView = true;
|
||||||
}
|
}
|
||||||
@ -390,7 +390,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
|
|||||||
boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
|
boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
|
||||||
|
|
||||||
if (isActionDown && !isTouchInputConsumed(pointerId)) {
|
if (isActionDown && !isTouchInputConsumed(pointerId)) {
|
||||||
NativeLibrary.onTouchEvent(xPosition, yPosition, true);
|
NativeLibrary.INSTANCE.onTouchEvent(xPosition, yPosition, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActionMove) {
|
if (isActionMove) {
|
||||||
@ -399,12 +399,12 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener {
|
|||||||
if (isTouchInputConsumed(fingerId)) {
|
if (isTouchInputConsumed(fingerId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NativeLibrary.onTouchMoved(xPosition, yPosition);
|
NativeLibrary.INSTANCE.onTouchMoved(xPosition, yPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActionUp && !isTouchInputConsumed(pointerId)) {
|
if (isActionUp && !isTouchInputConsumed(pointerId)) {
|
||||||
NativeLibrary.onTouchEvent(0, 0, false);
|
NativeLibrary.INSTANCE.onTouchEvent(0, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,334 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.main;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
||||||
import java.util.Collections;
|
|
||||||
import androidx.core.graphics.Insets;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.WindowCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.ExistingWorkPolicy;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.OutOfQuotaPolicy;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import androidx.work.WorkRequest;
|
|
||||||
|
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
|
||||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
|
||||||
import org.citra.citra_emu.model.GameProvider;
|
|
||||||
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
|
||||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
|
||||||
import org.citra.citra_emu.utils.BillingManager;
|
|
||||||
import org.citra.citra_emu.utils.CiaInstallWorker;
|
|
||||||
import org.citra.citra_emu.utils.CitraDirectoryHelper;
|
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
|
||||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
|
||||||
import org.citra.citra_emu.utils.InsetsHelper;
|
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
|
||||||
import org.citra.citra_emu.utils.StartupHandler;
|
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
|
|
||||||
* individually display a grid of available games for each Fragment, in a tabbed layout.
|
|
||||||
*/
|
|
||||||
public final class MainActivity extends AppCompatActivity implements MainView {
|
|
||||||
private Toolbar mToolbar;
|
|
||||||
private int mFrameLayoutId;
|
|
||||||
private PlatformGamesFragment mPlatformGamesFragment;
|
|
||||||
|
|
||||||
private final MainPresenter mPresenter = new MainPresenter(this);
|
|
||||||
|
|
||||||
// private final CiaInstallWorker mCiaInstallWorker = new CiaInstallWorker();
|
|
||||||
|
|
||||||
// Singleton to manage user billing state
|
|
||||||
private static BillingManager mBillingManager;
|
|
||||||
|
|
||||||
private static MenuItem mPremiumButton;
|
|
||||||
|
|
||||||
private final CitraDirectoryHelper citraDirectoryHelper = new CitraDirectoryHelper(this, () -> {
|
|
||||||
// If mPlatformGamesFragment is null means game directory have not been set yet.
|
|
||||||
if (mPlatformGamesFragment == null) {
|
|
||||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.add(mFrameLayoutId, mPlatformGamesFragment)
|
|
||||||
.commit();
|
|
||||||
showGameInstallDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Uri> mOpenCitraDirectory =
|
|
||||||
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
|
||||||
if (result == null)
|
|
||||||
return;
|
|
||||||
citraDirectoryHelper.showCitraDirectoryDialog(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Uri> mOpenGameListLauncher =
|
|
||||||
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
|
||||||
if (result == null)
|
|
||||||
return;
|
|
||||||
int takeFlags =
|
|
||||||
(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
getContentResolver().takePersistableUriPermission(result, takeFlags);
|
|
||||||
// When a new directory is picked, we currently will reset the existing games
|
|
||||||
// database. This effectively means that only one game directory is supported.
|
|
||||||
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
|
|
||||||
getContentResolver().insert(GameProvider.URI_RESET, null);
|
|
||||||
// Add the new directory
|
|
||||||
mPresenter.onDirectorySelected(result.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<Boolean> mInstallCiaFileLauncher =
|
|
||||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
|
||||||
if (result == null)
|
|
||||||
return;
|
|
||||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
|
||||||
result, getApplicationContext(), Collections.singletonList("cia"));
|
|
||||||
if (selectedFiles == null) {
|
|
||||||
Toast
|
|
||||||
.makeText(getApplicationContext(), R.string.cia_file_not_found,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WorkManager workManager = WorkManager.getInstance(getApplicationContext());
|
|
||||||
workManager.enqueueUniqueWork("installCiaWork", ExistingWorkPolicy.APPEND_OR_REPLACE,
|
|
||||||
new OneTimeWorkRequest.Builder(CiaInstallWorker.class)
|
|
||||||
.setInputData(
|
|
||||||
new Data.Builder().putStringArray("CIA_FILES", selectedFiles)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
private final ActivityResultLauncher<String> requestNotificationPermissionLauncher =
|
|
||||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { });
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
|
|
||||||
splashScreen.setKeepOnScreenCondition(
|
|
||||||
()
|
|
||||||
-> (PermissionsHandler.hasWriteAccess(this) &&
|
|
||||||
!DirectoryInitialization.areCitraDirectoriesReady()));
|
|
||||||
|
|
||||||
ThemeUtil.applyTheme(this);
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
||||||
|
|
||||||
findViews();
|
|
||||||
|
|
||||||
setSupportActionBar(mToolbar);
|
|
||||||
|
|
||||||
mFrameLayoutId = R.id.games_platform_frame;
|
|
||||||
mPresenter.onCreate();
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
StartupHandler.HandleInit(this, mOpenCitraDirectory);
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
|
||||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
|
||||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
|
||||||
}
|
|
||||||
PicassoUtils.init();
|
|
||||||
|
|
||||||
// Setup billing manager, so we can globally query for Premium status
|
|
||||||
mBillingManager = new BillingManager(this);
|
|
||||||
|
|
||||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
|
||||||
EmulationActivity.tryDismissRunningNotification(this);
|
|
||||||
|
|
||||||
setInsets();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
|
||||||
if (getSupportFragmentManager() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (outState == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
|
|
||||||
|
|
||||||
ThemeUtil.setSystemBarMode(this, ThemeUtil.getIsLightMode(getResources()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Replace with a ButterKnife injection.
|
|
||||||
private void findViews() {
|
|
||||||
mToolbar = findViewById(R.id.toolbar_main);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.menu_game_grid, menu);
|
|
||||||
mPremiumButton = menu.findItem(R.id.button_premium);
|
|
||||||
|
|
||||||
if (mBillingManager.isPremiumCached()) {
|
|
||||||
// User had premium in a previous session, hide upsell option
|
|
||||||
setPremiumButtonVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static public void setPremiumButtonVisible(boolean isVisible) {
|
|
||||||
if (mPremiumButton != null) {
|
|
||||||
mPremiumButton.setVisible(isVisible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MainView
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setVersionString(String version) {
|
|
||||||
mToolbar.setSubtitle(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void refresh() {
|
|
||||||
getContentResolver().insert(GameProvider.URI_REFRESH, null);
|
|
||||||
refreshFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void launchSettingsActivity(String menuTag) {
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
|
||||||
SettingsActivity.launch(this, menuTag, "");
|
|
||||||
} else {
|
|
||||||
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void launchFileListActivity(int request) {
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
|
||||||
switch (request) {
|
|
||||||
case MainPresenter.REQUEST_SELECT_CITRA_DIRECTORY:
|
|
||||||
mOpenCitraDirectory.launch(null);
|
|
||||||
break;
|
|
||||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
|
||||||
mOpenGameListLauncher.launch(null);
|
|
||||||
break;
|
|
||||||
case MainPresenter.REQUEST_INSTALL_CIA:
|
|
||||||
mInstallCiaFileLauncher.launch(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the framework whenever any actionbar/toolbar icon is clicked.
|
|
||||||
*
|
|
||||||
* @param item The icon that was clicked on.
|
|
||||||
* @return True if the event was handled, false to bubble it up to the OS.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
return mPresenter.handleOptionSelection(item.getItemId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshFragment() {
|
|
||||||
if (mPlatformGamesFragment != null) {
|
|
||||||
mPlatformGamesFragment.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showGameInstallDialog() {
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
|
||||||
.setTitle(R.string.app_name)
|
|
||||||
.setMessage(R.string.app_game_install_description)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(android.R.string.ok,
|
|
||||||
(d, v) -> mOpenGameListLauncher.launch(null))
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
EmulationActivity.tryDismissRunningNotification(this);
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if Premium subscription is currently active
|
|
||||||
*/
|
|
||||||
public static boolean isPremiumActive() {
|
|
||||||
return mBillingManager.isPremiumActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the billing flow for Premium
|
|
||||||
*
|
|
||||||
* @param callback Optional callback, called once, on completion of billing
|
|
||||||
*/
|
|
||||||
public static void invokePremiumBilling(Runnable callback) {
|
|
||||||
mBillingManager.invokePremiumBilling(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInsets() {
|
|
||||||
AppBarLayout appBar = findViewById(R.id.appbar);
|
|
||||||
FrameLayout frame = findViewById(R.id.games_platform_frame);
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
|
||||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
||||||
InsetsHelper.insetAppBar(insets, appBar);
|
|
||||||
frame.setPadding(insets.left, 0, insets.right, 0);
|
|
||||||
return windowInsets;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,327 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.ui.main
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.animation.PathInterpolator
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.navigation.ui.setupWithNavController
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.OutOfQuotaPolicy
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
|
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||||
|
import org.citra.citra_emu.databinding.ActivityMainBinding
|
||||||
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||||
|
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||||
|
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
|
||||||
|
import org.citra.citra_emu.utils.CiaInstallWorker
|
||||||
|
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||||
|
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||||
|
import org.citra.citra_emu.utils.FileBrowserHelper
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
|
import org.citra.citra_emu.utils.ThemeUtil
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by viewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val splashScreen = installSplashScreen()
|
||||||
|
splashScreen.setKeepOnScreenCondition {
|
||||||
|
!DirectoryInitialization.areCitraDirectoriesReady() &&
|
||||||
|
PermissionsHandler.hasWriteAccess(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeUtil.setTheme(this)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||||
|
|
||||||
|
window.statusBarColor =
|
||||||
|
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
||||||
|
window.navigationBarColor =
|
||||||
|
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
||||||
|
|
||||||
|
binding.statusBarShade.setBackgroundColor(
|
||||||
|
ThemeUtil.getColorWithOpacity(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.root,
|
||||||
|
com.google.android.material.R.attr.colorSurface
|
||||||
|
),
|
||||||
|
ThemeUtil.SYSTEM_BAR_ALPHA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||||
|
InsetsHelper.GESTURE_NAVIGATION
|
||||||
|
) {
|
||||||
|
binding.navigationBarShade.setBackgroundColor(
|
||||||
|
ThemeUtil.getColorWithOpacity(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.root,
|
||||||
|
com.google.android.material.R.attr.colorSurface
|
||||||
|
),
|
||||||
|
ThemeUtil.SYSTEM_BAR_ALPHA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val navHostFragment =
|
||||||
|
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||||
|
setUpNavigation(navHostFragment.navController)
|
||||||
|
(binding.navigationView as NavigationBarView).setOnItemReselectedListener {
|
||||||
|
when (it.itemId) {
|
||||||
|
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
|
||||||
|
R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
|
||||||
|
R.id.homeSettingsFragment -> SettingsActivity.launch(
|
||||||
|
this,
|
||||||
|
SettingsFile.FILE_NAME_CONFIG,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevents navigation from being drawn for a short time on recreation if set to hidden
|
||||||
|
if (!homeViewModel.navigationVisible.value.first) {
|
||||||
|
binding.navigationView.visibility = View.INVISIBLE
|
||||||
|
binding.statusBarShade.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.navigationVisible.collect {
|
||||||
|
showNavigation(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.statusBarShadeVisible.collect {
|
||||||
|
showStatusBarShade(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.isPickingUserDir.collect { checkUserPermissions() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||||
|
EmulationActivity.tryDismissRunningNotification(this)
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
checkUserPermissions()
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
EmulationActivity.tryDismissRunningNotification(this)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkUserPermissions() {
|
||||||
|
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||||
|
|
||||||
|
if (!firstTimeSetup && !PermissionsHandler.hasWriteAccess(this) &&
|
||||||
|
!homeViewModel.isPickingUserDir.value
|
||||||
|
) {
|
||||||
|
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||||
|
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishSetup(navController: NavController) {
|
||||||
|
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
|
||||||
|
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpNavigation(navController: NavController) {
|
||||||
|
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||||
|
|
||||||
|
if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
|
||||||
|
navController.navigate(R.id.firstTimeSetupFragment)
|
||||||
|
homeViewModel.navigatedToSetup = true
|
||||||
|
} else {
|
||||||
|
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNavigation(visible: Boolean, animated: Boolean) {
|
||||||
|
if (!animated) {
|
||||||
|
if (visible) {
|
||||||
|
binding.navigationView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.navigationView.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val smallLayout = resources.getBoolean(R.bool.small_layout)
|
||||||
|
binding.navigationView.animate().apply {
|
||||||
|
if (visible) {
|
||||||
|
binding.navigationView.visibility = View.VISIBLE
|
||||||
|
duration = 300
|
||||||
|
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
|
||||||
|
|
||||||
|
if (smallLayout) {
|
||||||
|
binding.navigationView.translationY =
|
||||||
|
binding.navigationView.height.toFloat() * 2
|
||||||
|
translationY(0f)
|
||||||
|
} else {
|
||||||
|
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
|
||||||
|
ViewCompat.LAYOUT_DIRECTION_LTR
|
||||||
|
) {
|
||||||
|
binding.navigationView.translationX =
|
||||||
|
binding.navigationView.width.toFloat() * -2
|
||||||
|
translationX(0f)
|
||||||
|
} else {
|
||||||
|
binding.navigationView.translationX =
|
||||||
|
binding.navigationView.width.toFloat() * 2
|
||||||
|
translationX(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
duration = 300
|
||||||
|
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
|
||||||
|
|
||||||
|
if (smallLayout) {
|
||||||
|
translationY(binding.navigationView.height.toFloat() * 2)
|
||||||
|
} else {
|
||||||
|
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
|
||||||
|
ViewCompat.LAYOUT_DIRECTION_LTR
|
||||||
|
) {
|
||||||
|
translationX(binding.navigationView.width.toFloat() * -2)
|
||||||
|
} else {
|
||||||
|
translationX(binding.navigationView.width.toFloat() * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.withEndAction {
|
||||||
|
if (!visible) {
|
||||||
|
binding.navigationView.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showStatusBarShade(visible: Boolean) {
|
||||||
|
binding.statusBarShade.animate().apply {
|
||||||
|
if (visible) {
|
||||||
|
binding.statusBarShade.visibility = View.VISIBLE
|
||||||
|
binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
|
||||||
|
duration = 300
|
||||||
|
translationY(0f)
|
||||||
|
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
|
||||||
|
} else {
|
||||||
|
duration = 300
|
||||||
|
translationY(binding.navigationView.height.toFloat() * -2)
|
||||||
|
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
|
||||||
|
}
|
||||||
|
}.withEndAction {
|
||||||
|
if (!visible) {
|
||||||
|
binding.statusBarShade.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams
|
||||||
|
mlpStatusShade.height = insets.top
|
||||||
|
binding.statusBarShade.layoutParams = mlpStatusShade
|
||||||
|
|
||||||
|
// The only situation where we care to have a nav bar shade is when it's at the bottom
|
||||||
|
// of the screen where scrolling list elements can go behind it.
|
||||||
|
val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
|
||||||
|
mlpNavShade.height = insets.bottom
|
||||||
|
binding.navigationBarShade.layoutParams = mlpNavShade
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
val openCitraDirectory = registerForActivityResult<Uri, Uri>(
|
||||||
|
ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { result: Uri? ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ciaFileInstaller = registerForActivityResult(
|
||||||
|
OpenFileResultContract()
|
||||||
|
) { result: Intent? ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedFiles =
|
||||||
|
FileBrowserHelper.getSelectedFiles(result, applicationContext, listOf("cia"))
|
||||||
|
if (selectedFiles == null) {
|
||||||
|
Toast.makeText(applicationContext, R.string.cia_file_not_found, Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val workManager = WorkManager.getInstance(applicationContext)
|
||||||
|
workManager.enqueueUniqueWork(
|
||||||
|
"installCiaWork", ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||||
|
OneTimeWorkRequest.Builder(CiaInstallWorker::class.java)
|
||||||
|
.setInputData(
|
||||||
|
Data.Builder().putStringArray("CIA_FILES", selectedFiles)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,92 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.main;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.BuildConfig;
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.settings.model.Settings;
|
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
|
||||||
|
|
||||||
public final class MainPresenter {
|
|
||||||
public static final int REQUEST_ADD_DIRECTORY = 1;
|
|
||||||
public static final int REQUEST_INSTALL_CIA = 2;
|
|
||||||
public static final int REQUEST_SELECT_CITRA_DIRECTORY = 3;
|
|
||||||
|
|
||||||
private final MainView mView;
|
|
||||||
private String mDirToAdd;
|
|
||||||
private long mLastClickTime = 0;
|
|
||||||
|
|
||||||
public MainPresenter(MainView view) {
|
|
||||||
mView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate() {
|
|
||||||
String versionName = BuildConfig.VERSION_NAME;
|
|
||||||
mView.setVersionString(versionName);
|
|
||||||
refreshGameList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchFileListActivity(int request) {
|
|
||||||
if (mView != null) {
|
|
||||||
mView.launchFileListActivity(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean handleOptionSelection(int itemId) {
|
|
||||||
// Double-click prevention, using threshold of 500 ms
|
|
||||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 500) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mLastClickTime = SystemClock.elapsedRealtime();
|
|
||||||
|
|
||||||
switch (itemId) {
|
|
||||||
case R.id.menu_settings_core:
|
|
||||||
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.button_select_root:
|
|
||||||
mView.launchFileListActivity(REQUEST_SELECT_CITRA_DIRECTORY);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.button_add_directory:
|
|
||||||
launchFileListActivity(REQUEST_ADD_DIRECTORY);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.button_install_cia:
|
|
||||||
launchFileListActivity(REQUEST_INSTALL_CIA);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.button_premium:
|
|
||||||
mView.launchSettingsActivity(Settings.SECTION_PREMIUM);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDirIfNeeded(AddDirectoryHelper helper) {
|
|
||||||
if (mDirToAdd != null) {
|
|
||||||
helper.addDirectory(mDirToAdd, mView::refresh);
|
|
||||||
|
|
||||||
mDirToAdd = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDirectorySelected(String dir) {
|
|
||||||
mDirToAdd = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshGameList() {
|
|
||||||
Context context = CitraApplication.getAppContext();
|
|
||||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
|
||||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
|
||||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
|
||||||
mView.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.main;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstraction for the screen that shows on application launch.
|
|
||||||
* Implementations will differ primarily to target touch-screen
|
|
||||||
* or non-touch screen devices.
|
|
||||||
*/
|
|
||||||
public interface MainView {
|
|
||||||
/**
|
|
||||||
* Pass the view the native library's version string. Displaying
|
|
||||||
* it is optional.
|
|
||||||
*
|
|
||||||
* @param version A string pulled from native code.
|
|
||||||
*/
|
|
||||||
void setVersionString(String version);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tell the view to refresh its contents.
|
|
||||||
*/
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
void launchSettingsActivity(String menuTag);
|
|
||||||
|
|
||||||
void launchFileListActivity(int request);
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.platform;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.core.graphics.Insets;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import com.google.android.material.color.MaterialColors;
|
|
||||||
import com.google.android.material.divider.MaterialDividerItemDecoration;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.adapters.GameAdapter;
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
|
|
||||||
public final class PlatformGamesFragment extends Fragment implements PlatformGamesView {
|
|
||||||
private final PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this);
|
|
||||||
|
|
||||||
private GameAdapter mAdapter;
|
|
||||||
private RecyclerView mRecyclerView;
|
|
||||||
private TextView mTextView;
|
|
||||||
private SwipeRefreshLayout mPullToRefresh;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_grid, container, false);
|
|
||||||
|
|
||||||
findViews(rootView);
|
|
||||||
|
|
||||||
mPresenter.onCreateView();
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
private void onPullToRefresh() {
|
|
||||||
Runnable onPostRunnable = () -> {
|
|
||||||
updateTextView();
|
|
||||||
mPullToRefresh.setRefreshing(false);
|
|
||||||
};
|
|
||||||
Runnable scanLibraryRunnable = () -> {
|
|
||||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
|
||||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
|
||||||
mPresenter.refresh();
|
|
||||||
mHandler.post(onPostRunnable);
|
|
||||||
};
|
|
||||||
|
|
||||||
mExecutor.execute(scanLibraryRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
|
||||||
int columns = getResources().getInteger(R.integer.game_grid_columns);
|
|
||||||
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
|
|
||||||
mAdapter = new GameAdapter();
|
|
||||||
|
|
||||||
mRecyclerView.setLayoutManager(layoutManager);
|
|
||||||
mRecyclerView.setAdapter(mAdapter);
|
|
||||||
MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL);
|
|
||||||
divider.setLastItemDecorated(false);
|
|
||||||
mRecyclerView.addItemDecoration(divider);
|
|
||||||
|
|
||||||
// Add swipe down to refresh gesture
|
|
||||||
mPullToRefresh.setOnRefreshListener(this::onPullToRefresh);
|
|
||||||
mPullToRefresh.setProgressBackgroundColorSchemeColor(MaterialColors.getColor(mPullToRefresh, R.attr.colorPrimary));
|
|
||||||
mPullToRefresh.setColorSchemeColors(MaterialColors.getColor(mPullToRefresh, R.attr.colorOnPrimary));
|
|
||||||
|
|
||||||
setInsets();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void refresh() {
|
|
||||||
mPresenter.refresh();
|
|
||||||
updateTextView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showGames(Cursor games) {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter.swapCursor(games);
|
|
||||||
}
|
|
||||||
updateTextView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTextView() {
|
|
||||||
mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void findViews(View root) {
|
|
||||||
mRecyclerView = root.findViewById(R.id.grid_games);
|
|
||||||
mTextView = root.findViewById(R.id.gamelist_empty_text);
|
|
||||||
mPullToRefresh = root.findViewById(R.id.refresh_grid_games);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
|
||||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
||||||
v.setPadding(0, 0, 0, insets.bottom);
|
|
||||||
return windowInsets;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.platform;
|
|
||||||
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
import org.citra.citra_emu.utils.Log;
|
|
||||||
|
|
||||||
import rx.android.schedulers.AndroidSchedulers;
|
|
||||||
import rx.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public final class PlatformGamesPresenter {
|
|
||||||
private final PlatformGamesView mView;
|
|
||||||
|
|
||||||
public PlatformGamesPresenter(PlatformGamesView view) {
|
|
||||||
mView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreateView() {
|
|
||||||
loadGames();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
Log.debug("[PlatformGamesPresenter] : Refreshing...");
|
|
||||||
loadGames();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadGames() {
|
|
||||||
Log.debug("[PlatformGamesPresenter] : Loading games...");
|
|
||||||
|
|
||||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
|
||||||
|
|
||||||
databaseHelper.getGames()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(games ->
|
|
||||||
{
|
|
||||||
Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...");
|
|
||||||
|
|
||||||
mView.showGames(games);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package org.citra.citra_emu.ui.platform;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstraction for a screen representing a single platform's games.
|
|
||||||
*/
|
|
||||||
public interface PlatformGamesView {
|
|
||||||
/**
|
|
||||||
* Tell the view to refresh its contents.
|
|
||||||
*/
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be called when an asynchronous database read completes. Passes the
|
|
||||||
* result, in this case a {@link Cursor}, to the view.
|
|
||||||
*
|
|
||||||
* @param games A Cursor containing the games read from the database.
|
|
||||||
*/
|
|
||||||
void showGames(Cursor games);
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.content.AsyncQueryHandler;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
|
||||||
import org.citra.citra_emu.model.GameProvider;
|
|
||||||
|
|
||||||
public class AddDirectoryHelper {
|
|
||||||
private Context mContext;
|
|
||||||
|
|
||||||
public AddDirectoryHelper(Context context) {
|
|
||||||
this.mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) {
|
|
||||||
AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
|
|
||||||
@Override
|
|
||||||
protected void onInsertComplete(int token, Object cookie, Uri uri) {
|
|
||||||
addDirectoryListener.onDirectoryAdded();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ContentValues file = new ContentValues();
|
|
||||||
file.put(GameDatabase.KEY_FOLDER_PATH, dir);
|
|
||||||
|
|
||||||
handler.startInsert(0, // We don't need to identify this call to the handler
|
|
||||||
null, // We don't need to pass additional data to the handler
|
|
||||||
GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
|
|
||||||
file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AddDirectoryListener {
|
|
||||||
void onDirectoryAdded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,215 +0,0 @@
|
|||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
|
||||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
|
||||||
import com.android.billingclient.api.BillingClient;
|
|
||||||
import com.android.billingclient.api.BillingClientStateListener;
|
|
||||||
import com.android.billingclient.api.BillingFlowParams;
|
|
||||||
import com.android.billingclient.api.BillingResult;
|
|
||||||
import com.android.billingclient.api.Purchase;
|
|
||||||
import com.android.billingclient.api.Purchase.PurchasesResult;
|
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
|
||||||
import com.android.billingclient.api.SkuDetails;
|
|
||||||
import com.android.billingclient.api.SkuDetailsParams;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
|
||||||
import org.citra.citra_emu.ui.main.MainActivity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class BillingManager implements PurchasesUpdatedListener {
|
|
||||||
private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium";
|
|
||||||
|
|
||||||
private final Activity mActivity;
|
|
||||||
private BillingClient mBillingClient;
|
|
||||||
private SkuDetails mSkuPremium;
|
|
||||||
private boolean mIsPremiumActive = false;
|
|
||||||
private boolean mIsServiceConnected = false;
|
|
||||||
private Runnable mUpdateBillingCallback;
|
|
||||||
|
|
||||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
|
||||||
|
|
||||||
public BillingManager(Activity activity) {
|
|
||||||
mActivity = activity;
|
|
||||||
mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();
|
|
||||||
querySkuDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
static public boolean isPremiumCached() {
|
|
||||||
return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if Premium subscription is currently active
|
|
||||||
*/
|
|
||||||
public boolean isPremiumActive() {
|
|
||||||
return mIsPremiumActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes the billing flow for Premium
|
|
||||||
*
|
|
||||||
* @param callback Optional callback, called once, on completion of billing
|
|
||||||
*/
|
|
||||||
public void invokePremiumBilling(Runnable callback) {
|
|
||||||
if (mSkuPremium == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional callback to refresh the UI for the caller when billing completes
|
|
||||||
mUpdateBillingCallback = callback;
|
|
||||||
|
|
||||||
// Invoke the billing flow
|
|
||||||
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
|
|
||||||
.setSkuDetails(mSkuPremium)
|
|
||||||
.build();
|
|
||||||
mBillingClient.launchBillingFlow(mActivity, flowParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePremiumState(boolean isPremiumActive) {
|
|
||||||
mIsPremiumActive = isPremiumActive;
|
|
||||||
|
|
||||||
// Cache state for synchronous UI
|
|
||||||
SharedPreferences.Editor editor = mPreferences.edit();
|
|
||||||
editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive);
|
|
||||||
editor.apply();
|
|
||||||
|
|
||||||
// No need to show button in action bar if Premium is active
|
|
||||||
MainActivity.setPremiumButtonVisible(!isPremiumActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) {
|
|
||||||
if (purchaseList == null || purchaseList.isEmpty()) {
|
|
||||||
// Premium is not active, or billing is unavailable
|
|
||||||
updatePremiumState(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Purchase premiumPurchase = null;
|
|
||||||
for (Purchase purchase : purchaseList) {
|
|
||||||
if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) {
|
|
||||||
premiumPurchase = purchase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
|
|
||||||
// Premium has been purchased
|
|
||||||
updatePremiumState(true);
|
|
||||||
|
|
||||||
// Acknowledge the purchase if it hasn't already been acknowledged.
|
|
||||||
if (!premiumPurchase.isAcknowledged()) {
|
|
||||||
AcknowledgePurchaseParams acknowledgePurchaseParams =
|
|
||||||
AcknowledgePurchaseParams.newBuilder()
|
|
||||||
.setPurchaseToken(premiumPurchase.getPurchaseToken())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> {
|
|
||||||
Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show();
|
|
||||||
};
|
|
||||||
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mUpdateBillingCallback != null) {
|
|
||||||
try {
|
|
||||||
mUpdateBillingCallback.run();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
mUpdateBillingCallback = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) {
|
|
||||||
if (skuDetailsList == null) {
|
|
||||||
// This can happen when no user is signed in
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skuDetailsList.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mSkuPremium = skuDetailsList.get(0);
|
|
||||||
|
|
||||||
queryPurchases();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void querySkuDetails() {
|
|
||||||
Runnable queryToExecute = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
|
||||||
List<String> skuList = new ArrayList<>();
|
|
||||||
|
|
||||||
skuList.add(BILLING_SKU_PREMIUM);
|
|
||||||
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
|
|
||||||
|
|
||||||
mBillingClient.querySkuDetailsAsync(params.build(),
|
|
||||||
(billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
executeServiceRequest(queryToExecute);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onQueryPurchasesFinished(PurchasesResult result) {
|
|
||||||
// Have we been disposed of in the meantime? If so, or bad result code, then quit
|
|
||||||
if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
|
||||||
updatePremiumState(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update the UI and purchases inventory with new list of purchases
|
|
||||||
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void queryPurchases() {
|
|
||||||
Runnable queryToExecute = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
|
|
||||||
onQueryPurchasesFinished(purchasesResult);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
executeServiceRequest(queryToExecute);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startServiceConnection(final Runnable executeOnFinish) {
|
|
||||||
mBillingClient.startConnection(new BillingClientStateListener() {
|
|
||||||
@Override
|
|
||||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
|
||||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
||||||
mIsServiceConnected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executeOnFinish != null) {
|
|
||||||
executeOnFinish.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBillingServiceDisconnected() {
|
|
||||||
mIsServiceConnected = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeServiceRequest(Runnable runnable) {
|
|
||||||
if (mIsServiceConnected) {
|
|
||||||
runnable.run();
|
|
||||||
} else {
|
|
||||||
// If billing service was disconnected, we try to reconnect 1 time.
|
|
||||||
startServiceConnection(runnable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,6 +5,7 @@ import android.app.NotificationManager;
|
|||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -13,6 +14,7 @@ import androidx.work.ForegroundInfo;
|
|||||||
import androidx.work.Worker;
|
import androidx.work.Worker;
|
||||||
import androidx.work.WorkerParameters;
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.NativeLibrary.InstallStatus;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
|
|
||||||
public class CiaInstallWorker extends Worker {
|
public class CiaInstallWorker extends Worker {
|
||||||
@ -56,15 +58,6 @@ public class CiaInstallWorker extends Worker {
|
|||||||
super(context, params);
|
super(context, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InstallStatus {
|
|
||||||
Success,
|
|
||||||
ErrorFailedToOpenFile,
|
|
||||||
ErrorFileNotFound,
|
|
||||||
ErrorAborted,
|
|
||||||
ErrorInvalid,
|
|
||||||
ErrorEncrypted,
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyInstallStatus(String filename, InstallStatus status) {
|
private void notifyInstallStatus(String filename, InstallStatus status) {
|
||||||
switch(status){
|
switch(status){
|
||||||
case Success:
|
case Success:
|
||||||
@ -126,10 +119,10 @@ public class CiaInstallWorker extends Worker {
|
|||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (String file : selectedFiles) {
|
for (String file : selectedFiles) {
|
||||||
String filename = FileUtil.getFilename(mContext, file);
|
String filename = FileUtil.getFilename(Uri.parse(file));
|
||||||
mInstallProgressBuilder.setContentText(mContext.getString(
|
mInstallProgressBuilder.setContentText(mContext.getString(
|
||||||
R.string.cia_install_notification_installing, filename, ++i, selectedFiles.length));
|
R.string.cia_install_notification_installing, filename, ++i, selectedFiles.length));
|
||||||
InstallStatus res = InstallCIA(file);
|
InstallStatus res = installCIA(file);
|
||||||
notifyInstallStatus(filename, res);
|
notifyInstallStatus(filename, res);
|
||||||
}
|
}
|
||||||
mNotificationManager.cancel(PROGRESS_NOTIFICATION_ID);
|
mNotificationManager.cancel(PROGRESS_NOTIFICATION_ID);
|
||||||
@ -156,5 +149,5 @@ public class CiaInstallWorker extends Worker {
|
|||||||
return new ForegroundInfo(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
|
return new ForegroundInfo(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private native InstallStatus InstallCIA(String path);
|
private native InstallStatus installCIA(String path);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import org.citra.citra_emu.dialogs.CitraDirectoryDialog;
|
|
||||||
import org.citra.citra_emu.dialogs.CopyDirProgressDialog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Citra directory initialization ui flow controller.
|
|
||||||
*/
|
|
||||||
public class CitraDirectoryHelper {
|
|
||||||
public interface Listener {
|
|
||||||
void onDirectoryInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final FragmentActivity mFragmentActivity;
|
|
||||||
private final Listener mListener;
|
|
||||||
|
|
||||||
public CitraDirectoryHelper(FragmentActivity mFragmentActivity, Listener mListener) {
|
|
||||||
this.mFragmentActivity = mFragmentActivity;
|
|
||||||
this.mListener = mListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showCitraDirectoryDialog(Uri result) {
|
|
||||||
CitraDirectoryDialog citraDirectoryDialog = CitraDirectoryDialog.newInstance(
|
|
||||||
result.toString(), ((moveData, path) -> {
|
|
||||||
Uri previous = PermissionsHandler.getCitraDirectory();
|
|
||||||
// Do noting if user select the previous path.
|
|
||||||
if (path.equals(previous)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
mFragmentActivity.getContentResolver().takePersistableUriPermission(path,
|
|
||||||
takeFlags);
|
|
||||||
if (!moveData || previous == null) {
|
|
||||||
initializeCitraDirectory(path);
|
|
||||||
mListener.onDirectoryInitialized();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user check move data, show copy progress dialog.
|
|
||||||
showCopyDialog(previous, path);
|
|
||||||
}));
|
|
||||||
citraDirectoryDialog.show(mFragmentActivity.getSupportFragmentManager(),
|
|
||||||
CitraDirectoryDialog.TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showCopyDialog(Uri previous, Uri path) {
|
|
||||||
CopyDirProgressDialog copyDirProgressDialog = new CopyDirProgressDialog();
|
|
||||||
copyDirProgressDialog.showNow(mFragmentActivity.getSupportFragmentManager(),
|
|
||||||
CopyDirProgressDialog.TAG);
|
|
||||||
|
|
||||||
// Run copy dir in background
|
|
||||||
Executors.newSingleThreadExecutor().execute(() -> {
|
|
||||||
FileUtil.copyDir(
|
|
||||||
mFragmentActivity, previous.toString(), path.toString(),
|
|
||||||
new FileUtil.CopyDirListener() {
|
|
||||||
@Override
|
|
||||||
public void onSearchProgress(String directoryName) {
|
|
||||||
copyDirProgressDialog.onUpdateSearchProgress(directoryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCopyProgress(String filename, int progress, int max) {
|
|
||||||
copyDirProgressDialog.onUpdateCopyProgress(filename, progress, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
initializeCitraDirectory(path);
|
|
||||||
copyDirProgressDialog.dismissAllowingStateLoss();
|
|
||||||
mListener.onDirectoryInitialized();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeCitraDirectory(Uri path) {
|
|
||||||
if (!PermissionsHandler.setCitraDirectory(path.toString()))
|
|
||||||
return;
|
|
||||||
DirectoryInitialization.resetCitraDirectoryState();
|
|
||||||
DirectoryInitialization.start(mFragmentActivity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.utils
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.citra.citra_emu.fragments.CitraDirectoryDialogFragment
|
||||||
|
import org.citra.citra_emu.fragments.CopyDirProgressDialog
|
||||||
|
import org.citra.citra_emu.model.SetupCallback
|
||||||
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Citra directory initialization ui flow controller.
|
||||||
|
*/
|
||||||
|
class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) {
|
||||||
|
fun showCitraDirectoryDialog(result: Uri, callback: SetupCallback? = null) {
|
||||||
|
val citraDirectoryDialog = CitraDirectoryDialogFragment.newInstance(
|
||||||
|
fragmentActivity,
|
||||||
|
result.toString(),
|
||||||
|
CitraDirectoryDialogFragment.Listener { moveData: Boolean, path: Uri ->
|
||||||
|
val previous = PermissionsHandler.citraDirectory
|
||||||
|
// Do noting if user select the previous path.
|
||||||
|
if (path == previous) {
|
||||||
|
return@Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
val takeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
fragmentActivity.contentResolver.takePersistableUriPermission(
|
||||||
|
path,
|
||||||
|
takeFlags
|
||||||
|
)
|
||||||
|
if (!moveData || previous.toString().isEmpty()) {
|
||||||
|
initializeCitraDirectory(path)
|
||||||
|
callback?.onStepCompleted()
|
||||||
|
val viewModel = ViewModelProvider(fragmentActivity)[HomeViewModel::class.java]
|
||||||
|
viewModel.setUserDir(fragmentActivity, path.path!!)
|
||||||
|
viewModel.setPickingUserDir(false)
|
||||||
|
return@Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user check move data, show copy progress dialog.
|
||||||
|
CopyDirProgressDialog.newInstance(fragmentActivity, previous, path, callback)
|
||||||
|
?.show(fragmentActivity.supportFragmentManager, CopyDirProgressDialog.TAG)
|
||||||
|
})
|
||||||
|
citraDirectoryDialog.show(
|
||||||
|
fragmentActivity.supportFragmentManager,
|
||||||
|
CitraDirectoryDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun initializeCitraDirectory(path: Uri) {
|
||||||
|
PermissionsHandler.setCitraDirectory(path.toString())
|
||||||
|
DirectoryInitialization.resetCitraDirectoryState()
|
||||||
|
DirectoryInitialization.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,189 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2014 Dolphin Emulator Project
|
|
||||||
* Licensed under GPLv2+
|
|
||||||
* Refer to the license.txt file included.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service that spawns its own thread in order to copy several binary and shader files
|
|
||||||
* from the Citra APK to the external file system.
|
|
||||||
*/
|
|
||||||
public final class DirectoryInitialization {
|
|
||||||
public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST";
|
|
||||||
|
|
||||||
public static final String EXTRA_STATE = "directoryState";
|
|
||||||
private static volatile DirectoryInitializationState directoryState = null;
|
|
||||||
private static String userPath;
|
|
||||||
private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
public static void start(Context context) {
|
|
||||||
// Can take a few seconds to run, so don't block UI thread.
|
|
||||||
//noinspection TrivialFunctionalExpressionUsage
|
|
||||||
((Runnable) () -> init(context)).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void init(Context context) {
|
|
||||||
if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
|
||||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
|
||||||
if (setCitraUserDirectory()) {
|
|
||||||
initializeInternalStorage(context);
|
|
||||||
CitraApplication.documentsTree.setRoot(Uri.parse(userPath));
|
|
||||||
NativeLibrary.CreateLogFile();
|
|
||||||
NativeLibrary.LogUserDirectory(userPath);
|
|
||||||
NativeLibrary.CreateConfigFile();
|
|
||||||
directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
|
||||||
} else {
|
|
||||||
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isCitraDirectoryInitializationRunning.set(false);
|
|
||||||
sendBroadcastState(directoryState, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void deleteDirectoryRecursively(File file) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
for (File child : file.listFiles())
|
|
||||||
deleteDirectoryRecursively(child);
|
|
||||||
}
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean areCitraDirectoriesReady() {
|
|
||||||
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void resetCitraDirectoryState() {
|
|
||||||
directoryState = null;
|
|
||||||
isCitraDirectoryInitializationRunning.compareAndSet(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUserDirectory() {
|
|
||||||
if (directoryState == null) {
|
|
||||||
throw new IllegalStateException("DirectoryInitialization has to run at least once!");
|
|
||||||
} else if (isCitraDirectoryInitializationRunning.get()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"DirectoryInitialization has to finish running first!");
|
|
||||||
}
|
|
||||||
return userPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void SetSysDirectory(String path);
|
|
||||||
|
|
||||||
private static boolean setCitraUserDirectory() {
|
|
||||||
Uri dataPath = PermissionsHandler.getCitraDirectory();
|
|
||||||
if (dataPath != null) {
|
|
||||||
userPath = dataPath.toString();
|
|
||||||
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void initializeInternalStorage(Context context) {
|
|
||||||
File sysDirectory = new File(context.getFilesDir(), "Sys");
|
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
String revision = NativeLibrary.GetGitRevision();
|
|
||||||
if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
|
|
||||||
// There is no extracted Sys directory, or there is a Sys directory from another
|
|
||||||
// version of Citra that might contain outdated files. Let's (re-)extract Sys.
|
|
||||||
deleteDirectoryRecursively(sysDirectory);
|
|
||||||
copyAssetFolder("Sys", sysDirectory, true, context);
|
|
||||||
|
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
|
||||||
editor.putString("sysDirectoryVersion", revision);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the native code know where the Sys directory is.
|
|
||||||
SetSysDirectory(sysDirectory.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
|
|
||||||
Intent localIntent =
|
|
||||||
new Intent(BROADCAST_ACTION)
|
|
||||||
.putExtra(EXTRA_STATE, state);
|
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
|
|
||||||
Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!output.exists() || overwrite) {
|
|
||||||
InputStream in = context.getAssets().open(asset);
|
|
||||||
OutputStream out = new FileOutputStream(output);
|
|
||||||
copyFile(in, out);
|
|
||||||
in.close();
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
|
|
||||||
Context context) {
|
|
||||||
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
|
|
||||||
outputFolder);
|
|
||||||
|
|
||||||
try {
|
|
||||||
boolean createdFolder = false;
|
|
||||||
for (String file : context.getAssets().list(assetFolder)) {
|
|
||||||
if (!createdFolder) {
|
|
||||||
outputFolder.mkdir();
|
|
||||||
createdFolder = true;
|
|
||||||
}
|
|
||||||
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
|
|
||||||
overwrite, context);
|
|
||||||
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
|
|
||||||
context);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyFile(InputStream in, OutputStream out) throws IOException {
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int read;
|
|
||||||
|
|
||||||
while ((read = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DirectoryInitializationState {
|
|
||||||
CITRA_DIRECTORIES_INITIALIZED,
|
|
||||||
EXTERNAL_STORAGE_PERMISSION_NEEDED,
|
|
||||||
CANT_FIND_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import org.citra.citra_emu.BuildConfig
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler.hasWriteAccess
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that spawns its own thread in order to copy several binary and shader files
|
||||||
|
* from the Citra APK to the external file system.
|
||||||
|
*/
|
||||||
|
object DirectoryInitialization {
|
||||||
|
private const val SYS_DIR_VERSION = "sysDirectoryVersion"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var directoryState: DirectoryInitializationState? = null
|
||||||
|
var userPath: String? = null
|
||||||
|
val internalUserPath
|
||||||
|
get() = CitraApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
|
||||||
|
private val isCitraDirectoryInitializationRunning = AtomicBoolean(false)
|
||||||
|
|
||||||
|
val context: Context get() = CitraApplication.appContext
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun start(): DirectoryInitializationState? {
|
||||||
|
if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
|
||||||
|
directoryState = if (hasWriteAccess(context)) {
|
||||||
|
if (setCitraUserDirectory()) {
|
||||||
|
CitraApplication.documentsTree.setRoot(Uri.parse(userPath))
|
||||||
|
NativeLibrary.createLogFile()
|
||||||
|
NativeLibrary.logUserDirectory(userPath.toString())
|
||||||
|
NativeLibrary.createConfigFile()
|
||||||
|
GpuDriverHelper.initializeDriverParameters()
|
||||||
|
DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED
|
||||||
|
} else {
|
||||||
|
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCitraDirectoryInitializationRunning.set(false)
|
||||||
|
return directoryState
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDirectoryRecursively(file: File) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
for (child in file.listFiles()!!) {
|
||||||
|
deleteDirectoryRecursively(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun areCitraDirectoriesReady(): Boolean {
|
||||||
|
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetCitraDirectoryState() {
|
||||||
|
directoryState = null
|
||||||
|
isCitraDirectoryInitializationRunning.compareAndSet(true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val userDirectory: String?
|
||||||
|
get() {
|
||||||
|
checkNotNull(directoryState) {
|
||||||
|
"DirectoryInitialization has to run at least once!"
|
||||||
|
}
|
||||||
|
check(!isCitraDirectoryInitializationRunning.get()) {
|
||||||
|
"DirectoryInitialization has to finish running first!"
|
||||||
|
}
|
||||||
|
return userPath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCitraUserDirectory(): Boolean {
|
||||||
|
val dataPath = PermissionsHandler.citraDirectory
|
||||||
|
if (dataPath.toString().isNotEmpty()) {
|
||||||
|
userPath = dataPath.toString()
|
||||||
|
Log.debug("[DirectoryInitialization] User Dir: $userPath")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyAsset(asset: String, output: File, overwrite: Boolean, context: Context) {
|
||||||
|
Log.verbose("[DirectoryInitialization] Copying File $asset to $output")
|
||||||
|
try {
|
||||||
|
if (!output.exists() || overwrite) {
|
||||||
|
val inputStream = context.assets.open(asset)
|
||||||
|
val outputStream = FileOutputStream(output)
|
||||||
|
copyFile(inputStream, outputStream)
|
||||||
|
inputStream.close()
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.error("[DirectoryInitialization] Failed to copy asset file: $asset" + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyAssetFolder(
|
||||||
|
assetFolder: String,
|
||||||
|
outputFolder: File,
|
||||||
|
overwrite: Boolean,
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
Log.verbose("[DirectoryInitialization] Copying Folder $assetFolder to $outputFolder")
|
||||||
|
try {
|
||||||
|
var createdFolder = false
|
||||||
|
for (file in context.assets.list(assetFolder)!!) {
|
||||||
|
if (!createdFolder) {
|
||||||
|
outputFolder.mkdir()
|
||||||
|
createdFolder = true
|
||||||
|
}
|
||||||
|
copyAssetFolder(
|
||||||
|
assetFolder + File.separator + file, File(outputFolder, file),
|
||||||
|
overwrite, context
|
||||||
|
)
|
||||||
|
copyAsset(
|
||||||
|
assetFolder + File.separator + file, File(outputFolder, file), overwrite,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.error(
|
||||||
|
"[DirectoryInitialization] Failed to copy asset folder: $assetFolder" +
|
||||||
|
e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DirectoryInitializationState {
|
||||||
|
CITRA_DIRECTORIES_INITIALIZED,
|
||||||
|
EXTERNAL_STORAGE_PERMISSION_NEEDED,
|
||||||
|
CANT_FIND_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +0,0 @@
|
|||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
|
|
||||||
|
|
||||||
public class DirectoryStateReceiver extends BroadcastReceiver {
|
|
||||||
Action1<DirectoryInitializationState> callback;
|
|
||||||
|
|
||||||
public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) {
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
DirectoryInitializationState state = (DirectoryInitializationState) intent
|
|
||||||
.getSerializableExtra(DirectoryInitialization.EXTRA_STATE);
|
|
||||||
callback.call(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,6 +12,7 @@ import android.view.View;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
@ -25,6 +26,7 @@ import org.citra.citra_emu.utils.Log;
|
|||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Keep
|
||||||
public class DiskShaderCacheProgress {
|
public class DiskShaderCacheProgress {
|
||||||
|
|
||||||
// Equivalent to VideoCore::LoadCallbackStage
|
// Equivalent to VideoCore::LoadCallbackStage
|
||||||
|
|||||||
@ -1,271 +0,0 @@
|
|||||||
package org.citra.citra_emu.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
|
||||||
import org.citra.citra_emu.model.CheapDocument;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.StringTokenizer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cached document tree for citra user directory.
|
|
||||||
* For every filepath which is not startsWith "content://" will need to use this class to traverse.
|
|
||||||
* For example:
|
|
||||||
* C++ citra log file directory will be /log/citra_log.txt.
|
|
||||||
* After DocumentsTree.resolvePath() it will become content URI.
|
|
||||||
*/
|
|
||||||
public class DocumentsTree {
|
|
||||||
private DocumentsNode root;
|
|
||||||
private final Context context;
|
|
||||||
public static final String DELIMITER = "/";
|
|
||||||
|
|
||||||
public DocumentsTree() {
|
|
||||||
context = CitraApplication.getAppContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRoot(Uri rootUri) {
|
|
||||||
root = null;
|
|
||||||
root = new DocumentsNode();
|
|
||||||
root.uri = rootUri;
|
|
||||||
root.isDirectory = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean createFile(String filepath, String name) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) return false;
|
|
||||||
if (!node.isDirectory) return false;
|
|
||||||
if (!node.loaded) structTree(node);
|
|
||||||
Uri mUri = node.uri;
|
|
||||||
try {
|
|
||||||
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
|
||||||
if (node.children.get(filename) != null) return true;
|
|
||||||
DocumentFile createdFile = FileUtil.createFile(context, mUri.toString(), name);
|
|
||||||
if (createdFile == null) return false;
|
|
||||||
DocumentsNode document = new DocumentsNode(createdFile, false);
|
|
||||||
document.parent = node;
|
|
||||||
node.children.put(document.key, document);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean createDir(String filepath, String name) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) return false;
|
|
||||||
if (!node.isDirectory) return false;
|
|
||||||
if (!node.loaded) structTree(node);
|
|
||||||
Uri mUri = node.uri;
|
|
||||||
try {
|
|
||||||
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
|
||||||
if (node.children.get(filename) != null) return true;
|
|
||||||
DocumentFile createdDirectory = FileUtil.createDir(context, mUri.toString(), name);
|
|
||||||
if (createdDirectory == null) return false;
|
|
||||||
DocumentsNode document = new DocumentsNode(createdDirectory, true);
|
|
||||||
document.parent = node;
|
|
||||||
node.children.put(document.key, document);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int openContentUri(String filepath, String openmode) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return FileUtil.openContentUri(context, node.uri.toString(), openmode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename(String filepath) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return node.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getFilesName(String filepath) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null || !node.isDirectory) {
|
|
||||||
return new String[0];
|
|
||||||
}
|
|
||||||
// If this directory have not been iterate struct it.
|
|
||||||
if (!node.loaded) structTree(node);
|
|
||||||
return node.children.keySet().toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getFileSize(String filepath) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null || node.isDirectory) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return FileUtil.getFileSize(context, node.uri.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDirectory(String filepath) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) return false;
|
|
||||||
return node.isDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean Exists(String filepath) {
|
|
||||||
return resolvePath(filepath) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
|
||||||
DocumentsNode sourceNode = resolvePath(sourcePath);
|
|
||||||
if (sourceNode == null) return false;
|
|
||||||
DocumentsNode destinationNode = resolvePath(destinationParentPath);
|
|
||||||
if (destinationNode == null) return false;
|
|
||||||
try {
|
|
||||||
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationNode.uri);
|
|
||||||
if (destinationParent == null) return false;
|
|
||||||
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
|
|
||||||
DocumentFile destination = destinationParent.createFile("application/octet-stream", filename);
|
|
||||||
if (destination == null) return false;
|
|
||||||
DocumentsNode document = new DocumentsNode();
|
|
||||||
document.uri = destination.getUri();
|
|
||||||
document.parent = destinationNode;
|
|
||||||
document.name = destination.getName();
|
|
||||||
document.isDirectory = destination.isDirectory();
|
|
||||||
document.loaded = true;
|
|
||||||
InputStream input = context.getContentResolver().openInputStream(sourceNode.uri);
|
|
||||||
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri(), "wt");
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = input.read(buffer)) != -1) {
|
|
||||||
output.write(buffer, 0, len);
|
|
||||||
}
|
|
||||||
input.close();
|
|
||||||
output.flush();
|
|
||||||
output.close();
|
|
||||||
destinationNode.children.put(document.key, document);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.error("[DocumentsTree]: Cannot copy file, error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean renameFile(String filepath, String destinationFilename) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) return false;
|
|
||||||
try {
|
|
||||||
Uri mUri = node.uri;
|
|
||||||
String filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD);
|
|
||||||
DocumentsContract.renameDocument(context.getContentResolver(), mUri, filename);
|
|
||||||
node.rename(filename);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean deleteDocument(String filepath) {
|
|
||||||
DocumentsNode node = resolvePath(filepath);
|
|
||||||
if (node == null) return false;
|
|
||||||
try {
|
|
||||||
Uri mUri = node.uri;
|
|
||||||
if (!DocumentsContract.deleteDocument(context.getContentResolver(), mUri)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (node.parent != null) {
|
|
||||||
node.parent.children.remove(node.key);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private DocumentsNode resolvePath(String filepath) {
|
|
||||||
if (root == null)
|
|
||||||
return null;
|
|
||||||
StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
|
|
||||||
DocumentsNode iterator = root;
|
|
||||||
while (tokens.hasMoreTokens()) {
|
|
||||||
String token = tokens.nextToken();
|
|
||||||
if (token.isEmpty()) continue;
|
|
||||||
iterator = find(iterator, token);
|
|
||||||
if (iterator == null) return null;
|
|
||||||
}
|
|
||||||
return iterator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private DocumentsNode find(DocumentsNode parent, String filename) {
|
|
||||||
if (parent.isDirectory && !parent.loaded) {
|
|
||||||
structTree(parent);
|
|
||||||
}
|
|
||||||
return parent.children.get(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct current level directory tree
|
|
||||||
*
|
|
||||||
* @param parent parent node of this level
|
|
||||||
*/
|
|
||||||
private void structTree(DocumentsNode parent) {
|
|
||||||
CheapDocument[] documents = FileUtil.listFiles(context, parent.uri);
|
|
||||||
for (CheapDocument document : documents) {
|
|
||||||
DocumentsNode node = new DocumentsNode(document);
|
|
||||||
node.parent = parent;
|
|
||||||
parent.children.put(node.key, node);
|
|
||||||
}
|
|
||||||
parent.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DocumentsNode {
|
|
||||||
private DocumentsNode parent;
|
|
||||||
private final Map<String, DocumentsNode> children = new HashMap<>();
|
|
||||||
private String key;
|
|
||||||
private String name;
|
|
||||||
private Uri uri;
|
|
||||||
private boolean loaded = false;
|
|
||||||
private boolean isDirectory = false;
|
|
||||||
|
|
||||||
private DocumentsNode() {}
|
|
||||||
|
|
||||||
private DocumentsNode(CheapDocument document) {
|
|
||||||
name = document.getFilename();
|
|
||||||
uri = document.getUri();
|
|
||||||
key = FileUtil.getFilenameWithExtensions(uri);
|
|
||||||
isDirectory = document.isDirectory();
|
|
||||||
loaded = !isDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DocumentsNode(DocumentFile document, boolean isCreateDir) {
|
|
||||||
name = document.getName();
|
|
||||||
uri = document.getUri();
|
|
||||||
key = FileUtil.getFilenameWithExtensions(uri);
|
|
||||||
isDirectory = isCreateDir;
|
|
||||||
loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rename(String key) {
|
|
||||||
if (parent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parent.children.remove(this.key);
|
|
||||||
this.name = key;
|
|
||||||
parent.children.put(key, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,275 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.utils
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.citra.citra_emu.CitraApplication
|
||||||
|
import org.citra.citra_emu.model.CheapDocument
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.util.StringTokenizer
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cached document tree for Citra user directory.
|
||||||
|
* For every filepath which is not startsWith "content://" will need to use this class to traverse.
|
||||||
|
* For example:
|
||||||
|
* C++ Citra log file directory will be /log/citra_log.txt.
|
||||||
|
* After DocumentsTree.resolvePath() it will become content URI.
|
||||||
|
*/
|
||||||
|
class DocumentsTree {
|
||||||
|
private var root: DocumentsNode? = null
|
||||||
|
private val context get() = CitraApplication.appContext
|
||||||
|
|
||||||
|
fun setRoot(rootUri: Uri?) {
|
||||||
|
root = null
|
||||||
|
root = DocumentsNode()
|
||||||
|
root!!.uri = rootUri
|
||||||
|
root!!.isDirectory = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun createFile(filepath: String, name: String): Boolean {
|
||||||
|
val node = resolvePath(filepath) ?: return false
|
||||||
|
if (!node.isDirectory) return false
|
||||||
|
if (!node.loaded) structTree(node)
|
||||||
|
try {
|
||||||
|
val filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD)
|
||||||
|
if (node.findChild(filename) != null) return true
|
||||||
|
val createdFile = FileUtil.createFile(node.uri.toString(), name) ?: return false
|
||||||
|
val document = DocumentsNode(createdFile, false)
|
||||||
|
document.parent = node
|
||||||
|
node.addChild(document)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("[DocumentsTree]: Cannot create file, error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun createDir(filepath: String, name: String): Boolean {
|
||||||
|
val node = resolvePath(filepath) ?: return false
|
||||||
|
if (!node.isDirectory) return false
|
||||||
|
if (!node.loaded) structTree(node)
|
||||||
|
try {
|
||||||
|
val filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD)
|
||||||
|
if (node.findChild(filename) != null) return true
|
||||||
|
val createdDirectory = FileUtil.createDir(node.uri.toString(), name) ?: return false
|
||||||
|
val document = DocumentsNode(createdDirectory, true)
|
||||||
|
document.parent = node
|
||||||
|
node.addChild(document)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("[DocumentsTree]: Cannot create file, error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun openContentUri(filepath: String, openMode: String): Int {
|
||||||
|
val node = resolvePath(filepath) ?: return -1
|
||||||
|
return FileUtil.openContentUri(node.uri.toString(), openMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getFilename(filepath: String): String {
|
||||||
|
val node = resolvePath(filepath) ?: return ""
|
||||||
|
return node.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getFilesName(filepath: String): Array<String?> {
|
||||||
|
val node = resolvePath(filepath)
|
||||||
|
if (node == null || !node.isDirectory) {
|
||||||
|
return arrayOfNulls(0)
|
||||||
|
}
|
||||||
|
// If this directory has not been iterated, struct it.
|
||||||
|
if (!node.loaded) structTree(node)
|
||||||
|
return node.getChildNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getFileSize(filepath: String): Long {
|
||||||
|
val node = resolvePath(filepath)
|
||||||
|
return if (node == null || node.isDirectory) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
FileUtil.getFileSize(node.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun isDirectory(filepath: String): Boolean {
|
||||||
|
val node = resolvePath(filepath) ?: return false
|
||||||
|
return node.isDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun exists(filepath: String): Boolean {
|
||||||
|
return resolvePath(filepath) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun copyFile(
|
||||||
|
sourcePath: String,
|
||||||
|
destinationParentPath: String,
|
||||||
|
destinationFilename: String
|
||||||
|
): Boolean {
|
||||||
|
val sourceNode = resolvePath(sourcePath) ?: return false
|
||||||
|
val destinationNode = resolvePath(destinationParentPath) ?: return false
|
||||||
|
try {
|
||||||
|
val destinationParent =
|
||||||
|
DocumentFile.fromTreeUri(context, destinationNode.uri!!) ?: return false
|
||||||
|
val filename = URLDecoder.decode(destinationFilename, "UTF-8")
|
||||||
|
val destination = destinationParent.createFile(
|
||||||
|
"application/octet-stream",
|
||||||
|
filename
|
||||||
|
) ?: return false
|
||||||
|
val document = DocumentsNode()
|
||||||
|
document.uri = destination.uri
|
||||||
|
document.parent = destinationNode
|
||||||
|
document.name = destination.name!!
|
||||||
|
document.isDirectory = destination.isDirectory
|
||||||
|
document.loaded = true
|
||||||
|
val input = context.contentResolver.openInputStream(sourceNode.uri!!)!!
|
||||||
|
val output = context.contentResolver.openOutputStream(destination.uri, "wt")!!
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var len: Int
|
||||||
|
while (input.read(buffer).also { len = it } != -1) {
|
||||||
|
output.write(buffer, 0, len)
|
||||||
|
}
|
||||||
|
input.close()
|
||||||
|
output.flush()
|
||||||
|
output.close()
|
||||||
|
destinationNode.addChild(document)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("[DocumentsTree]: Cannot copy file, error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun renameFile(filepath: String, destinationFilename: String?): Boolean {
|
||||||
|
val node = resolvePath(filepath) ?: return false
|
||||||
|
try {
|
||||||
|
val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD)
|
||||||
|
DocumentsContract.renameDocument(context.contentResolver, node.uri!!, filename)
|
||||||
|
node.rename(filename)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("[DocumentsTree]: Cannot rename file, error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteDocument(filepath: String): Boolean {
|
||||||
|
val node = resolvePath(filepath) ?: return false
|
||||||
|
try {
|
||||||
|
if (!DocumentsContract.deleteDocument(context.contentResolver, node.uri!!)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (node.parent != null) {
|
||||||
|
node.parent!!.removeChild(node)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("[DocumentsTree]: Cannot rename file, error: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun resolvePath(filepath: String): DocumentsNode? {
|
||||||
|
root ?: return null
|
||||||
|
val tokens = StringTokenizer(filepath, DELIMITER, false)
|
||||||
|
var iterator = root
|
||||||
|
while (tokens.hasMoreTokens()) {
|
||||||
|
val token = tokens.nextToken()
|
||||||
|
if (token.isEmpty()) continue
|
||||||
|
iterator = find(iterator!!, token)
|
||||||
|
if (iterator == null) return null
|
||||||
|
}
|
||||||
|
return iterator
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun find(parent: DocumentsNode, filename: String): DocumentsNode? {
|
||||||
|
if (parent.isDirectory && !parent.loaded) {
|
||||||
|
structTree(parent)
|
||||||
|
}
|
||||||
|
return parent.findChild(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct current level directory tree
|
||||||
|
*
|
||||||
|
* @param parent parent node of this level
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
private fun structTree(parent: DocumentsNode) {
|
||||||
|
val documents = FileUtil.listFiles(parent.uri!!)
|
||||||
|
for (document in documents) {
|
||||||
|
val node = DocumentsNode(document)
|
||||||
|
node.parent = parent
|
||||||
|
parent.addChild(node)
|
||||||
|
}
|
||||||
|
parent.loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DocumentsNode {
|
||||||
|
@get:Synchronized
|
||||||
|
@set:Synchronized
|
||||||
|
var parent: DocumentsNode? = null
|
||||||
|
val children: MutableMap<String?, DocumentsNode?> = ConcurrentHashMap()
|
||||||
|
lateinit var name: String
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
@set:Synchronized
|
||||||
|
var uri: Uri? = null
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
@set:Synchronized
|
||||||
|
var loaded = false
|
||||||
|
var isDirectory = false
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
constructor(document: CheapDocument) {
|
||||||
|
name = document.filename
|
||||||
|
uri = document.uri
|
||||||
|
isDirectory = document.isDirectory
|
||||||
|
loaded = !isDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(document: DocumentFile, isCreateDir: Boolean) {
|
||||||
|
name = document.name!!
|
||||||
|
uri = document.uri
|
||||||
|
isDirectory = isCreateDir
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun rename(name: String) {
|
||||||
|
parent ?: return
|
||||||
|
parent!!.removeChild(this)
|
||||||
|
this.name = name
|
||||||
|
parent!!.addChild(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addChild(node: DocumentsNode) {
|
||||||
|
children[node.name.lowercase()] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeChild(node: DocumentsNode) = children.remove(node.name.lowercase())
|
||||||
|
|
||||||
|
fun findChild(filename: String) = children[filename.lowercase()]
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getChildNames(): Array<String?> =
|
||||||
|
children.mapNotNull { it.value!!.name }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DELIMITER = "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user