Compare commits

...

80 Commits

Author SHA1 Message Date
94a46c4b82 citra_qt: Hide refresh rate dialog on global configure 2023-04-06 22:54:49 +03:00
ce115869c9 citra_qt: Add option to adjust screen refresh rate 2023-03-30 00:05:29 +03:00
b5d6f645bd Prepare frontend for multiple graphics APIs (#6347)
* externals: Update dynarmic

* settings: Introduce GraphicsAPI enum

* For now it's OpenGL only but will be expanded upon later

* citra_qt: Introduce backend agnostic context management

* Mostly a direct port from yuzu

* core: Simplify context acquire

* settings: Add option to create debug contexts

* renderer_opengl: Abstract initialization to Driver

* This commit also updates glad and adds some useful extensions which we will use in part 2

* Rasterizer construction is moved to the specific renderer instead of RendererBase.
  Software rendering has been disable to achieve this but will be brought back in the next commit.

* video_core: Remove Init/Shutdown methods from renderer

* The constructor and destructor can do the same job

* In addition move opengl function loading to Qt since SDL already does this. Also remove ErrorVideoCore which is never reached

* citra_qt: Decouple software renderer from opengl part 1

* citra: Decouple software renderer from opengl part 2

* android: Decouple software renderer from opengl part 3

* swrasterizer: Decouple software renderer from opengl part 4

* This commit simply enforces the renderer naming conventions in the software renderer

* video_core: Move RendererBase to VideoCore

* video_core: De-globalize screenshot state

* video_core: Pass system to the renderers

* video_core: Commonize shader uniform data

* video_core: Abstract backend agnostic rasterizer operations

* bootmanager: Remove references to OpenGL for macOS

OpenGL macOS headers definitions clash heavily with each other

* citra_qt: Proper title for api settings

* video_core: Reduce boost usage

* bootmanager: Fix hide mouse option

Remove event handlers from RenderWidget for events that are
already handled by the parent GRenderWindow.
Also enable mouse tracking on the RenderWidget.

* android: Remove software from graphics api list

* code: Address review comments

* citra: Port per-game settings read

* Having to update the default value for all backends is a pain so lets centralize it

* android: Rename to OpenGLES

---------

Co-authored-by: MerryMage <MerryMage@users.noreply.github.com>
Co-authored-by: Vitor Kiguchi <vitor-kiguchi@hotmail.com>
2023-03-27 14:29:17 +03:00
9ef42040af GameDatabase.java: Fix file exists check on SAF (#6374)
Doesn't really do much other than reduce log spam
2023-03-26 21:24:44 +03:00
d9d0fc63ec applet: Fix HLE applet pre-start lifecycle. (#6362) 2023-03-25 22:36:14 +01:00
44097c2a8d gl_shader_disk_cache: Avoid reopening files every time a shader need to be written. (#6344)
Hopefully reduces stutters with Android SAF
2023-03-25 23:35:17 +02:00
fdb7ab47ff Merge pull request #6364 from Steveice10/break_error
kernel: Set system error status on svcBreak.
2023-03-25 01:18:26 -07:00
5317c00c45 Merge pull request #6368 from GPUCode/msvc-dup
Check fd before using dup
2023-03-24 10:29:55 -05:00
c7f8bc5582 file_util: Check fd before using dup 2023-03-24 16:56:31 +02:00
54385a54eb citra_qt: Prevent OS sleep on Linux when a game is running (#6249) 2023-03-23 19:37:10 +01:00
343717e683 citra_android: Implement edge-to-edge (#6349) 2023-03-23 19:36:54 +01:00
8d563d37b4 citra_android: Storage Access Framework implementation (#6313) 2023-03-23 14:30:52 +01:00
Mai
8c12eb4905 Merge pull request #6360 from SachinVin/b
common\common_funcs.h:  use __builtin_trap for Crash()
2023-03-21 21:55:35 -04:00
f1c282775d kernel: Set system error status on svcBreak. 2023-03-21 14:19:42 -07:00
0c3fe272b6 citra_qt: Add enhancement options to per-game (#6308)
Co-authored-by: Tobias <thm.frey@gmail.com>
2023-03-21 22:12:13 +01:00
8434d30768 common\common_funcs.h: use __builtin_trap for Crash() 2023-03-21 22:34:06 +05:30
fbf53686c3 apt: Fix exiting to game list on application close. (#6353) 2023-03-21 17:07:49 +01:00
794d051f0c common\CMakeLists.txt: add missing arch.h (#6359) 2023-03-21 17:06:28 +01:00
3fb48716c5 CreateThread invalid processor ID return error instead of assert. (#6354) 2023-03-21 17:06:07 +01:00
359a1b3296 citra_android: Fix input shifting in emulation activity (#6352) 2023-03-21 17:05:42 +01:00
a2fd43deab Revert "citra_android: Use androidx splash screen (#6355)" (#6357)
This reverts commit 27c280534d.
2023-03-17 10:34:00 +02:00
27c280534d citra_android: Use androidx splash screen (#6355) 2023-03-16 08:30:47 +02:00
e18e30a8cc citra_android: Enable themed icon (#6351) 2023-03-15 09:21:54 +02:00
6fbc54b0c5 citra-qt: fixes to per game settings (#6298)
* citra-qt config: small misc changes

Remove unused ReadSettingGlobal

Remove unused WriteSetting overload

ReadGlobalSetting: rename default value variable

* qt config: fix u16 values being written as QMetaType

* qt config: rework post processing shader setting

handles post processing setting properly when per-game settings are used.
the anaglyph shader is given its own setting, separate from the post
processing name.

* qt config: use u32 instead of unsigned int when casting
2023-03-13 23:02:07 +02:00
49acfe428a citra_android: Bundle speex by default (#6348)
Without this, builds will fail on linux
2023-03-13 22:46:22 +02:00
aa8df317af citra_android: fix DiskShaderCacheProgress crash (#6346) 2023-03-13 11:16:38 +05:30
2cbf6fbb17 qt: Fix keys tutorial link in update downloader. (#6343) 2023-03-12 21:28:04 +05:30
27be16ee31 Update AndroidManifest for Android TV (#6330) 2023-03-10 00:44:45 +01:00
c96f54f022 Implement app management support (suspend, resume, close, etc) (#6322) 2023-03-10 00:44:26 +01:00
d2caf2d386 citra_android: Start material 3 migration (#6335) 2023-03-09 23:22:11 +01:00
1cca713e3b citra_android: New settings fragment animations (#6332) 2023-03-09 21:54:32 +05:30
976995ba08 cfg: Set system setup complete flag when formatting save. (#6331) 2023-03-09 00:05:00 +01:00
07e02a1acf Port multiplayer related PRs from yuzu (yuzu-emu/yuzu#9661 and yuzu-emu/yuzu#9713) (#6319)
Co-authored-by: SoRadGaming <sohorhab.azizdel@outlook.com>
Co-authored-by: Luke Sawczak <luke@unfamiliarplace.com>
2023-03-08 00:51:46 +01:00
455a0198d9 ci: Bump macOS target to 11 (Big Sur) (#6325) 2023-03-03 15:04:31 +02:00
8f2a5374c3 ci: Build macOS architectures separately and combine (#6321)
* ci: Build macOS for different architectures separately.

* ci: Combine macOS builds into universal binary.

* ci: Disable uploading final macOS artifacts until ready to resume producing.
2023-03-01 19:58:09 +02:00
c961ecb9a4 jni\native.cpp: Log g_build_fullname on Android (#6318) 2023-02-28 21:59:30 +05:30
c6f9fd3b65 qt: Remove status bar 3D controls due to issues. (#6317)
The 3D toggle does not behave correctly as it does not have some
special logic from the enhancements configuration UI that determines
the post-processing shader defaults to use. Because of that, plus
an uptick in people seemingly accidentally enabling 3D options and
not being sure why Citra is rendering differently, just remove the
new UI components for now until better ideas for 3D control can
be worked out.
2023-02-28 14:10:14 +02:00
3c15398f9e apt: Implement additional applet state management. (#6303)
* apt: Implement additional library applet state management.

* kernel: Clear process handle table on exit.

* apt: Implement system applet commands.

* apt: Pop MediaType from command buffers with correct size.

* apt: Improve accuracy of parameters and HLE applet lifecycle.

* apt: General cleanup.

* file_sys: Make system save data open error code more correct.

Not sure if this is the exact right error code, but it's at least
more correct than before as Game Notes will now create its system
save data instead of throwing a fatal error.

* apt: Fix launching New 3DS Internet Browser.

* frd: Correct fix to GetMyScreenName response.
2023-02-28 14:09:54 +02:00
8b116aaa04 externals: Fix mismatched CryptoPP definitions between compile time and header use. (#6314) 2023-02-25 12:58:38 +02:00
cc5ea21f1c citra_qt: Write to config file on important config changes (#6311)
Qt isn't always writing changes on save. This causes config to be lost on crash. This PR ensures all changes are always saved on the file.

Ported from yuzu.

Co-authored-by: Narr the Reg <5944268+german77@users.noreply.github.com>
2023-02-25 12:57:59 +02:00
286f750c6c citra_qt: Move CPU speed slider to debug tab and Report Comptaibility to help menu (#6250) 2023-02-18 23:24:15 +01:00
d8c9335ef0 Resolve Black Screen on Intel GPU Regression (#6306)
* Get value for swap screen setting and check mono_render_option again

* resolve clang-format issue

* do not disable opengl blending since it is enabled by default

* reset blending state to default values after drawing second screen

* prevent resetting state blending when custom opacity is not used
2023-02-18 18:54:12 +02:00
cda358443f nim: Fully stub nim:u service. (#6290) 2023-02-17 19:30:47 +01:00
bf73cb57ca am: Return installed titles in GetNumTickets and GetTicketList stubs. (#6292) 2023-02-17 16:20:56 +02:00
9eb1cd2875 Added an option to set the proportion of the screens when using layout "Large Screen Small Screen", to allow the user to define how much bigger the large screen should be with respect to the smaller screen. Currently the value must be between 1 and 16, but I could set a different maximum value if that would be desired. Thank you very much! (#6252) 2023-02-17 16:19:52 +02:00
bb8dde8480 aes: Fix derivation of slot 0x25 key X from NATIVE_FIRM. (#6283) 2023-02-16 15:35:17 +02:00
5aa80873e2 qt: Enable application options for system applications. (#6286) 2023-02-15 21:24:54 +01:00
5215468ff6 core\file_sys\archive_sdmc.cpp: Log error message if file failed to open. (#6284) 2023-02-14 22:19:45 +01:00
68162c29b4 cfg: Initialize backlight controls config blocks. (#6291) 2023-02-14 22:10:07 +01:00
a8e4e11cd5 Better Support for Picture-in-Picture Custom Layouts (Based on #6127) (#6247) 2023-02-14 22:06:11 +01:00
ab8d1c7d8b github: Improve Readme and add better issue templates (#6276)
* README: Update with latest information and better design

* .github: Add better issue templates

* Readme: Address review comments
2023-02-13 14:51:09 +01:00
1ab9b60a60 Services/APT: Implement PrepareToStartApplication, StartApplication, and WakeupApplication (#6280)
* Services/APT: Implemented PrepareToStartApplication and StartApplication.

This allows games to be launched from the Home Menu, however, there is still a bug with the GSP where the Home Menu doesn't release the GPU rights. It is unknown if the Home Menu should terminate itself after launching a new application.

To get the Home Menu to not hang when launching stuff, you need to have config block 0xF0006 (size 40 flags 8) in your config savegame, it doesn't matter if it's filled with zeros.

* Services/APT: Implement WakeupApplication.

With this, the Home Menu is now able to launch games when using an LLE NIM imlementation.

* Services/APT: Reset the app_start_parameters after launching the application with StartApplication.

* Services/APT: Simplify the StartApplication code by directly calling WakeupApplication.

---------

Co-authored-by: Subv <subv2112@gmail.com>
2023-02-12 08:47:08 +02:00
c2903a6b9d citra_qt: Hide updates on per-game config (#6296) 2023-02-12 08:45:43 +02:00
849d795f0e Port yuzu-emu/yuzu#8367: "Logging: Report Post Windows 10 2004 versions, like Windows 11" (#6295)
Co-authored-by: Kyle K <190571+Docteh@users.noreply.github.com>
2023-02-11 23:22:58 +01:00
a40cde7f76 msvc: fix missing qtconcurrent dll (#6294) 2023-02-10 15:36:36 -03:00
5eb72e9489 main: Enable High DPI fixes for Qt >= 5.14 (#6262)
Fixes https://github.com/citra-emu/citra/issues/4175
Fixes https://github.com/citra-emu/citra/issues/4977
2023-02-10 01:19:43 +01:00
6bef34852c Add option to configure to download system files from Nintendo Update Service (#6269)
Co-authored-by: B3n30 <benediktthomas@gmail.com>
2023-02-09 21:58:08 +02:00
691cb43871 Add shortcuts and status bar widgets to toggle and set 3D factor (#6277) 2023-02-09 21:57:06 +02:00
f66d03dd48 citra_qt: Improvements to hotkeys and ui state management (#6224) 2023-02-04 19:06:20 +01:00
9b49d94644 Lower log level of some sdmc logs (#6266) 2023-02-03 23:41:06 +01:00
78cb48b23c common: Make TrimSourcePath constexpr (#6279) 2023-02-03 19:01:10 +01:00
3a6a17c708 externals: Bundle cryptopp as submodule. (#6272)
fix https://github.com/citra-emu/citra/issues/6271
2023-02-02 16:26:21 +01:00
3d3dd2d162 Merge pull request #6275 from zhaowenlan1779/tx-migrate
dist/languages: Migrate to new tx client
2023-02-02 16:51:13 +08:00
99c3eb21b0 dist/languages: Migrate to new tx client 2023-02-01 14:57:12 +08:00
bb8cb3ff82 main: Globally disable the "?" button on dialogs (#6263)
Sets the AA_DisableWindowContextHelpButton attribute to disable this useless button globally. This is disabled by default on Qt6.
2023-01-28 21:38:15 +05:30
0f6478a928 Fix crash when plugin has missing text section (#6264) 2023-01-27 15:06:35 +02:00
84e54a52a6 core: Detect and return error if GBA virtual console is loaded. (#6257) 2023-01-23 15:21:57 +05:30
d704c6a3ac common: Support macOS application data path conventions. (#6258) 2023-01-23 15:20:50 +05:30
9b20bcea0f android: Allow opening in-emulation menu by pressing Back (#6248)
It seems like the menu isn't showing up for users on Android 13.
We're not sure what's wrong, and the approach we've been using for the
menu hasn't been entirely reliable in the past either (in particular
not on non-mobile form factors like Chromebooks and VR devices),
so let's make it possible to open the menu by pressing Back,
an action that works reliably on most kinds of Android devices.
(Not sure if there's an equivalent of Back on devices like watches,
but I think we can pretty safely ignore those for now.)
2023-01-22 13:12:27 +05:30
9c6035f254 Implement svcGetHandleInfo, svcOpenProcess/Thread, svcGetProcessList (#6243)
* Implement svcGetHandleInfo, svcOpenProcess/Thread, svcGetProcessList

* Apply suggestions

* Add comment to stubbed enum values in svcGetHandleInfo

* Revert u32 -> size_t
2023-01-15 21:50:54 +05:30
a298e4969b externals: Switch to newer cryptopp-cmake. (#6242) 2023-01-15 21:45:42 +05:30
bd1cabce86 Instead of there being an "Abort/Continue" prompt when a savestate fails to save or load, it just brings up a warning box. (#6236)
* This fixes #6041 by changing OnCoreError. Instead of there being an "Abort/Continue" prompt when a savestate fails to save or load, it just brings up a warning box.

I also changed "Abort/Continue" to "Quit Game/Continue" for better clarity

* Fixed formatting
2023-01-15 21:45:16 +05:30
ad2cbe2b26 macOS: Make Citra show up in the Launchpad Games folder (#6245) 2023-01-10 01:27:11 +01:00
e60a816d89 Merge pull request #6240 from Steveice10/universal
ci: Generate universal macOS build.
2023-01-07 19:39:22 +05:30
5d0e60a6aa ci: Generate universal macOS build. 2023-01-07 01:09:36 -08:00
a8848cce43 build: Update to support multi-arch builds. 2023-01-07 01:09:32 -08:00
0e325255f3 externals: point to upstream dynarmic 2023-01-06 06:41:51 -08:00
21fe65c29c externals: bump xbyak to v6.68 2023-01-06 06:41:51 -08:00
f5ed7c775d qt: Fix global settings being inaccessible on macOS. (#6235) 2023-01-03 23:58:04 +02:00
f18437e59f android: Close soft keyboard on panel close in cheats activity (#6234)
This prevents the keyboard from being stuck in an open state if the user
slides the panel while focused on a text box. Now it closes gracefully.

Co-authored-by: Charles Lombardo <clombardo169@gmail.com>
2023-01-01 16:55:05 +05:30
383 changed files with 13266 additions and 8107 deletions

View File

@ -2,7 +2,6 @@
set -o pipefail
export Qt5_DIR=$(brew --prefix)/opt/qt5
export PATH="/usr/local/opt/ccache/libexec:$PATH"
# ccache configurations
export CCACHE_CPP2=yes
@ -16,16 +15,22 @@ export ASM="clang"
ccache -s
mkdir build && cd build
# TODO: LibreSSL ASM disabled due to platform detection issues in build.
cmake .. -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES="$TARGET_ARCH" \
-DENABLE_QT_TRANSLATION=ON \
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} \
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
-DUSE_DISCORD_PRESENCE=ON \
-DENABLE_FFMPEG_AUDIO_DECODER=ON \
-DENABLE_FFMPEG_VIDEO_DUMPER=ON \
-DENABLE_ASM=OFF \
-GNinja
ninja
ccache -s
ctest -VV -C Release
CURRENT_ARCH=`arch`
if [ "$TARGET_ARCH" = "$CURRENT_ARCH" ]; then
ctest -VV -C Release
fi

View File

@ -1,23 +1,20 @@
#!/bin/sh -ex
brew update
brew unlink python@2 || true
rm '/usr/local/bin/2to3' || true
brew install qt5 p7zip ccache ninja || true
brew install ccache ninja || true
pip3 install macpack
export SDL_VER=2.0.16
export FFMPEG_VER=4.4
export FFMPEG_VER=5.1
export QT_VER=5.15.8
mkdir tmp
cd tmp/
# install SDL
wget https://github.com/SachinVin/ext-macos-bin/raw/main/sdl2/sdl-${SDL_VER}.7z
7z x sdl-${SDL_VER}.7z
cp -rv $(pwd)/sdl-${SDL_VER}/* /
# install FFMPEG
wget https://github.com/SachinVin/ext-macos-bin/raw/main/ffmpeg/ffmpeg-${FFMPEG_VER}.7z
7z x ffmpeg-${FFMPEG_VER}.7z
cp -rv $(pwd)/ffmpeg-${FFMPEG_VER}/* /
# install Qt
wget https://github.com/SachinVin/ext-macos-bin/raw/main/qt/qt-${QT_VER}.7z
7z x qt-${QT_VER}.7z
sudo cp -rv $(pwd)/qt-${QT_VER}/* /usr/local/

45
.ci/macos/universal.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash -ex
. .ci/common/pre-upload.sh
REV_NAME="citra-osx-${GITDATE}-${GITREV}"
ARCHIVE_NAME="${REV_NAME}.tar.gz"
COMPRESSION_FLAGS="-czvf"
ARTIFACTS_LIST=($ARTIFACTS)
# Set up the base artifact to combine into.
BASE_ARTIFACT=${ARTIFACTS_LIST[0]}
BASE_ARTIFACT_ARCH="${BASE_ARTIFACT##*-}"
tar xf $BASE_ARTIFACT/$REV_NAME.tar.gz -C $BASE_ARTIFACT
mv $BASE_ARTIFACT/$REV_NAME $REV_NAME
# Executable binary paths that need to be combined.
BIN_PATHS=(citra citra-room citra-qt.app/Contents/MacOS/citra-qt)
# Dylib paths that need to be combined.
IFS=$'\n'
DYLIB_PATHS=($(cd $REV_NAME && find . -name '*.dylib'))
unset IFS
# Combine all of the executable binaries and dylibs.
for OTHER_ARTIFACT in "${ARTIFACTS_LIST[@]:1}"; do
OTHER_ARTIFACT_ARCH="${OTHER_ARTIFACT##*-}"
tar xf $OTHER_ARTIFACT/$REV_NAME.tar.gz -C $OTHER_ARTIFACT
for BIN_PATH in "${BIN_PATHS[@]}"; do
lipo -create -output $REV_NAME/$BIN_PATH $REV_NAME/$BIN_PATH $OTHER_ARTIFACT/$REV_NAME/$BIN_PATH
done
for DYLIB_PATH in "${DYLIB_PATHS[@]}"; do
# Only merge if the libraries do not have conflicting arches, otherwise it will fail.
DYLIB_INFO=`file $REV_NAME/$DYLIB_PATH`
OTHER_DYLIB_INFO=`file $OTHER_ARTIFACT/$REV_NAME/$DYLIB_PATH`
if ! [[ "$DYLIB_INFO" =~ "$OTHER_ARTIFACT_ARCH" ]] && ! [[ "$OTHER_DYLIB_INFO" =~ "$BASE_ARTIFACT_ARCH" ]]; then
lipo -create -output $REV_NAME/$DYLIB_PATH $REV_NAME/$DYLIB_PATH $OTHER_ARTIFACT/$REV_NAME/$DYLIB_PATH
fi
done
done
. .ci/common/post-upload.sh

View File

@ -15,7 +15,7 @@ cp build/bin/Release/citra-room "$REV_NAME"
# move libs into folder for deployment
macpack "${REV_NAME}/citra-qt.app/Contents/MacOS/citra-qt" -d "../Frameworks"
# move qt frameworks into app bundle for deployment
$(brew --prefix)/opt/qt5/bin/macdeployqt "${REV_NAME}/citra-qt.app" -executable="${REV_NAME}/citra-qt.app/Contents/MacOS/citra-qt"
macdeployqt "${REV_NAME}/citra-qt.app" -executable="${REV_NAME}/citra-qt.app/Contents/MacOS/citra-qt"
# move libs into folder for deployment
macpack "${REV_NAME}/citra" -d "libs"

View File

@ -0,0 +1,10 @@
name: New Issue (Developers Only)
description: A blank issue template for developers only. If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template.
body:
- type: markdown
attributes:
value: |
**If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template.**
- type: textarea
attributes:
label: "Issue"

View File

@ -1,35 +0,0 @@
---
name: Bug Report / Feature Request
about: Tech support does not belong here. You should only file an issue here if you think you have experienced an actual bug with Citra or you are requesting a feature you believe would make Citra better.
title: ''
labels: ''
assignees: ''
---
<!---
Please read the FAQ:
https://citra-emu.org/wiki/faq/
THIS IS NOT A SUPPORT FORUM, FOR SUPPORT GO TO:
https://community.citra-emu.org/
If the FAQ does not answer your question, please go to:
https://community.citra-emu.org/
====================================================
When submitting an issue, please check the following:
- You have read the above.
- You have provided the version (commit hash) of Citra you are using.
- You have provided sufficient detail for the issue to be reproduced.
- You have provided system specs (if relevant).
- Please also provide:
- For any issues, a log file
- For crashes, a backtrace.
- For graphical issues, comparison screenshots with real hardware.
- For emulation inaccuracies, a test-case (if able).
--->

64
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Bug Report
description: File a bug report
body:
- type: markdown
attributes:
value: Tech support does not belong here. You should only file an issue here if you think you have experienced an actual bug with Citra.
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: input
attributes:
label: Affected Build(s)
description: List the affected build(s) that this issue applies to.
placeholder: Nightly 1234 / Canary 1234
validations:
required: true
- type: textarea
id: issue-desc
attributes:
label: Description of Issue
description: A brief description of the issue encountered along with any images and/or videos.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A brief description of how it is expected to work along with any images and/or videos.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Reproduction Steps
description: A brief explanation of how to reproduce this issue. If possible, provide a save file to aid in reproducing the issue.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log File
description: A log file will help our developers to better diagnose and fix the issue.
validations:
required: true
- type: textarea
id: system-config
attributes:
label: System Configuration
placeholder: |
CPU: Intel i5-10400 / AMD Ryzen 5 3600
GPU/Driver: NVIDIA GeForce GTX 1060 (Driver 512.95)
RAM: 16GB DDR4-3200
OS: Windows 11 22H2 (Build 22621.819)
value: |
CPU:
GPU/Driver:
RAM:
OS:
validations:
required: true

View File

@ -6,6 +6,3 @@ contact_links:
- name: Community forums
url: https://community.citra-emu.org
about: This is an alternative place for tech support, however helpers there are not as active.
- name: Citra Android
url: https://github.com/citra-emu/citra-android
about: If you need tech support on Citra Android, you should use either of the above two options. If you want to file an issue, you should go to the Android repo linked here.

View File

@ -0,0 +1,28 @@
name: Feature Request
description: File a feature request
labels: "request"
body:
- type: markdown
attributes:
value: Tech support does not belong here. You should only file an issue here if you are requesting a feature you believe would make Citra better.
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: what-feature
attributes:
label: What feature are you suggesting?
description: A brief description of the requested feature.
validations:
required: true
- type: textarea
id: why-feature
attributes:
label: Why would this feature be useful?
description: A brief description of why this feature would make Citra better.
validations:
required: true

View File

@ -74,6 +74,9 @@ jobs:
path: artifacts/
macos:
runs-on: macos-latest
strategy:
matrix:
arch: ["x86_64", "arm64"]
steps:
- uses: actions/checkout@v3
with:
@ -82,9 +85,9 @@ jobs:
uses: actions/cache@v3
with:
path: ~/Library/Caches/ccache
key: ${{ runner.os }}-macos-${{ github.sha }}
key: ${{ runner.os }}-macos-${{ matrix.arch }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-macos-
${{ runner.os }}-macos-${{ matrix.arch }}-
- name: Query tag name
uses: little-core-labs/get-git-tag@v3.0.2
id: tagName
@ -93,8 +96,51 @@ jobs:
- name: Build
run: ./.ci/macos/build.sh
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
MACOSX_DEPLOYMENT_TARGET: "11"
ENABLE_COMPATIBILITY_REPORTING: "ON"
TARGET_ARCH: ${{ matrix.arch }}
- name: Pack
run: ./.ci/macos/upload.sh
- name: Upload
uses: actions/upload-artifact@v3
with:
name: macos-${{ matrix.arch }}
path: artifacts/
macos-universal:
runs-on: macos-latest
needs: macos
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Query tag name
uses: little-core-labs/get-git-tag@v3.0.2
id: tagName
- name: Download x86 build
uses: actions/download-artifact@master
with:
name: macos-x86_64
path: macos-x86_64/
- name: Download ARM64 build
uses: actions/download-artifact@master
with:
name: macos-arm64
path: macos-arm64/
- name: Create universal app
run: ./.ci/macos/universal.sh
env:
ARTIFACTS: macos-x86_64 macos-arm64
# - name: Upload
# uses: actions/upload-artifact@v3
# with:
# name: macos
# path: artifacts/
- name: Delete intermediate artifacts
uses: geekyeggo/delete-artifact@v2
with:
name: |
macos-x86_64
macos-arm64
windows:
runs-on: windows-latest
steps:
@ -178,7 +224,7 @@ jobs:
TRANSIFEX_API_TOKEN: ${{ secrets.TRANSIFEX_API_TOKEN }}
release:
runs-on: ubuntu-latest
needs: [build, android, macos, source, windows]
needs: [build, android, macos-universal, source, windows]
if: ${{ startsWith(github.ref, 'refs/tags/') }}
steps:
- uses: actions/download-artifact@v3

11
.gitmodules vendored
View File

@ -12,13 +12,10 @@
url = https://github.com/catchorg/Catch2
[submodule "dynarmic"]
path = externals/dynarmic
url = https://github.com/citra-emu/dynarmic.git
url = https://github.com/merryhime/dynarmic.git
[submodule "xbyak"]
path = externals/xbyak
url = https://github.com/herumi/xbyak.git
[submodule "cryptopp"]
path = externals/cryptopp/cryptopp
url = https://github.com/weidai11/cryptopp.git
[submodule "fmt"]
path = externals/fmt
url = https://github.com/fmtlib/fmt.git
@ -58,3 +55,9 @@
[submodule "sdl2"]
path = externals/sdl2/SDL
url = https://github.com/libsdl-org/SDL
[submodule "cryptopp-cmake"]
path = externals/cryptopp-cmake
url = https://github.com/abdes/cryptopp-cmake.git
[submodule "cryptopp"]
path = externals/cryptopp
url = https://github.com/weidai11/cryptopp.git

View File

@ -146,14 +146,14 @@ function(detect_architecture symbol arch)
# CMake's crazy scope rules will keep it defined
if (ARCHITECTURE_${arch})
set(ARCHITECTURE "${arch}" PARENT_SCOPE)
set(ARCHITECTURE_${arch} 1 PARENT_SCOPE)
add_definitions(-DARCHITECTURE_${arch}=1)
endif()
endif()
endfunction()
if (NOT ENABLE_GENERIC)
if (MSVC)
if (CMAKE_OSX_ARCHITECTURES)
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
elseif (MSVC)
detect_architecture("_M_AMD64" x86_64)
detect_architecture("_M_IX86" x86)
detect_architecture("_M_ARM" arm)
@ -167,8 +167,6 @@ if (NOT ENABLE_GENERIC)
endif()
if (NOT DEFINED ARCHITECTURE)
set(ARCHITECTURE "GENERIC")
set(ARCHITECTURE_GENERIC 1)
add_definitions(-DARCHITECTURE_GENERIC=1)
endif()
message(STATUS "Target architecture: ${ARCHITECTURE}")
@ -194,7 +192,7 @@ find_package(Threads REQUIRED)
if (ENABLE_QT)
if (CITRA_USE_BUNDLED_QT)
if (MSVC_VERSION GREATER_EQUAL 1920 AND ARCHITECTURE_x86_64)
if (MSVC_VERSION GREATER_EQUAL 1920 AND "x86_64" IN_LIST ARCHITECTURE)
set(QT_VER qt-5.15.7-msvc2019_64)
else()
message(FATAL_ERROR "No bundled Qt binaries for your toolchain. Disable CITRA_USE_BUNDLED_QT and provide your own.")
@ -211,7 +209,11 @@ if (ENABLE_QT)
set(QT_PREFIX_HINT)
endif()
find_package(Qt5 REQUIRED COMPONENTS Widgets Multimedia ${QT_PREFIX_HINT})
find_package(Qt5 REQUIRED COMPONENTS Widgets Multimedia Concurrent ${QT_PREFIX_HINT})
if (UNIX AND NOT APPLE)
find_package(Qt5 REQUIRED COMPONENTS DBus ${QT_PREFIX_HINT})
endif()
if (ENABLE_QT_TRANSLATION)
find_package(Qt5 REQUIRED COMPONENTS LinguistTools ${QT_PREFIX_HINT})
@ -231,7 +233,7 @@ endif()
if (ENABLE_FFMPEG)
if (CITRA_USE_BUNDLED_FFMPEG)
if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND ARCHITECTURE_x86_64)
if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND "x86_64" IN_LIST ARCHITECTURE)
set(FFmpeg_VER "ffmpeg-4.1-win64")
else()
message(FATAL_ERROR "No bundled FFmpeg binaries for your toolchain. Disable CITRA_USE_BUNDLED_FFMPEG and provide your own.")
@ -381,12 +383,18 @@ endif()
enable_testing()
add_subdirectory(externals)
# See externals/CMakeLists.txt
foreach(def ${CRYPTOPP_COMPILE_DEFINITIONS})
add_definitions(-D${def})
endforeach()
# Boost
if (USE_SYSTEM_BOOST)
find_package(Boost 1.70.0 COMPONENTS serialization REQUIRED)
find_package(Boost 1.70.0 COMPONENTS serialization iostreams REQUIRED)
else()
add_library(Boost::boost ALIAS boost)
add_library(Boost::serialization ALIAS boost_serialization)
add_library(Boost::iostreams ALIAS boost_iostreams)
endif()
# SDL2

View File

@ -17,6 +17,7 @@ function(copy_citra_Qt5_deps target_dir)
Qt5Core$<$<CONFIG:Debug>:d>.*
Qt5Gui$<$<CONFIG:Debug>:d>.*
Qt5Widgets$<$<CONFIG:Debug>:d>.*
Qt5Concurrent$<$<CONFIG:Debug>:d>.*
Qt5Multimedia$<$<CONFIG:Debug>:d>.*
Qt5Network$<$<CONFIG:Debug>:d>.*
)

View File

@ -1,24 +1,49 @@
**BEFORE FILING AN ISSUE, READ THE RELEVANT SECTION IN THE [CONTRIBUTING](https://github.com/citra-emu/citra/wiki/Contributing#reporting-issues) FILE!!!**
<h1 align="center">
<br>
<a href="https://citra-emu.org/"><img src="https://raw.githubusercontent.com/citra-emu/citra-assets/master/Main/citra_logo.svg" alt="Citra" width="200"></a>
<br>
<b>Citra</b>
<br>
</h1>
# Citra
<h4 align="center"><b>Citra</b> is the world's most popular, open-source, Nintendo 3DS emulator.
<br>
It is written in C++ with portability in mind and builds are actively maintained for Windows, Linux, Android and macOS.
</h4>
[![GitHub Actions Build Status](https://github.com/citra-emu/citra/workflows/citra-ci/badge.svg)](https://github.com/citra-emu/citra/actions)
[![Bitrise CI Build Status](https://app.bitrise.io/app/4ccd8e5720f0d13b/status.svg?token=H32TmbCwxb3OQ-M66KbAyw&branch=master)](https://app.bitrise.io/app/4ccd8e5720f0d13b)
[![Discord](https://img.shields.io/discord/220740965957107713?color=%237289DA&label=Citra&logo=discord&logoColor=white)](https://discord.gg/FAXfZV9)
<p align="center">
<a href="https://github.com/citra-emu/citra/actions/">
<img src="https://github.com/citra-emu/citra/workflows/citra-ci/badge.svg"
alt="GitHub Actions Build Status">
</a>
<a href="https://discord.gg/FAXfZV9">
<img src="https://img.shields.io/discord/220740965957107713?color=%237289DA&label=Citra&logo=discord&logoColor=white"
alt="Discord">
</a>
</p>
Citra is an experimental open-source Nintendo 3DS emulator/debugger written in C++. It is written with portability in mind, with builds actively maintained for Windows, Linux and macOS.
<p align="center">
<a href="#compatibility">Compatibility</a> |
<a href="#releases">Releases</a> |
<a href="#development">Development</a> |
<a href="#building">Building</a> |
<a href="#support">Support</a> |
<a href="#license">License</a>
</p>
Citra emulates a subset of 3DS hardware and therefore is useful for running/debugging homebrew applications, and it is also able to run many commercial games! Some of these do not run at a playable state, but we are working every day to advance the project forward. (Playable here means compatibility of at least "Okay" on our [game compatibility list](https://citra-emu.org/game).)
Citra is licensed under the GPLv2 (or any later version). Refer to the license.txt file included. Please read the [FAQ](https://citra-emu.org/wiki/faq/) before getting started with the project.
## Compatibility
Check out our [website](https://citra-emu.org/)!
The emulator is capable of running most commercial games at full speed, provided you meet the necessary hardware requirements.
For a full list of games Citra supports, please visit our [Compatibility page](https://citra-emu.org/game/)
Check out our [website](https://citra-emu.org/) for the latest news on exciting features, progress reports, and more!
Please read the [FAQ](https://citra-emu.org/wiki/faq/) before getting started with the project.
Need help? Check out our [asking for help](https://citra-emu.org/help/reference/asking/) guide.
For development discussion, please join us on our [Discord server](https://citra-emu.org/discord/) or at #citra-dev on libera.
### Releases
## Releases
Citra has two main release channels: Nightly and Canary.
@ -28,30 +53,46 @@ The [Canary](https://github.com/citra-emu/citra-canary) build is based on the ma
Both builds can be installed with the installer provided on the [website](https://citra-emu.org/download/), but those looking for specific versions or standalone releases can find them in the release tabs of the [Nightly](https://github.com/citra-emu/citra-nightly/releases) and [Canary](https://github.com/citra-emu/citra-canary/releases) repositories.
Currently, development and releases of the Android version are in [a separate repository](https://github.com/citra-emu/citra-android).
Android builds can be downloaded from the Google Play Store.
A Flatpak for Citra is available on [Flathub](https://flathub.org/apps/details/org.citra_emu.citra). Details on the build process can be found in [our Flathub repository](https://github.com/flathub/org.citra_emu.citra).
### Development
## Development
Most of the development happens on GitHub. It's also where [our central repository](https://github.com/citra-emu/citra) is hosted.
For development discussion, please join us on our [Discord server](https://citra-emu.org/discord/) or at #citra-dev on libera.
If you want to contribute please take a look at the [Contributor's Guide](https://github.com/citra-emu/citra/wiki/Contributing) and [Developer Information](https://github.com/citra-emu/citra/wiki/Developer-Information). You should also contact any of the developers in the forum in order to know about the current state of the emulator because the [TODO list](https://docs.google.com/document/d/1SWIop0uBI9IW8VGg97TAtoT_CHNoP42FzYmvG1F4QDA) isn't maintained anymore.
If you want to contribute please take a look at the [Contributor's Guide](https://github.com/citra-emu/citra/wiki/Contributing) and [Developer Information](https://github.com/citra-emu/citra/wiki/Developer-Information). You can also contact any of the developers on Discord in order to know about the current state of the emulator.
If you want to contribute to the user interface translation, please check out the [citra project on transifex](https://www.transifex.com/citra/citra). We centralize the translation work there, and periodically upstream translations.
If you want to contribute to the user interface translation, please check out the [Citra project on transifex](https://www.transifex.com/citra/citra). We centralize the translation work there, and periodically upstream translations.
### Building
## Building
* __Windows__: [Windows Build](https://github.com/citra-emu/citra/wiki/Building-For-Windows)
* __Linux__: [Linux Build](https://github.com/citra-emu/citra/wiki/Building-For-Linux)
* __macOS__: [macOS Build](https://github.com/citra-emu/citra/wiki/Building-for-macOS)
* __Android__: [Android Build](https://github.com/citra-emu/citra/wiki/Building-for-Android)
### Support
We happily accept monetary donations or donated games and hardware. Please see our [donations page](https://citra-emu.org/donate/) for more information on how you can contribute to Citra. Any donations received will go towards things like:
## Support
If you enjoy the project and want to support us financially, check out our Patreon!
<a href="https://www.patreon.com/citraemu">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
We also happily accept donated games and hardware.
Please see our [donations page](https://citra-emu.org/donate/) for more information on how you can contribute to Citra.
Any donations received will go towards things like:
* 3DS consoles for developers to explore the hardware
* 3DS games for testing
* Any equipment required for homebrew
* Infrastructure setup
We also more than gladly accept used 3DS consoles! If you would like to give yours away, don't hesitate to join our [Discord server](https://citra-emu.org/discord/) and talk to bunnei.
## License
Citra is licensed under the GPLv2 (or any later version). Refer to the [LICENSE.txt](https://github.com/citra-emu/citra/blob/master/license.txt) file.

View File

@ -1,9 +1,9 @@
[main]
host = https://www.transifex.com
[citra.emulator]
file_filter = <lang>.ts
source_file = en.ts
source_lang = en
type = QT
[main]
host = https://www.transifex.com
[o:citra:p:citra:r:emulator]
file_filter = <lang>.ts
source_file = en.ts
source_lang = en
type = QT

View File

@ -13,6 +13,6 @@
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource>
<qresource prefix="colorful">
<file>style.qss</file>
<file alias="style.qss">../default/style.qss</file>
</qresource>
</RCC>

View File

@ -1,4 +0,0 @@
/*
This file is intentionally left blank.
We do not want to apply any stylesheet for colorful, only icons.
*/

View File

@ -1,33 +1,22 @@
<RCC>
<qresource prefix="icons/default">
<file alias="index.theme">icons/index.theme</file>
<file alias="16x16/checked.png">icons/16x16/checked.png</file>
<file alias="16x16/failed.png">icons/16x16/failed.png</file>
<file alias="16x16/connected.png">icons/16x16/connected.png</file>
<file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file>
<file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file>
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
<file alias="256x256/citra.png">icons/256x256/citra.png</file>
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
</qresource>
<qresource prefix="default">
<file>style.qss</file>
</qresource>
</RCC>

14
dist/qt_themes/default/style.qss vendored Normal file
View File

@ -0,0 +1,14 @@
QPushButton#3DOptionStatusBarButton {
color: #A5A5A5;
font-weight: bold;
border: 1px solid transparent;
background-color: transparent;
padding: 0px 3px 0px 3px;
text-align: center;
min-width: 60px;
min-height: 20px;
}
QPushButton#3DOptionStatusBarButton:hover {
border: 1px solid #76797C;
}

View File

@ -522,13 +522,12 @@ QToolButton#qt_toolbar_ext_button {
QPushButton {
color: #eff0f1;
border-width: 1px;
border-color: #54575B;
border-style: solid;
padding: 6px 4px;
border: 1px solid #54575B;
border-radius: 2px;
padding: 5px 0px 5px 0px;
outline: none;
min-width: 100px;
min-height: 13px;
background-color: #232629;
}
@ -1237,3 +1236,18 @@ QPlainTextEdit:disabled {
TouchScreenPreview {
qproperty-dotHighlightColor: #3daee9;
}
QPushButton#3DOptionStatusBarButton {
color: #A5A5A5;
font-weight: bold;
border: 1px solid transparent;
background-color: transparent;
padding: 0px 3px 0px 3px;
text-align: center;
min-width: 60px;
min-height: 20px;
}
QPushButton#3DOptionStatusBarButton:hover {
border: 1px solid #76797C;
}

View File

@ -21,6 +21,15 @@ file(GLOB boost_serialization_SRC "${CMAKE_SOURCE_DIR}/externals/boost/libs/seri
add_library(boost_serialization STATIC ${boost_serialization_SRC})
target_link_libraries(boost_serialization PUBLIC boost)
# Boost::iostreams
add_library(
boost_iostreams
STATIC
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/file_descriptor.cpp
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/mapped_file.cpp
)
target_link_libraries(boost_iostreams PUBLIC boost)
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
# Catch2
@ -29,25 +38,45 @@ set(CATCH_INSTALL_EXTRAS OFF)
add_subdirectory(catch2)
# Crypto++
add_subdirectory(cryptopp)
set(CRYPTOPP_BUILD_DOCUMENTATION OFF)
set(CRYPTOPP_BUILD_TESTING OFF)
set(CRYPTOPP_INSTALL OFF)
set(CRYPTOPP_SOURCES "${CMAKE_SOURCE_DIR}/externals/cryptopp")
add_subdirectory(cryptopp-cmake)
# HACK: Mismatch between compilation of CryptoPP and headers used in Citra can cause runtime issues.
# Pull out the compile definitions from CryptoPP and apply them to Citra as well to fix this.
# See: https://github.com/weidai11/cryptopp/issues/1191
get_source_file_property(CRYPTOPP_COMPILE_DEFINITIONS ${CRYPTOPP_SOURCES}/cryptlib.cpp TARGET_DIRECTORY cryptopp COMPILE_DEFINITIONS)
set(CRYPTOPP_COMPILE_DEFINITIONS ${CRYPTOPP_COMPILE_DEFINITIONS} PARENT_SCOPE)
# HACK: The logic to set up the base include directory for CryptoPP does not work with Android SDK CMake 3.22.1.
# Until there is a fixed version available, this code will detect and add in the proper include if it does not exist.
if(ANDROID)
message(STATUS "Applying CryptoPP include fix.")
get_target_property(CRYPTOPP_INCLUDES cryptopp INTERFACE_INCLUDE_DIRECTORIES)
foreach(CRYPTOPP_INCLUDE ${CRYPTOPP_INCLUDES})
if("${CRYPTOPP_INCLUDE}" MATCHES "\\$<BUILD_INTERFACE:(.*)/cryptopp>")
message(STATUS "Fixed include path: ${CMAKE_MATCH_1}")
target_include_directories(cryptopp PUBLIC $<BUILD_INTERFACE:${CMAKE_MATCH_1}>)
break()
endif()
endforeach()
endif()
# fmt and Xbyak need to be added before dynarmic
# libfmt
option(FMT_INSTALL "" ON)
add_subdirectory(fmt)
# Xbyak
if (ARCHITECTURE_x86_64)
add_library(xbyak INTERFACE)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/xbyak/include)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/xbyak/xbyak DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/xbyak/include)
target_include_directories(xbyak SYSTEM INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/xbyak/include)
target_compile_definitions(xbyak INTERFACE XBYAK_NO_OP_NAMES)
if ("x86_64" IN_LIST ARCHITECTURE)
add_subdirectory(xbyak)
endif()
# Dynarmic
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
set(DYNARMIC_TESTS OFF)
set(DYNARMIC_NO_BUNDLED_FMT ON)
set(DYNARMIC_FRONTENDS "A32")
add_subdirectory(dynarmic)
endif()

1
externals/cryptopp vendored Submodule

Submodule externals/cryptopp added at 511806c0eb

1
externals/cryptopp-cmake vendored Submodule

View File

@ -1,449 +0,0 @@
# A trimmed down version of the CMakeLists.txt from noloader/cryptopp-cmake
# The differences are:
# - removed support for legacy CMake versions
# - removed support for 32-bit
# - added prefix "CRYPTOPP_OPT_" to all option names
# - disabled testing
# - disabled installation
# - disabled documentation
# - configured to build a static library only
# - adds include directories to the library target
cmake_minimum_required(VERSION 3.1)
if (POLICY CMP0048)
cmake_policy(SET CMP0048 NEW)
endif ()
project(cryptopp VERSION 8.5.0)
if (POLICY CMP0054)
cmake_policy(SET CMP0054 NEW)
endif ()
set(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cryptopp)
include(TestBigEndian)
include(GNUInstallDirs)
include(CheckCXXCompilerFlag)
set(TEST_PROG_DIR ${SRC_DIR}/TestPrograms)
set(TEST_CXX_FILE ${TEST_PROG_DIR}/test_cxx.cxx)
#============================================================================
# Settable options
#============================================================================
option(CRYPTOPP_OPT_DISABLE_ASM "Disable ASM" OFF)
option(CRYPTOPP_OPT_DISABLE_SSSE3 "Disable SSSE3" OFF)
option(CRYPTOPP_OPT_DISABLE_SSE4 "Disable SSE4" OFF)
option(CRYPTOPP_OPT_DISABLE_AESNI "Disable AES-NI" OFF)
option(CRYPTOPP_OPT_DISABLE_SHA "Disable SHA" OFF)
option(CRYPTOPP_OPT_DISABLE_AVX "Disable AVX" OFF)
option(CRYPTOPP_OPT_DISABLE_AVX2 "Disable AVX2" OFF)
#============================================================================
# Compiler options
#============================================================================
# Only set when cross-compiling, http://www.vtk.org/Wiki/CMake_Cross_Compiling
if (NOT (CMAKE_SYSTEM_VERSION AND CMAKE_SYSTEM_PROCESSOR))
set(CRYPTOPP_CROSS_COMPILE 1)
else()
set(CRYPTOPP_CROSS_COMPILE 0)
endif()
set(CRYPTOPP_COMPILE_DEFINITIONS)
set(CRYPTOPP_COMPILE_OPTIONS)
# Don't use RPATH's. The resulting binary could fail a security audit.
set(CMAKE_MACOSX_RPATH 0)
if(CMAKE_CXX_COMPILER_ID MATCHES "Intel")
list(APPEND CRYPTOPP_COMPILE_OPTIONS -wd68 -wd186 -wd279 -wd327 -wd161 -wd3180)
endif()
if(MSVC)
# Disable C4390: empty controlled statement found: is this the intent?
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4390")
endif()
# Endianness
TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
add_definitions(-DIS_BIG_ENDIAN)
endif()
if (CRYPTOPP_OPT_DISABLE_ASM)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_ASM)
endif ()
if (CRYPTOPP_OPT_DISABLE_SSSE3)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_SSSE3)
endif ()
if (CRYPTOPP_OPT_DISABLE_SSE4)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_SSSE4)
endif ()
if (CRYPTOPP_OPT_DISABLE_CLMUL)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_CLMUL)
endif ()
if (CRYPTOPP_OPT_DISABLE_AESNI)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_AESNI)
endif ()
if (CRYPTOPP_OPT_DISABLE_RDRAND)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_RDRAND)
endif ()
if (CRYPTOPP_OPT_DISABLE_RDSEED)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_RDSEED)
endif ()
if (CRYPTOPP_OPT_DISABLE_AVX)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_AVX)
endif ()
if (CRYPTOPP_OPT_DISABLE_AVX2)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_AVX2)
endif ()
if (CRYPTOPP_OPT_DISABLE_SHA)
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS CRYPTOPP_DISABLE_SHA)
endif ()
# We need the output 'uname -s' for Unix and Linux system detection
if (NOT CRYPTOPP_CROSS_COMPILE)
set (UNAME_CMD "uname")
set (UNAME_ARG "-s")
execute_process(COMMAND ${UNAME_CMD} ${UNAME_ARG}
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
RESULT_VARIABLE UNAME_RESULT
OUTPUT_VARIABLE UNAME_SYSTEM)
string(REGEX REPLACE "\n$" "" UNAME_SYSTEM "${UNAME_SYSTEM}")
endif()
# We need the output 'uname -m' for Unix and Linux platform detection
if (NOT CRYPTOPP_CROSS_COMPILE)
set (UNAME_CMD "uname")
set (UNAME_ARG "-m")
execute_process(COMMAND ${UNAME_CMD} ${UNAME_ARG}
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
RESULT_VARIABLE UNAME_RESULT
OUTPUT_VARIABLE UNAME_MACHINE)
string(REGEX REPLACE "\n$" "" UNAME_MACHINE "${UNAME_MACHINE}")
endif()
###############################################################################
# Try to find a Posix compatible grep and sed. Solaris, Digital Unix,
# Tru64, HP-UX and a few others need tweaking
if (EXISTS /usr/xpg4/bin/grep)
set(GREP_CMD /usr/xpg4/bin/grep)
elseif (EXISTS /usr/gnu/bin/grep)
set(GREP_CMD /usr/gnu/bin/grep)
elseif (EXISTS /usr/linux/bin/grep)
set(GREP_CMD /usr/linux/bin/grep)
else ()
set(GREP_CMD grep)
endif ()
if (EXISTS /usr/xpg4/bin/sed)
set(SED_CMD /usr/xpg4/bin/sed)
elseif (EXISTS /usr/gnu/bin/sed)
set(SED_CMD /usr/gnu/bin/sed)
elseif (EXISTS /usr/linux/bin/sed)
set(SED_CMD /usr/linux/bin/sed)
else ()
set(SED_CMD sed)
endif ()
###############################################################################
function(CheckCompileOption opt var)
CHECK_CXX_COMPILER_FLAG(${opt} ${var})
endfunction(CheckCompileOption)
function(CheckCompileLinkOption opt var prog)
if (MSVC)
# TODO: improve this...
CHECK_CXX_COMPILER_FLAG(${opt} ${var})
elseif (APPLE)
message(STATUS "Performing Test ${var}")
try_compile(COMMAND_SUCCESS ${CMAKE_BINARY_DIR} ${prog} COMPILE_DEFINITIONS ${opt})
if (COMMAND_SUCCESS)
set(${var} 1 PARENT_SCOPE)
message(STATUS "Performing Test ${var} - Success")
else ()
set(${var} 0 PARENT_SCOPE)
message(STATUS "Performing Test ${var} - Failed")
endif ()
else ()
message(STATUS "Performing Test ${var}")
try_compile(COMMAND_SUCCESS ${CMAKE_BINARY_DIR} ${prog} COMPILE_DEFINITIONS ${opt})
if (COMMAND_SUCCESS)
set(${var} 1 PARENT_SCOPE)
message(STATUS "Performing Test ${var} - Success")
else ()
set(${var} 0 PARENT_SCOPE)
message(STATUS "Performing Test ${var} - Failed")
endif ()
endif ()
endfunction(CheckCompileLinkOption)
function(AddCompileOption opt)
if ("${COMMAND_OUTPUT}" NOT STREQUAL "")
list(APPEND CRYPTOPP_COMPILE_OPTIONS "${opt}")
endif ()
endfunction(AddCompileOption)
###############################################################################
function(DumpMachine output pattern)
if (MSVC)
# CMake does not provide a generic shell/terminal mechanism
# and Microsoft environments don't know what 'sh' is.
set(${output} 0 PARENT_SCOPE)
else ()
if(CMAKE_SYSTEM_PROCESSOR MATCHES ${pattern})
set(${output} TRUE PARENT_SCOPE)
endif()
endif()
endfunction(DumpMachine)
# Thansk to Anonimal for MinGW; see http://github.com/weidai11/cryptopp/issues/466
DumpMachine(CRYPTOPP_AMD64 "(x86_64|AMD64|amd64)")
DumpMachine(CRYPTOPP_I386 "(i.86)")
DumpMachine(CRYPTOPP_MINGW64 "(w64-mingw32)|(mingw64)")
DumpMachine(CRYPTOPP_ARMV8 "(armv8|aarch64)")
###############################################################################
if(MSVC)
if(CMAKE_SYSTEM_VERSION MATCHES "10\\.0.*")
list(APPEND CRYPTOPP_COMPILE_DEFINITIONS "_WIN32_WINNT=0x0A00")
endif()
list(APPEND CRYPTOPP_COMPILE_OPTIONS /FI winapifamily.h)
endif()
# Enable PIC for all targets except Windows and 32-bit x86.
# Avoid on 32-bit x86 due to register pressures.
if ((NOT CRYPTOPP_CROSS_COMPILE) AND (NOT (WINDOWS OR WINDOWS_STORE OR WINDOWS_PHONE)))
# Use Regex; match i386, i486, i586 and i686
if (NOT (${UNAME_MACHINE} MATCHES "i.86"))
SET(CMAKE_POSITION_INDEPENDENT_CODE 1)
endif()
endif()
# Link is driven through the compiler, but CXXFLAGS are not used. Also see
# http://public.kitware.com/pipermail/cmake/2003-June/003967.html
if (NOT (WINDOWS OR WINDOWS_STORE OR WINDOWS_PHONE))
SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_FLAGS}")
endif()
#============================================================================
# Sources & headers
#============================================================================
# Library headers
file(GLOB cryptopp_HEADERS ${SRC_DIR}/*.h)
# Remove headers used to build test suite
list(REMOVE_ITEM cryptopp_HEADERS
${SRC_DIR}/bench.h
${SRC_DIR}/validate.h
)
# Library sources.
# These have been trimmed to include only things Citra uses. This speeds up
# compiles and reduces the amount of compilation breakage.
set(cryptopp_SOURCES
# The Crypto++ readme says you should put these 3 object files first,
# to avoid "problems associated with C++ static initialization order",
# but doesn't actually tell what could go wrong. Better safe than sorry
# I guess...
${SRC_DIR}/cryptlib.cpp
${SRC_DIR}/cpu.cpp
${SRC_DIR}/integer.cpp
${SRC_DIR}/algparam.cpp
${SRC_DIR}/allocate.cpp
${SRC_DIR}/asn.cpp
${SRC_DIR}/authenc.cpp
${SRC_DIR}/base64.cpp
${SRC_DIR}/basecode.cpp
${SRC_DIR}/ccm.cpp
${SRC_DIR}/crc_simd.cpp
${SRC_DIR}/des.cpp
${SRC_DIR}/dessp.cpp
${SRC_DIR}/dll.cpp
${SRC_DIR}/ec2n.cpp
${SRC_DIR}/ecp.cpp
${SRC_DIR}/filters.cpp
${SRC_DIR}/fips140.cpp
${SRC_DIR}/gcm_simd.cpp
${SRC_DIR}/gf2n_simd.cpp
${SRC_DIR}/gf2n.cpp
${SRC_DIR}/gfpcrypt.cpp
${SRC_DIR}/hex.cpp
${SRC_DIR}/hmac.cpp
${SRC_DIR}/hrtimer.cpp
${SRC_DIR}/iterhash.cpp
${SRC_DIR}/md5.cpp
${SRC_DIR}/misc.cpp
${SRC_DIR}/modes.cpp
${SRC_DIR}/mqueue.cpp
${SRC_DIR}/nbtheory.cpp
${SRC_DIR}/neon_simd.cpp
${SRC_DIR}/oaep.cpp
${SRC_DIR}/osrng.cpp
${SRC_DIR}/ppc_power7.cpp
${SRC_DIR}/ppc_power8.cpp
${SRC_DIR}/ppc_power9.cpp
${SRC_DIR}/ppc_simd.cpp
${SRC_DIR}/pubkey.cpp
${SRC_DIR}/queue.cpp
${SRC_DIR}/randpool.cpp
${SRC_DIR}/rdtables.cpp
${SRC_DIR}/rijndael_simd.cpp
${SRC_DIR}/rijndael.cpp
${SRC_DIR}/rng.cpp
${SRC_DIR}/sha_simd.cpp
${SRC_DIR}/sha.cpp
${SRC_DIR}/sse_simd.cpp
)
if(ANDROID)
include_directories(${ANDROID_NDK}/sources/android/cpufeatures)
list(APPEND cryptopp_SOURCES ${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c)
endif()
set(cryptopp_SOURCES_ASM)
if (MSVC AND NOT DISABLE_ASM)
if (${CMAKE_GENERATOR} MATCHES ".*ARM")
message(STATUS "Disabling ASM because ARM is specified as target platform.")
else ()
enable_language(ASM_MASM)
list(APPEND cryptopp_SOURCES_ASM
${SRC_DIR}/rdrand.asm
${SRC_DIR}/rdseed.asm
)
if (CMAKE_SIZEOF_VOID_P EQUAL 8)
list(APPEND cryptopp_SOURCES_ASM
${SRC_DIR}/x64dll.asm
${SRC_DIR}/x64masm.asm
)
set_source_files_properties(${cryptopp_SOURCES_ASM} PROPERTIES COMPILE_DEFINITIONS "_M_X64")
else ()
set_source_files_properties(${cryptopp_SOURCES_ASM} PROPERTIES COMPILE_DEFINITIONS "_M_X86" COMPILE_FLAGS "/safeseh")
endif ()
set_source_files_properties(${cryptopp_SOURCES_ASM} PROPERTIES LANGUAGE ASM_MASM)
endif ()
endif ()
#============================================================================
# Architecture flags
#============================================================================
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Intel" OR CMAKE_CXX_COMPILER MATCHES "xlC")
if (CRYPTOPP_AMD64 OR CRYPTOPP_I386)
CheckCompileLinkOption("-msse2" CRYPTOPP_IA32_SSE2
"${TEST_PROG_DIR}/test_x86_sse2.cxx")
CheckCompileLinkOption("-mssse3" CRYPTOPP_IA32_SSSE3
"${TEST_PROG_DIR}/test_x86_ssse3.cxx")
CheckCompileLinkOption("-msse4.1" CRYPTOPP_IA32_SSE41
"${TEST_PROG_DIR}/test_x86_sse41.cxx")
CheckCompileLinkOption("-msse4.2" CRYPTOPP_IA32_SSE42
"${TEST_PROG_DIR}/test_x86_sse42.cxx")
CheckCompileLinkOption("-mssse3 -mpclmul" CRYPTOPP_IA32_CLMUL
"${TEST_PROG_DIR}/test_x86_clmul.cxx")
CheckCompileLinkOption("-msse4.1 -maes" CRYPTOPP_IA32_AES
"${TEST_PROG_DIR}/test_x86_aes.cxx")
CheckCompileLinkOption("-mavx" CRYPTOPP_IA32_AVX
"${TEST_PROG_DIR}/test_x86_avx.cxx")
CheckCompileLinkOption("-mavx2" CRYPTOPP_IA32_AVX2
"${TEST_PROG_DIR}/test_x86_avx2.cxx")
CheckCompileLinkOption("-msse4.2 -msha" CRYPTOPP_IA32_SHA
"${TEST_PROG_DIR}/test_x86_sha.cxx")
if (EXISTS "${TEST_PROG_DIR}/test_asm_mixed.cxx")
CheckCompileLinkOption("" CRYPTOPP_MIXED_ASM
"${TEST_PROG_DIR}/test_asm_mixed.cxx")
else ()
CheckCompileLinkOption("" CRYPTOPP_MIXED_ASM
"${TEST_PROG_DIR}/test_mixed_asm.cxx")
endif ()
if (NOT CRYPTOPP_MIXED_ASM)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_MIXED_ASM")
endif ()
if (NOT CRYPTOPP_IA32_SSE2 AND NOT CRYPTOPP_DISABLE_ASM)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_ASM")
elseif (CRYPTOPP_IA32_SSE2 AND NOT CRYPTOPP_DISABLE_ASM)
set_source_files_properties(${SRC_DIR}/sse_simd.cpp PROPERTIES COMPILE_FLAGS "-msse2")
endif ()
if (NOT CRYPTOPP_IA32_SSSE3 AND NOT CRYPTOPP_DISABLE_SSSE3)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_SSSE3")
elseif (CRYPTOPP_IA32_SSSE3 AND NOT CRYPTOPP_DISABLE_SSSE3)
if (NOT CRYPTOPP_IA32_SSE41 AND NOT CRYPTOPP_DISABLE_SSE4)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_SSE4")
endif ()
if (NOT CRYPTOPP_IA32_SSE42 AND NOT CRYPTOPP_DISABLE_SSE4)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_SSE4")
elseif (CRYPTOPP_IA32_SSE42 AND NOT CRYPTOPP_DISABLE_SSE4)
set_source_files_properties(${SRC_DIR}/crc_simd.cpp PROPERTIES COMPILE_FLAGS "-msse4.2")
if (NOT CRYPTOPP_IA32_CLMUL AND NOT CRYPTOPP_DISABLE_CLMUL)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_CLMUL")
elseif (CRYPTOPP_IA32_CLMUL AND NOT CRYPTOPP_DISABLE_CLMUL)
set_source_files_properties(${SRC_DIR}/gcm_simd.cpp PROPERTIES COMPILE_FLAGS "-mssse3 -mpclmul")
set_source_files_properties(${SRC_DIR}/gf2n_simd.cpp PROPERTIES COMPILE_FLAGS "-mpclmul")
endif ()
if (NOT CRYPTOPP_IA32_AES AND NOT CRYPTOPP_DISABLE_AES)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_AESNI")
elseif (CRYPTOPP_IA32_AES AND NOT CRYPTOPP_DISABLE_AES)
set_source_files_properties(${SRC_DIR}/rijndael_simd.cpp PROPERTIES COMPILE_FLAGS "-msse4.1 -maes")
endif ()
if (NOT CRYPTOPP_IA32_AVX2 AND NOT CRYPTOPP_DISABLE_AVX2)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_AVX2")
endif ()
if (NOT CRYPTOPP_IA32_SHA AND NOT CRYPTOPP_DISABLE_SHA)
list(APPEND CRYPTOPP_COMPILE_OPTIONS "-DCRYPTOPP_DISABLE_SHANI")
elseif (CRYPTOPP_IA32_SHA AND NOT CRYPTOPP_DISABLE_SHA)
set_source_files_properties(${SRC_DIR}/sha_simd.cpp PROPERTIES COMPILE_FLAGS "-msse4.2 -msha")
endif ()
endif ()
endif ()
endif ()
endif ()
#============================================================================
# Compile targets
#============================================================================
set(cryptopp_LIBRARY_SOURCES ${cryptopp_SOURCES_ASM})
list(APPEND cryptopp_LIBRARY_SOURCES ${cryptopp_SOURCES})
add_library(cryptopp STATIC ${cryptopp_LIBRARY_SOURCES})
target_compile_definitions(cryptopp PUBLIC ${CRYPTOPP_COMPILE_DEFINITIONS})
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COVERAGE_COMPILE_FLAGS}")
target_include_directories(cryptopp INTERFACE .)
#============================================================================
# Third-party libraries
#============================================================================
find_package(Threads)
target_link_libraries(cryptopp PRIVATE ${CMAKE_THREAD_LIBS_INIT})
if(ANDROID)
include(AndroidNdkModules)
android_ndk_import_module_cpufeatures()
target_link_libraries(cryptopp PRIVATE cpufeatures)
endif()

View File

@ -1,5 +1,5 @@
These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL loader generator and have been checked in as-is. You can re-generate them using glad with the following command:
```
python -m glad --profile core --out-path glad/ --api "gl=3.3,gles2=3.2" --generator=c
python -m glad --profile core --out-path glad/ --api "gl=4.6,gles2=3.2" --generator=c
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ android {
// TODO If this is ever modified, change application_id in strings.xml
applicationId "org.citra.citra_emu"
minSdkVersion 28
targetSdkVersion 29
targetSdkVersion 31
versionCode autoVersion
versionName getVersion()
ndk.abiFilters abiFilter
@ -96,7 +96,7 @@ android {
externalNativeBuild {
cmake {
version "3.18.1"
version "3.22.1"
path "../../../CMakeLists.txt"
}
}
@ -107,7 +107,8 @@ android {
arguments "-DENABLE_QT=0", // Don't use QT
"-DENABLE_SDL2=0", // Don't use SDL
"-DENABLE_WEB_SERVICE=0", // Don't use telemetry
"-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
"-DBUNDLE_SPEEX=ON"
abiFilters abiFilter
}
@ -116,22 +117,25 @@ android {
}
dependencies {
implementation "androidx.activity:activity:1.5.1"
implementation "androidx.fragment:fragment:1.5.5"
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.exifinterface:exifinterface:1.3.4'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.documentfile:documentfile:1.0.1"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.fragment:fragment:1.5.3'
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.0'
// For loading huge screenshots from the disk.
implementation 'com.squareup.picasso:picasso:2.71828'
// Allows FRP-style asynchronous operations in Android.
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation 'org.ini4j:ini4j:0.5.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
@ -139,6 +143,10 @@ dependencies {
// Please don't upgrade the billing library as the newer version is not GPL-compatible
implementation 'com.android.billingclient:billing:2.0.3'
// To use the androidx.test.core APIs
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test.ext:junit:1.1.5"
}
def getVersion() {

View File

@ -1,8 +1,8 @@
package org.citra.citra_emu;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -19,7 +19,7 @@ public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
Context appContext = ApplicationProvider.getApplicationContext();
assertEquals("org.citra.citra_emu", appContext.getPackageName());
}

View File

@ -7,21 +7,30 @@
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
<uses-feature android:name="android.hardware.opengles.aep" android:required="true" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
<uses-feature android:name="android.hardware.opengles.aep" android:required="true" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name="org.citra.citra_emu.CitraApplication"
android:label="@string/app_name"
@ -34,48 +43,38 @@
<activity
android:name="org.citra.citra_emu.ui.main.MainActivity"
android:theme="@style/CitraBase"
android:theme="@style/Theme.Citra.Splash.Main"
android:exported="true"
android:resizeableActivity="false">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="org.citra.citra_emu.features.settings.ui.SettingsActivity"
android:configChanges="orientation|screenSize|uiMode"
android:theme="@style/CitraSettingsBase"
android:theme="@style/Theme.Citra.Main"
android:label="@string/preferences_settings"/>
<activity
android:name="org.citra.citra_emu.activities.EmulationActivity"
android:resizeableActivity="false"
android:theme="@style/CitraEmulationBase"
android:theme="@style/Theme.Citra.Main"
android:launchMode="singleTop"/>
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
<activity
android:name="org.citra.citra_emu.activities.CustomFilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
android:exported="false"
android:theme="@style/CitraSettingsBase"
android:theme="@style/Theme.Citra.Main"
android:label="@string/cheats"/>
<service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
<provider
android:name="org.citra.citra_emu.model.GameProvider"
@ -83,16 +82,6 @@
android:enabled="true"
android:exported="false">
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.filesprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/nnf_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -12,10 +12,12 @@ 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() {
@ -39,6 +41,7 @@ public class CitraApplication extends Application {
public void onCreate() {
super.onCreate();
application = this;
documentsTree = new DocumentsTree();
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
DirectoryInitialization.start(getApplicationContext());

View File

@ -28,6 +28,7 @@ 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;
@ -38,6 +39,8 @@ 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.
@ -162,6 +165,10 @@ public final class NativeLibrary {
// 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();
/**
@ -245,7 +252,7 @@ public final class NativeLibrary {
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
return new AlertDialog.Builder(emulationActivity)
return new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.continue_button, (dialog, which) -> {
@ -260,11 +267,11 @@ public final class NativeLibrary {
coreErrorAlertLock.notify();
}
}).setOnDismissListener(dialog -> {
coreErrorAlertResult = true;
synchronized (coreErrorAlertLock) {
coreErrorAlertLock.notify();
}
}).create();
coreErrorAlertResult = true;
synchronized (coreErrorAlertLock) {
coreErrorAlertLock.notify();
}
}).create();
}
}
@ -345,7 +352,7 @@ public final class NativeLibrary {
} else {
// Create object used for waiting.
final Object lock = new Object();
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(caption)
.setMessage(text);
@ -427,7 +434,7 @@ public final class NativeLibrary {
return alertPromptResult;
}
public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
final EmulationActivity emulationActivity = sEmulationActivity.get();
alertPromptResult = "";
alertPromptButton = 0;
@ -444,7 +451,7 @@ public final class NativeLibrary {
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(alertPromptEditText);
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(caption)
.setView(container)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
@ -489,9 +496,6 @@ public final class NativeLibrary {
final int ErrorLoader_ErrorEncrypted = 5;
final int ErrorLoader_ErrorInvalidFormat = 6;
final int ErrorSystemFiles = 7;
final int ErrorVideoCore = 8;
final int ErrorVideoCore_ErrorGenericDrivers = 9;
final int ErrorVideoCore_ErrorBelowGL33 = 10;
final int ShutdownRequested = 11;
final int ErrorUnknown = 12;
@ -506,7 +510,7 @@ public final class NativeLibrary {
captionId = R.string.loader_error_encrypted;
}
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
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())
@ -663,4 +667,73 @@ public final class NativeLibrary {
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);
}
}

View File

@ -1,38 +0,0 @@
package org.citra.citra_emu.activities;
import android.content.Intent;
import android.os.Environment;
import androidx.annotation.Nullable;
import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.citra.citra_emu.fragments.CustomFilePickerFragment;
import java.io.File;
public class CustomFilePickerActivity extends FilePickerActivity {
public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
@Override
protected AbstractFilePickerFragment<File> getFragment(
@Nullable final String startPath, final int mode, final boolean allowMultiple,
final boolean allowCreateDir, final boolean allowExistingFile,
final boolean singleClick) {
CustomFilePickerFragment fragment = new CustomFilePickerFragment();
// startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
fragment.setArgs(
startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
Intent intent = getIntent();
int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
fragment.setTitle(title);
String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
fragment.setAllowedExtensions(allowedExtensions);
return fragment;
}
}

View File

@ -5,31 +5,35 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Pair;
import android.util.SparseIntArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.FragmentActivity;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.contracts.OpenFileResultContract;
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
@ -42,6 +46,7 @@ import org.citra.citra_emu.utils.EmulationMenuSettings;
import org.citra.citra_emu.utils.FileBrowserHelper;
import org.citra.citra_emu.utils.FileUtil;
import org.citra.citra_emu.utils.ForegroundService;
import org.citra.citra_emu.utils.ThemeUtil;
import java.io.File;
import java.io.IOException;
@ -53,6 +58,9 @@ import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.RECORD_AUDIO;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.slider.Slider;
public final class EmulationActivity extends AppCompatActivity {
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
@ -74,11 +82,24 @@ public final class EmulationActivity extends AppCompatActivity {
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
public static final int MENU_ACTION_OPEN_CHEATS = 17;
public static final int MENU_ACTION_CLOSE_GAME = 18;
public static final int REQUEST_SELECT_AMIIBO = 2;
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
registerForActivityResult(new OpenFileResultContract(), result -> {
if (result == null)
return;
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
result, getApplicationContext(), Collections.singletonList("bin"));
if (selectedFiles == null)
return;
onAmiiboSelected(selectedFiles[0]);
});
static {
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
@ -114,9 +135,10 @@ public final class EmulationActivity extends AppCompatActivity {
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
buttonsActionsMap
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
buttonsActionsMap
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
}
private View mDecorView;
private EmulationFragment mEmulationFragment;
private SharedPreferences mPreferences;
private ControllerMappingHelper mControllerMappingHelper;
@ -145,6 +167,8 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeUtil.applyTheme(this);
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
@ -160,21 +184,9 @@ public final class EmulationActivity extends AppCompatActivity {
mControllerMappingHelper = new ControllerMappingHelper();
// Get a handle to the Window containing the UI.
mDecorView = getWindow().getDecorView();
mDecorView.setOnSystemUiVisibilityChangeListener(visibility ->
{
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// Go back to immersive fullscreen mode in 3s
Handler handler = new Handler(getMainLooper());
handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */);
}
});
// Set these options now so that the SurfaceView the game renders into is the right size.
enableFullscreenImmersive();
setTheme(R.style.CitraEmulationBase);
setContentView(R.layout.activity_emulation);
// Find or create the EmulationFragment
@ -223,21 +235,12 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
NativeLibrary.PauseEmulation();
new AlertDialog.Builder(this)
.setTitle(R.string.emulation_close_game)
.setMessage(R.string.emulation_close_game_message)
.setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
{
mEmulationFragment.stopEmulation();
finish();
})
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
NativeLibrary.UnPauseEmulation())
.setOnCancelListener(dialogInterface ->
NativeLibrary.UnPauseEmulation())
.create()
.show();
View anchor = findViewById(R.id.menu_anchor);
PopupMenu popupMenu = new PopupMenu(this, anchor);
onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
updateSavestateMenuOptions(popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
popupMenu.show();
}
@Override
@ -246,7 +249,7 @@ public final class EmulationActivity extends AppCompatActivity {
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
shouldShowRequestPermissionRationale(CAMERA)) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.camera)
.setMessage(R.string.camera_permission_needed)
.setPositiveButton(android.R.string.ok, null)
@ -257,7 +260,7 @@ public final class EmulationActivity extends AppCompatActivity {
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.microphone)
.setMessage(R.string.microphone_permission_needed)
.setPositiveButton(android.R.string.ok, null)
@ -271,21 +274,30 @@ public final class EmulationActivity extends AppCompatActivity {
}
}
public void onEmulationStarted() {
Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
}
private void enableFullscreenImmersive() {
// It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
mDecorView.setSystemUiVisibility(
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE);
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_emulation, menu);
onCreateOptionsMenu(menu, getMenuInflater());
return true;
}
private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_emulation, menu);
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
@ -306,8 +318,6 @@ public final class EmulationActivity extends AppCompatActivity {
menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
return true;
}
private void DisplaySavestateWarning() {
@ -320,7 +330,7 @@ public final class EmulationActivity extends AppCompatActivity {
View view = inflater.inflate(R.layout.dialog_checkbox, null);
CheckBox checkBox = view.findViewById(R.id.checkBox);
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.savestate_warning_title)
.setMessage(R.string.savestate_warning_message)
.setView(view)
@ -333,12 +343,16 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
updateSavestateMenuOptions(menu);
return true;
}
private void updateSavestateMenuOptions(Menu menu) {
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
if (savestates == null) {
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
return true;
return;
}
menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
@ -367,7 +381,6 @@ public final class EmulationActivity extends AppCompatActivity {
saveStateMenu.getItem(info.slot - 1).setTitle(text);
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
}
return true;
}
@SuppressWarnings("WrongConstant")
@ -456,9 +469,7 @@ public final class EmulationActivity extends AppCompatActivity {
break;
case MENU_ACTION_LOAD_AMIIBO:
FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO,
R.string.select_amiibo,
Collections.singletonList("bin"), false);
mOpenFileLauncher.launch(false);
break;
case MENU_ACTION_REMOVE_AMIIBO:
@ -480,6 +491,21 @@ public final class EmulationActivity extends AppCompatActivity {
case MENU_ACTION_OPEN_CHEATS:
CheatsActivity.launch(this);
break;
case MENU_ACTION_CLOSE_GAME:
NativeLibrary.PauseEmulation();
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.emulation_close_game)
.setMessage(R.string.emulation_close_game_message)
.setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
{
mEmulationFragment.stopEmulation();
finish();
})
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.UnPauseEmulation())
.setOnCancelListener(dialogInterface -> NativeLibrary.UnPauseEmulation())
.show();
break;
}
return true;
@ -536,20 +562,8 @@ public final class EmulationActivity extends AppCompatActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
super.onActivityResult(requestCode, resultCode, result);
switch (requestCode) {
case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER:
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
break;
case REQUEST_SELECT_AMIIBO:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK) {
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result);
if (selectedFiles == null)
return;
onAmiiboSelected(selectedFiles[0]);
}
break;
if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
}
}
@ -564,11 +578,10 @@ public final class EmulationActivity extends AppCompatActivity {
}
if (!success) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.amiibo_load_error)
.setMessage(R.string.amiibo_load_error_message)
.setPositiveButton(android.R.string.ok, null)
.create()
.show();
}
}
@ -580,8 +593,6 @@ public final class EmulationActivity extends AppCompatActivity {
private void toggleControls() {
final SharedPreferences.Editor editor = mPreferences.edit();
boolean[] enabledButtons = new boolean[14];
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.emulation_toggle_controls);
for (int i = 0; i < enabledButtons.length; i++) {
// Buttons that are disabled by default
@ -596,63 +607,47 @@ public final class EmulationActivity extends AppCompatActivity {
enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
}
builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
(dialog, indexSelected, isChecked) -> editor
.putBoolean("buttonToggle" + indexSelected, isChecked));
builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
{
editor.apply();
mEmulationFragment.refreshInputOverlay();
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.emulation_toggle_controls)
.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
(dialog, indexSelected, isChecked) -> editor
.putBoolean("buttonToggle" + indexSelected, isChecked))
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
{
editor.apply();
mEmulationFragment.refreshInputOverlay();
})
.show();
}
private void adjustScale() {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.dialog_seekbar, null);
View view = inflater.inflate(R.layout.dialog_slider, null);
final SeekBar seekbar = view.findViewById(R.id.seekbar);
final TextView value = view.findViewById(R.id.text_value);
final Slider slider = view.findViewById(R.id.slider);
final TextView textValue = view.findViewById(R.id.text_value);
final TextView units = view.findViewById(R.id.text_units);
seekbar.setMax(150);
seekbar.setProgress(mPreferences.getInt("controlScale", 50));
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
value.setText(String.valueOf(progress + 50));
}
public void onStopTrackingTouch(SeekBar seekBar) {
setControlScale(seekbar.getProgress());
}
slider.setValueTo(150);
slider.setValue(mPreferences.getInt("controlScale", 50));
slider.addOnChangeListener((slider1, progress, fromUser) -> {
textValue.setText(String.valueOf((int) progress + 50));
setControlScale((int) slider1.getValue());
});
value.setText(String.valueOf(seekbar.getProgress() + 50));
textValue.setText(String.valueOf((int) slider.getValue() + 50));
units.setText("%");
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.emulation_control_scale);
builder.setView(view);
final int previousProgress = seekbar.getProgress();
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
setControlScale(previousProgress);
});
builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
{
setControlScale(seekbar.getProgress());
});
builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> {
setControlScale(50);
});
final int previousProgress = (int) slider.getValue();
AlertDialog alertDialog = builder.create();
alertDialog.show();
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.emulation_control_scale)
.setView(view)
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> setControlScale(previousProgress))
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> setControlScale((int) slider.getValue()))
.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> setControlScale(50))
.show();
}
private void setControlScale(int scale) {
@ -663,12 +658,10 @@ public final class EmulationActivity extends AppCompatActivity {
}
private void resetOverlay() {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.emulation_touch_overlay_reset))
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
})
.create()
.setNegativeButton(android.R.string.cancel, null)
.show();
}

View File

@ -2,8 +2,6 @@ package org.citra.citra_emu.adapters;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.view.LayoutInflater;
@ -12,20 +10,20 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.MaterialColors;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.model.GameDatabase;
import org.citra.citra_emu.ui.DividerItemDecoration;
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.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
/**
@ -88,8 +86,14 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textFileName.setText(gamePath.getFileName().toString());
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);
@ -99,9 +103,9 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
final int backgroundColorId = isValidGame(holder.path) ? R.color.card_view_background : R.color.card_view_disabled;
final int backgroundColorId = isValidGame(holder.path) ? R.attr.colorSurface : R.attr.colorErrorContainer;
View itemView = holder.getItemView();
itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
itemView.setBackgroundColor(MaterialColors.getColor(itemView, backgroundColorId));
} else {
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
}
@ -204,24 +208,6 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
}
public static class SpacesItemDecoration extends DividerItemDecoration {
private int space;
public SpacesItemDecoration(Drawable divider, int space) {
super(divider);
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
outRect.left = 0;
outRect.right = 0;
outRect.bottom = space;
outRect.top = 0;
}
}
private boolean isValidGame(String path) {
return Stream.of(
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));

View File

@ -22,6 +22,8 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public final class MiiSelector {
public static class MiiSelectorConfig implements java.io.Serializable {
public boolean enable_cancel_button;
@ -69,8 +71,8 @@ public final class MiiSelector {
? (int) config.initially_selected_mii_index
: 0;
data.index = initialIndex;
AlertDialog.Builder builder =
new AlertDialog.Builder(emulationActivity)
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(config.title.isEmpty()
? emulationActivity.getString(R.string.mii_selector)
: config.title)

View File

@ -19,6 +19,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
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;
@ -124,7 +126,7 @@ public final class SoftwareKeyboard {
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText);
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setView(container);
setCancelable(false);
@ -227,7 +229,7 @@ public final class SoftwareKeyboard {
break;
}
new AlertDialog.Builder(emulationActivity)
new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)

View File

@ -0,0 +1,24 @@
package org.citra.citra_emu.contracts;
import android.content.Context;
import android.content.Intent;
import android.util.Pair;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.setType("application/octet-stream")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
}
@Override
public Intent parseResult(int i, @Nullable Intent intent) {
return intent;
}
}

View File

@ -0,0 +1,91 @@
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;
}
}

View File

@ -0,0 +1,61 @@
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));
});
}
}

View File

@ -6,9 +6,7 @@ package org.citra.citra_emu.disk_shader_cache;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
@ -18,6 +16,8 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
@ -55,10 +55,10 @@ public class DiskShaderCacheProgress {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = Objects.requireNonNull(getActivity());
final Activity emulationActivity = requireActivity();
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
final String title = Objects.requireNonNull(requireArguments().getString("title"));
final String message = Objects.requireNonNull(requireArguments().getString("message"));
LayoutInflater inflater = LayoutInflater.from(emulationActivity);
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
@ -70,26 +70,21 @@ public class DiskShaderCacheProgress {
setCancelable(false);
setRetainInstance(true);
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity);
builder.setTitle(title);
builder.setMessage(message);
builder.setView(view);
builder.setNegativeButton(android.R.string.cancel, null);
dialog = builder.create();
dialog.create();
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v) -> emulationActivity.onBackPressed());
synchronized (finishLock) {
finishLock.notifyAll();
}
dialog = new MaterialAlertDialogBuilder(emulationActivity)
.setView(view)
.setTitle(title)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> emulationActivity.onBackPressed())
.create();
return dialog;
}
private void onUpdateProgress(String msg, int progress, int max) {
Objects.requireNonNull(getActivity()).runOnUiThread(() -> {
requireActivity().runOnUiThread(() -> {
progressBar.setProgress(progress);
progressBar.setMax(max);
progressText.setText(String.format("%d/%d", progress, max));
@ -133,6 +128,8 @@ public class DiskShaderCacheProgress {
case Complete:
// Workaround for when dialog is dismissed when the app is in the background
fragment.dismissAllowingStateLoss();
emulationActivity.runOnUiThread(emulationActivity::onEmulationStarted);
break;
}
}

View File

@ -11,10 +11,11 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
@ -80,12 +81,12 @@ public class CheatDetailsFragment extends Fragment {
private void onDeleteClicked(View view) {
String name = mEditName.getText().toString();
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setMessage(getString(R.string.cheats_delete_confirmation, name));
builder.setPositiveButton(android.R.string.yes,
(dialog, i) -> mViewModel.deleteSelectedCheat());
builder.setNegativeButton(android.R.string.no, null);
builder.show();
new MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.cheats_delete_confirmation, name))
.setPositiveButton(android.R.string.yes,
(dialog, i) -> mViewModel.deleteSelectedCheat())
.setNegativeButton(android.R.string.no, null)
.show();
}
private void onEditClicked(View view) {

View File

@ -7,6 +7,9 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -19,6 +22,9 @@ import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
import org.citra.citra_emu.ui.DividerItemDecoration;
public class CheatListFragment extends Fragment {
private RecyclerView mRecyclerView;
private FloatingActionButton mFab;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@ -28,19 +34,38 @@ public class CheatListFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
FloatingActionButton fab = view.findViewById(R.id.fab);
mRecyclerView = view.findViewById(R.id.cheat_list);
mFab = view.findViewById(R.id.fab);
CheatsActivity activity = (CheatsActivity) requireActivity();
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
fab.setOnClickListener(v -> {
mFab.setOnClickListener(v -> {
viewModel.startAddingCheat();
viewModel.openDetailsView();
});
setInsets();
}
private void setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
ViewGroup.MarginLayoutParams mlpFab =
(ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
mlpFab.leftMargin = insets.left + fabPadding;
mlpFab.bottomMargin = insets.bottom + fabPadding;
mlpFab.rightMargin = insets.right + fabPadding;
mFab.setLayoutParams(mlpFab);
return windowInsets;
});
}
}

View File

@ -10,14 +10,25 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
import org.citra.citra_emu.utils.InsetsHelper;
import org.citra.citra_emu.utils.ThemeUtil;
import java.util.List;
public class CheatsActivity extends AppCompatActivity
implements SlidingPaneLayout.PanelSlideListener {
@ -37,16 +48,20 @@ public class CheatsActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeUtil.applyTheme(this);
super.onCreate(savedInstanceState);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
mViewModel.load();
setContentView(R.layout.activity_cheats);
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
mCheatList = findViewById(R.id.cheat_list);
mCheatDetails = findViewById(R.id.cheat_details);
mCheatList = findViewById(R.id.cheat_list_container);
mCheatDetails = findViewById(R.id.cheat_details_container);
mCheatListLastFocus = mCheatList;
mCheatDetailsLastFocus = mCheatDetails;
@ -63,7 +78,11 @@ public class CheatsActivity extends AppCompatActivity
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
// Show "Up" button in the action bar for navigation
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setInsets();
}
@Override
@ -146,8 +165,7 @@ public class CheatsActivity extends AppCompatActivity
}
}
public static void setOnFocusChangeListenerRecursively(@NonNull View view,
View.OnFocusChangeListener listener) {
public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
view.setOnFocusChangeListener(listener);
if (view instanceof ViewGroup) {
@ -158,4 +176,56 @@ public class CheatsActivity extends AppCompatActivity
}
}
}
private void setInsets() {
AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
InsetsHelper.insetAppBar(barInsets, appBarLayout);
mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
// Set keyboard insets if the system supports smooth keyboard animations
ViewGroup.MarginLayoutParams mlpDetails =
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
if (keyboardInsets.bottom > 0) {
mlpDetails.bottomMargin = keyboardInsets.bottom;
} else {
mlpDetails.bottomMargin = barInsets.bottom;
}
} else {
if (mlpDetails.bottomMargin == 0) {
mlpDetails.bottomMargin = barInsets.bottom;
}
}
mCheatDetails.setLayoutParams(mlpDetails);
return windowInsets;
});
// Update the layout for every frame that the keyboard animates in
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
int keyboardInsets = 0;
int barInsets = 0;
@NonNull
@Override
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
ViewGroup.MarginLayoutParams mlpDetails =
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
mCheatDetails.setLayoutParams(mlpDetails);
return insets;
}
});
}
}
}

View File

@ -8,18 +8,28 @@ import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.FragmentTransaction;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
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.InsetsHelper;
import org.citra.citra_emu.utils.ThemeUtil;
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
private static final String ARG_MENU_TAG = "menu_tag";
@ -38,10 +48,13 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.applyTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
Intent launcher = getIntent();
String gameID = launcher.getStringExtra(ARG_GAME_ID);
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
@ -49,7 +62,11 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
mPresenter.onCreate(savedInstanceState, menuTag, gameID);
// Show "Back" button in the action bar for navigation
MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setInsets();
}
@Override
@ -107,10 +124,10 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
if (addToStack) {
if (areSystemAnimationsEnabled()) {
transaction.setCustomAnimations(
R.animator.settings_enter,
R.animator.settings_exit,
R.animator.settings_pop_enter,
R.animator.setttings_pop_exit);
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
0,
R.anim.anim_pop_settings_fragment_out);
}
transaction.addToBackStack(null);
@ -212,4 +229,14 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
private SettingsFragment getFragment() {
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
private void setInsets() {
AppBarLayout appBar = findViewById(R.id.appbar_settings);
FrameLayout frame = findViewById(R.id.frame_content);
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
InsetsHelper.insetAppBar(insets, appBar);
return windowInsets;
});
}
}

View File

@ -3,7 +3,9 @@ package org.citra.citra_emu.features.settings.ui;
import android.content.IntentFilter;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
@ -13,8 +15,6 @@ import org.citra.citra_emu.utils.DirectoryStateReceiver;
import org.citra.citra_emu.utils.Log;
import org.citra.citra_emu.utils.ThemeUtil;
import java.io.File;
public final class SettingsActivityPresenter {
private static final String KEY_SHOULD_SAVE = "should_save";
@ -60,8 +60,8 @@ public final class SettingsActivityPresenter {
}
private void prepareCitraDirectoriesIfNeeded() {
File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini");
if (!configFile.exists()) {
DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG);
if (configFile == null || !configFile.exists()) {
Log.error("Citra config file could not be found!");
}
if (DirectoryInitialization.areCitraDirectoriesReady()) {
@ -109,8 +109,6 @@ public final class SettingsActivityPresenter {
mSettings.saveSettings(mView);
}
ThemeUtil.applyTheme();
NativeLibrary.ReloadSettings();
}

View File

@ -6,13 +6,16 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.DatePicker;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.TimePicker;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.slider.Slider;
import org.citra.citra_emu.R;
import org.citra.citra_emu.dialogs.MotionAlertDialog;
import org.citra.citra_emu.features.settings.model.FloatSetting;
@ -41,15 +44,14 @@ import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener {
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener {
private SettingsFragmentView mView;
private Context mContext;
private ArrayList<SettingsItem> mSettings;
private SettingsItem mClickedItem;
private int mClickedPosition;
private int mSeekbarProgress;
private int mSliderProgress;
private AlertDialog mDialog;
private TextView mTextSliderValue;
@ -149,11 +151,9 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
int value = getSelectionForSingleChoiceValue(item);
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), value, this);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
@ -162,11 +162,9 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
int value = getSelectionForSingleChoiceValue(item);
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), value, this);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
@ -199,11 +197,9 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
mClickedItem = item;
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
mDialog = builder.show();
}
@ -226,8 +222,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
mClickedItem = item;
mClickedPosition = position;
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
@ -265,44 +259,45 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
closeDialog();
};
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, ok);
builder.setNegativeButton(android.R.string.cancel, defaultCancelListener);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setView(view)
.setPositiveButton(android.R.string.ok, ok)
.setNegativeButton(android.R.string.cancel, defaultCancelListener);
mDialog = builder.show();
}
public void onSliderClick(SliderSetting item, int position) {
mClickedItem = item;
mClickedPosition = position;
mSeekbarProgress = item.getSelectedValue();
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
mSliderProgress = item.getSelectedValue();
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.dialog_seekbar, null);
View view = inflater.inflate(R.layout.dialog_slider, null);
SeekBar seekbar = view.findViewById(R.id.seekbar);
Slider slider = view.findViewById(R.id.slider);
builder.setTitle(item.getNameId());
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, this);
builder.setNegativeButton(android.R.string.cancel, defaultCancelListener);
builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
seekbar.setProgress(item.getDefaultValue());
onClick(dialog, which);
});
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setView(view)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
slider.setValue(item.getDefaultValue());
onClick(dialog, which);
});
mDialog = builder.show();
mTextSliderValue = view.findViewById(R.id.text_value);
mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
mTextSliderValue.setText(String.valueOf(mSliderProgress));
TextView units = view.findViewById(R.id.text_units);
units.setText(item.getUnits());
seekbar.setMin(item.getMin());
seekbar.setMax(item.getMax());
seekbar.setProgress(mSeekbarProgress);
slider.setValueFrom(item.getMin());
slider.setValueTo(item.getMax());
slider.setValue(mSliderProgress);
seekbar.setOnSeekBarChangeListener(this);
slider.addOnChangeListener(this);
}
public void onSubmenuClick(SubmenuSetting item) {
@ -375,19 +370,19 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
closeDialog();
} else if (mClickedItem instanceof SliderSetting) {
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
if (sliderSetting.getSelectedValue() != mSeekbarProgress) {
if (sliderSetting.getSelectedValue() != mSliderProgress) {
mView.onSettingChanged();
}
if (sliderSetting.getSetting() instanceof FloatSetting) {
float value = (float) mSeekbarProgress;
float value = (float) mSliderProgress;
FloatSetting setting = sliderSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
} else {
IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress);
IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress);
if (setting != null) {
mView.putSetting(setting);
}
@ -397,7 +392,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
}
mClickedItem = null;
mSeekbarProgress = -1;
mSliderProgress = -1;
}
public void closeDialog() {
@ -411,20 +406,6 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mSeekbarProgress = progress;
mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
int valuesId = item.getValuesId();
@ -484,4 +465,10 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
return -1;
}
@Override
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
mSliderProgress = (int) value;
mTextSliderValue.setText(String.valueOf(mSliderProgress));
}
}

View File

@ -8,6 +8,9 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -29,6 +32,8 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
private SettingsAdapter mAdapter;
private RecyclerView mRecyclerView;
public static Fragment newInstance(String menuTag, String gameId) {
SettingsFragment fragment = new SettingsFragment();
@ -71,15 +76,17 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
RecyclerView recyclerView = view.findViewById(R.id.list_settings);
mRecyclerView = view.findViewById(R.id.list_settings);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(manager);
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(manager);
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
SettingsActivityView activity = (SettingsActivityView) getActivity();
mPresenter.onViewCreated(activity.getSettings());
setInsets();
}
@Override
@ -133,4 +140,12 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
public void onSettingChanged() {
mActivity.onSettingChanged();
}
private void setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(insets.left, 0, insets.right, insets.bottom);
return windowInsets;
});
}
}

View File

@ -355,6 +355,7 @@ public final class SettingsFragmentPresenter {
mView.getActivity().setTitle(R.string.preferences_graphics);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API);
Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
@ -371,6 +372,7 @@ public final class SettingsFragmentPresenter {
//Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi));
sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
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));
@ -409,14 +411,14 @@ public final class SettingsFragmentPresenter {
SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER);
Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG);
sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView));
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_RENDERER, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug));
}
}

View File

@ -1,48 +0,0 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* FrameLayout subclass with few Properties added to simplify animations.
* Don't remove the methods appearing as unused, in order not to break the menu animations
*/
public final class SettingsFrameLayout extends FrameLayout {
private float mVisibleness = 1.0f;
public SettingsFrameLayout(Context context) {
super(context);
}
public SettingsFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public float getYFraction() {
return getY() / getHeight();
}
public void setYFraction(float yFraction) {
final int height = getHeight();
setY((height > 0) ? (yFraction * height) : -9999);
}
public float getVisibleness() {
return mVisibleness;
}
public void setVisibleness(float visibleness) {
setScaleX(visibleness);
setScaleY(visibleness);
setAlpha(visibleness);
}
}

View File

@ -1,6 +1,10 @@
package org.citra.citra_emu.features.settings.utils;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
@ -18,10 +22,11 @@ import org.citra.citra_emu.utils.Log;
import org.ini4j.Wini;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeMap;
@ -39,7 +44,8 @@ public final class SettingsFile {
public static final String KEY_PREMIUM = "premium";
public static final String KEY_HW_RENDERER = "use_hw_renderer";
public static final String KEY_GRAPHICS_API = "graphics_api";
public static final String KEY_RENDERER_DEBUG = "renderer_debug";
public static final String KEY_HW_SHADER = "use_hw_shader";
public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul";
public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
@ -145,13 +151,15 @@ public final class SettingsFile {
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
static HashMap<String, SettingSection> readFile(final File ini, boolean isCustomGame, SettingsActivityView view) {
static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) {
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(ini));
Context context = CitraApplication.getAppContext();
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
reader = new BufferedReader(new InputStreamReader(inputStream));
SettingSection current = null;
for (String line; (line = reader.readLine()) != null; ) {
@ -166,11 +174,11 @@ public final class SettingsFile {
}
}
} catch (FileNotFoundException e) {
Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage());
Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} catch (IOException e) {
Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage());
Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} finally {
@ -178,7 +186,7 @@ public final class SettingsFile {
try {
reader.close();
} catch (IOException e) {
Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage());
Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage());
}
}
}
@ -212,17 +220,23 @@ public final class SettingsFile {
*/
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
SettingsActivityView view) {
File ini = getSettingsFile(fileName);
DocumentFile ini = getSettingsFile(fileName);
try {
Wini writer = new Wini(ini);
Context context = CitraApplication.getAppContext();
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
Wini writer = new Wini(inputStream);
Set<String> keySet = sections.keySet();
for (String key : keySet) {
SettingSection section = sections.get(key);
writeSection(writer, section);
}
writer.store();
inputStream.close();
OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri());
writer.store(outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
@ -262,14 +276,16 @@ public final class SettingsFile {
return generalSectionName;
}
@NonNull
private static File getSettingsFile(String fileName) {
return new File(
DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini");
public static DocumentFile getSettingsFile(String fileName) {
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
DocumentFile configDirectory = root.findFile("config");
return configDirectory.findFile(fileName + ".ini");
}
private static File getCustomGameSettingsFile(String gameId) {
return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini");
private static DocumentFile getCustomGameSettingsFile(String gameId) {
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
DocumentFile configDirectory = root.findFile("GameSettings");
return configDirectory.findFile(gameId + ".ini");
}
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {

View File

@ -1,120 +0,0 @@
package org.citra.citra_emu.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider;
import com.nononsenseapps.filepicker.FilePickerFragment;
import org.citra.citra_emu.R;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class CustomFilePickerFragment extends FilePickerFragment {
private static String ALL_FILES = "*";
private int mTitle;
private static List<String> extensions = Collections.singletonList(ALL_FILES);
@NonNull
@Override
public Uri toUri(@NonNull final File file) {
return FileProvider
.getUriForFile(getContext(),
getContext().getApplicationContext().getPackageName() + ".filesprovider",
file);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mode == MODE_DIR) {
TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
ok.setText(R.string.select_dir);
TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
cancel.setVisibility(View.GONE);
}
}
@Override
protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
View view = super.inflateRootView(inflater, container);
if (mTitle != 0) {
Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
ViewGroup parent = (ViewGroup) toolbar.getParent();
int index = parent.indexOfChild(toolbar);
View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
TextView title = newToolbar.findViewById(R.id.filepicker_title);
title.setText(mTitle);
parent.removeView(toolbar);
parent.addView(newToolbar, index);
}
return view;
}
public void setTitle(int title) {
mTitle = title;
}
public void setAllowedExtensions(String allowedExtensions) {
if (allowedExtensions == null)
return;
extensions = Arrays.asList(allowedExtensions.split(","));
}
@Override
protected boolean isItemVisible(@NonNull final File file) {
// Some users jump to the conclusion that Dolphin isn't able to detect their
// files if the files don't show up in the file picker when mode == MODE_DIR.
// To avoid this, show files even when the user needs to select a directory.
return (showHiddenItems || !file.isHidden()) &&
(file.isDirectory() || extensions.contains(ALL_FILES) ||
extensions.contains(fileExtension(file.getName()).toLowerCase()));
}
@Override
public boolean isCheckable(@NonNull final File file) {
// We need to make a small correction to the isCheckable logic due to
// overriding isItemVisible to show files when mode == MODE_DIR.
// AbstractFilePickerFragment always treats files as checkable when
// allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
}
@Override
public void goUp() {
if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
goToDir(new File("/storage/"));
return;
}
if (mCurrentPath.equals(new File("/storage/"))){
return;
}
super.goUp();
}
@Override
public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
if(viewHolder.file.equals(new File("/storage/emulated/")))
viewHolder.file = new File("/storage/emulated/0/");
super.onClickDir(view, viewHolder);
}
private static String fileExtension(@NonNull String filename) {
int i = filename.lastIndexOf('.');
return i < 0 ? "" : filename.substring(i + 1);
}
}

View File

@ -0,0 +1,36 @@
package org.citra.citra_emu.model;
import android.net.Uri;
import android.provider.DocumentsContract;
/**
* A struct that is much more "cheaper" than DocumentFile.
* Only contains the information we needed.
*/
public class CheapDocument {
private final String filename;
private final Uri uri;
private final String mimeType;
public CheapDocument(String filename, String mimeType, Uri uri) {
this.filename = filename;
this.mimeType = mimeType;
this.uri = uri;
}
public String getFilename() {
return filename;
}
public Uri getUri() {
return uri;
}
public String getMimeType() {
return mimeType;
}
public boolean isDirectory() {
return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
}
}

View File

@ -5,8 +5,10 @@ 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;
@ -64,10 +66,12 @@ public final class GameDatabase extends SQLiteOpenHelper {
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
@ -121,9 +125,8 @@ public final class GameDatabase extends SQLiteOpenHelper {
while (fileCursor.moveToNext()) {
String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
File game = new File(gamePath);
if (!game.exists()) {
if (!FileUtil.Exists(mContext, gamePath)) {
Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
gamePath);
database.delete(TABLE_NAME_GAMES,
@ -151,9 +154,10 @@ public final class GameDatabase extends SQLiteOpenHelper {
while (folderCursor.moveToNext()) {
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
File folder = new File(folderPath);
Uri folder = Uri.parse(folderPath);
// If the folder is empty because it no longer exists, remove it from the library.
if (!folder.exists()) {
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,
@ -161,7 +165,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
}
addGamesRecursive(database, folder, allowedExtensions, 3);
addGamesRecursive(database, files, allowedExtensions, 3);
}
fileCursor.close();
@ -173,33 +177,28 @@ public final class GameDatabase extends SQLiteOpenHelper {
database.close();
}
private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
private void addGamesRecursive(SQLiteDatabase database, CheapDocument[] files,
Set<String> allowedExtensions, int depth) {
if (depth <= 0) {
return;
}
File[] children = parent.listFiles();
if (children != null) {
for (File file : children) {
if (file.isHidden()) {
continue;
}
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();
if (file.isDirectory()) {
Set<String> newExtensions = new HashSet<>(Arrays.asList(
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
addGamesRecursive(database, file, newExtensions, depth - 1);
} else {
String filePath = file.getPath();
int extensionStart = filename.lastIndexOf('.');
if (extensionStart > 0) {
String fileExtension = filename.substring(extensionStart);
int extensionStart = filePath.lastIndexOf('.');
if (extensionStart > 0) {
String fileExtension = filePath.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, filePath);
}
// 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);
}
}
}

View File

@ -1,6 +1,8 @@
package org.citra.citra_emu.ui;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
@ -32,6 +34,13 @@ public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
@Override
public void onPanelClosed(@NonNull View panel) {
closeKeyboard();
setEnabled(false);
}
private void closeKeyboard() {
InputMethodManager manager = (InputMethodManager) mSlidingPaneLayout.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
manager.hideSoftInputFromWindow(mSlidingPaneLayout.getRootView().getWindowToken(), 0);
}
}

View File

@ -1,35 +1,46 @@
package org.citra.citra_emu.ui.main;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
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.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 com.google.android.material.appbar.AppBarLayout;
import org.citra.citra_emu.NativeLibrary;
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.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;
import java.util.Arrays;
import java.util.Collections;
/**
* 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.
@ -46,13 +57,72 @@ public final class MainActivity extends AppCompatActivity implements MainView {
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> mOpenFileLauncher =
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;
}
NativeLibrary.InstallCIAS(selectedFiles);
mPresenter.refreshGameList();
});
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeUtil.applyTheme();
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);
@ -61,7 +131,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
mPresenter.onCreate();
if (savedInstanceState == null) {
StartupHandler.HandleInit(this);
StartupHandler.HandleInit(this, mOpenCitraDirectory);
if (PermissionsHandler.hasWriteAccess(this)) {
mPlatformGamesFragment = new PlatformGamesFragment();
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
@ -77,6 +147,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
// Dismiss previous notifications (should not happen unless a crash occurred)
EmulationActivity.tryDismissRunningNotification(this);
setInsets();
}
@Override
@ -144,7 +216,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
if (PermissionsHandler.hasWriteAccess(this)) {
SettingsActivity.launch(this, menuTag, "");
} else {
PermissionsHandler.checkWritePermission(this);
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
}
}
@ -152,79 +224,18 @@ public final class MainActivity extends AppCompatActivity implements MainView {
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:
FileBrowserHelper.openDirectoryPicker(this,
MainPresenter.REQUEST_ADD_DIRECTORY,
R.string.select_game_folder,
Arrays.asList("elf", "axf", "cci", "3ds",
"cxi", "app", "3dsx", "cia",
"rar", "zip", "7z", "torrent",
"tar", "gz"));
break;
mOpenGameListLauncher.launch(null);
break;
case MainPresenter.REQUEST_INSTALL_CIA:
FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA,
R.string.install_cia_title,
Collections.singletonList("cia"), true);
break;
mOpenFileLauncher.launch(true);
break;
}
} else {
PermissionsHandler.checkWritePermission(this);
}
}
/**
* @param requestCode An int describing whether the Activity that is returning did so successfully.
* @param resultCode An int describing what Activity is giving us this callback.
* @param result The information the returning Activity is providing us.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
super.onActivityResult(requestCode, resultCode, result);
switch (requestCode) {
case MainPresenter.REQUEST_ADD_DIRECTORY:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK) {
// 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(FileBrowserHelper.getSelectedDirectory(result));
}
break;
case MainPresenter.REQUEST_INSTALL_CIA:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK) {
NativeLibrary.InstallCIAS(FileBrowserHelper.getSelectedFiles(result));
mPresenter.refeshGameList();
}
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
DirectoryInitialization.start(this);
mPlatformGamesFragment = new PlatformGamesFragment();
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
.commit();
// Immediately prompt user to select a game directory on first boot
if (mPresenter != null) {
mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
}
} else {
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
}
}
@ -245,6 +256,18 @@ public final class MainActivity extends AppCompatActivity implements MainView {
}
}
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);
@ -266,4 +289,15 @@ public final class MainActivity extends AppCompatActivity implements MainView {
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;
});
}
}

View File

@ -1,5 +1,6 @@
package org.citra.citra_emu.ui.main;
import android.content.Context;
import android.os.SystemClock;
import org.citra.citra_emu.BuildConfig;
@ -9,10 +10,12 @@ 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;
@ -25,7 +28,7 @@ public final class MainPresenter {
public void onCreate() {
String versionName = BuildConfig.VERSION_NAME;
mView.setVersionString(versionName);
refeshGameList();
refreshGameList();
}
public void launchFileListActivity(int request) {
@ -46,6 +49,10 @@ public final class MainPresenter {
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;
@ -74,9 +81,12 @@ public final class MainPresenter {
mDirToAdd = dir;
}
public void refeshGameList() {
GameDatabase databaseHelper = CitraApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mView.refresh();
public void refreshGameList() {
Context context = CitraApplication.getAppContext();
if (PermissionsHandler.hasWriteAccess(context)) {
GameDatabase databaseHelper = CitraApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mView.refresh();
}
}
}

View File

@ -7,12 +7,18 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
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;
@ -49,7 +55,9 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1));
MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL);
divider.setLastItemDecorated(false);
mRecyclerView.addItemDecoration(divider);
// Add swipe down to refresh gesture
final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games);
@ -59,6 +67,11 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
refresh();
pullToRefresh.setRefreshing(false);
});
pullToRefresh.setProgressBackgroundColorSchemeColor(MaterialColors.getColor(pullToRefresh, R.attr.colorPrimary));
pullToRefresh.setColorSchemeColors(MaterialColors.getColor(pullToRefresh, R.attr.colorOnPrimary));
setInsets();
}
@Override
@ -83,4 +96,12 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
mRecyclerView = root.findViewById(R.id.grid_games);
mTextView = root.findViewById(R.id.gamelist_empty_text);
}
private void setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(0, 0, 0, insets.bottom);
return windowInsets;
});
}
}

View File

@ -0,0 +1,87 @@
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);
}
}

View File

@ -9,19 +9,18 @@ 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 org.citra.citra_emu.NativeLibrary;
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
@ -49,6 +48,9 @@ public final class DirectoryInitialization {
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 {
@ -75,6 +77,11 @@ public final class DirectoryInitialization {
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!");
@ -88,15 +95,11 @@ public final class DirectoryInitialization {
private static native void SetSysDirectory(String path);
private static boolean setCitraUserDirectory() {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File externalPath = Environment.getExternalStorageDirectory();
if (externalPath != null) {
userPath = externalPath.getAbsolutePath() + "/citra-emu";
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
// NativeLibrary.SetUserDirectory(userPath);
return true;
}
Uri dataPath = PermissionsHandler.getCitraDirectory();
if (dataPath != null) {
userPath = dataPath.toString();
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
return true;
}
return false;

View File

@ -0,0 +1,271 @@
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());
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);
}
}
}

View File

@ -1,71 +1,48 @@
package org.citra.citra_emu.utils;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.documentfile.provider.DocumentFile;
import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.Utils;
import org.citra.citra_emu.activities.CustomFilePickerActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public final class FileBrowserHelper {
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
Intent i = new Intent(activity, CustomFilePickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().getPath());
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
activity.startActivityForResult(i, requestCode);
}
public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
List<String> extensions, boolean allowMultiple) {
Intent i = new Intent(activity, CustomFilePickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
Environment.getExternalStorageDirectory().getPath());
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
activity.startActivityForResult(i, requestCode);
}
@Nullable
public static String getSelectedDirectory(Intent result) {
// Use the provided utility method to parse the result
List<Uri> files = Utils.getSelectedFilesFromResult(result);
if (!files.isEmpty()) {
File file = Utils.getFileForUri(files.get(0));
return file.getAbsolutePath();
public static String[] getSelectedFiles(Intent result, Context context, List<String> extension) {
ClipData clipData = result.getClipData();
List<DocumentFile> files = new ArrayList<>();
if (clipData == null) {
files.add(DocumentFile.fromSingleUri(context, result.getData()));
} else {
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
Uri uri = item.getUri();
files.add(DocumentFile.fromSingleUri(context, uri));
}
}
return null;
}
@Nullable
public static String[] getSelectedFiles(Intent result) {
// Use the provided utility method to parse the result
List<Uri> files = Utils.getSelectedFilesFromResult(result);
if (!files.isEmpty()) {
String[] paths = new String[files.size()];
for (int i = 0; i < files.size(); i++)
paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
return paths;
List<String> filePaths = new ArrayList<>();
for (int i = 0; i < files.size(); i++) {
DocumentFile file = files.get(i);
String filename = file.getName();
int extensionStart = filename.lastIndexOf('.');
if (extensionStart > 0) {
String fileExtension = filename.substring(extensionStart + 1);
if (extension.contains(fileExtension)) {
filePaths.add(file.getUri().toString());
}
}
}
if (filePaths.isEmpty()) {
return null;
}
return filePaths.toArray(new String[0]);
}
return null;

View File

@ -1,11 +1,385 @@
package org.citra.citra_emu.utils;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.system.Os;
import android.system.StructStatVfs;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import org.citra.citra_emu.model.CheapDocument;
public class FileUtil {
static final String PATH_TREE = "tree";
static final String DECODE_METHOD = "UTF-8";
static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
static final String TEXT_PLAIN = "text/plain";
public interface CopyDirListener {
void onSearchProgress(String directoryName);
void onCopyProgress(String filename, int progress, int max);
void onComplete();
}
/**
* Create a file from directory with filename.
*
* @param context Application context
* @param directory parent path for file.
* @param filename file display name.
* @return boolean
*/
@Nullable
public static DocumentFile createFile(Context context, String directory, String filename) {
try {
Uri directoryUri = Uri.parse(directory);
DocumentFile parent;
parent = DocumentFile.fromTreeUri(context, directoryUri);
if (parent == null) return null;
filename = URLDecoder.decode(filename, DECODE_METHOD);
int extensionPosition = filename.lastIndexOf('.');
String extension = "";
if (extensionPosition > 0) {
extension = filename.substring(extensionPosition);
}
String mimeType = APPLICATION_OCTET_STREAM;
if (extension.equals(".txt")) {
mimeType = TEXT_PLAIN;
}
DocumentFile isExist = parent.findFile(filename);
if (isExist != null) return isExist;
return parent.createFile(mimeType, filename);
} catch (Exception e) {
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
}
return null;
}
/**
* Create a directory from directory with filename.
*
* @param context Application context
* @param directory parent path for directory.
* @param directoryName directory display name.
* @return boolean
*/
@Nullable
public static DocumentFile createDir(Context context, String directory, String directoryName) {
try {
Uri directoryUri = Uri.parse(directory);
DocumentFile parent;
parent = DocumentFile.fromTreeUri(context, directoryUri);
if (parent == null) return null;
directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
DocumentFile isExist = parent.findFile(directoryName);
if (isExist != null) return isExist;
return parent.createDirectory(directoryName);
} catch (Exception e) {
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
}
return null;
}
/**
* Open content uri and return file descriptor to JNI.
*
* @param context Application context
* @param path Native content uri path
* @param openmode will be one of "r", "r", "rw", "wa", "rwa"
* @return file descriptor
*/
public static int openContentUri(Context context, String path, String openmode) {
try (ParcelFileDescriptor parcelFileDescriptor =
context.getContentResolver().openFileDescriptor(Uri.parse(path), openmode)) {
if (parcelFileDescriptor == null) {
Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
return -1;
}
return parcelFileDescriptor.detachFd();
} catch (Exception e) {
Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
}
return -1;
}
/**
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
* This function will be faster than DocumentFile.listFiles
*
* @param context Application context
* @param uri Directory uri.
* @return CheapDocument lists.
*/
public static CheapDocument[] listFiles(Context context, Uri uri) {
final ContentResolver resolver = context.getContentResolver();
final String[] columns = new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
};
Cursor c = null;
final List<CheapDocument> results = new ArrayList<>();
try {
String docId;
if (isRootTreeUri(uri)) {
docId = DocumentsContract.getTreeDocumentId(uri);
} else {
docId = DocumentsContract.getDocumentId(uri);
}
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
c = resolver.query(childrenUri, columns, null, null, null);
while (c.moveToNext()) {
final String documentId = c.getString(0);
final String documentName = c.getString(1);
final String documentMimeType = c.getString(2);
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
CheapDocument document = new CheapDocument(documentName, documentMimeType, documentUri);
results.add(document);
}
} catch (Exception e) {
Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
} finally {
closeQuietly(c);
}
return results.toArray(new CheapDocument[0]);
}
/**
* Check whether given path exists.
*
* @param path Native content uri path
* @return bool
*/
public static boolean Exists(Context context, String path) {
Cursor c = null;
try {
Uri mUri = Uri.parse(path);
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DOCUMENT_ID};
c = context.getContentResolver().query(mUri, columns, null, null, null);
return c.getCount() > 0;
} catch (Exception e) {
Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
} finally {
closeQuietly(c);
}
return false;
}
/**
* Check whether given path is a directory
*
* @param path content uri path
* @return bool
*/
public static boolean isDirectory(Context context, String path) {
final ContentResolver resolver = context.getContentResolver();
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_MIME_TYPE};
boolean isDirectory = false;
Cursor c = null;
try {
Uri mUri = Uri.parse(path);
c = resolver.query(mUri, columns, null, null, null);
c.moveToNext();
final String mimeType = c.getString(0);
isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
} catch (Exception e) {
Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
} finally {
closeQuietly(c);
}
return isDirectory;
}
/**
* Get file display name from given path
*
* @param path content uri path
* @return String display name
*/
public static String getFilename(Context context, String path) {
final ContentResolver resolver = context.getContentResolver();
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
String filename = "";
Cursor c = null;
try {
Uri mUri = Uri.parse(path);
c = resolver.query(mUri, columns, null, null, null);
c.moveToNext();
filename = c.getString(0);
} catch (Exception e) {
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
} finally {
closeQuietly(c);
}
return filename;
}
public static String[] getFilesName(Context context, String path) {
Uri uri = Uri.parse(path);
List<String> files = new ArrayList<>();
for (CheapDocument file : FileUtil.listFiles(context, uri)) {
files.add(file.getFilename());
}
return files.toArray(new String[0]);
}
/**
* Get file size from given path.
*
* @param path content uri path
* @return long file size
*/
public static long getFileSize(Context context, String path) {
final ContentResolver resolver = context.getContentResolver();
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_SIZE};
long size = 0;
Cursor c = null;
try {
Uri mUri = Uri.parse(path);
c = resolver.query(mUri, columns, null, null, null);
c.moveToNext();
size = c.getLong(0);
} catch (Exception e) {
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
} finally {
closeQuietly(c);
}
return size;
}
public static boolean copyFile(Context context, String sourcePath, String destinationParentPath, String destinationFilename) {
try {
Uri sourceUri = Uri.parse(sourcePath);
Uri destinationUri = Uri.parse(destinationParentPath);
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationUri);
if (destinationParent == null) return false;
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
DocumentFile destination = destinationParent.findFile(filename);
if (destination == null) {
destination = destinationParent.createFile("application/octet-stream", filename);
}
if (destination == null) return false;
InputStream input = context.getContentResolver().openInputStream(sourceUri);
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri());
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
input.close();
output.flush();
output.close();
return true;
} catch (Exception e) {
Log.error("[FileUtil]: Cannot copy file, error: " + e.getMessage());
}
return false;
}
public static void copyDir(Context context, String sourcePath, String destinationPath,
CopyDirListener listener) {
try {
Uri sourceUri = Uri.parse(sourcePath);
Uri destinationUri = Uri.parse(destinationPath);
final List<Pair<CheapDocument, DocumentFile>> files = new ArrayList<>();
final List<Pair<Uri, Uri>> dirs = new ArrayList<>();
dirs.add(new Pair<>(sourceUri, destinationUri));
// Searching all files which need to be copied and struct the directory in destination.
while (!dirs.isEmpty()) {
DocumentFile fromDir = DocumentFile.fromTreeUri(context, dirs.get(0).first);
DocumentFile toDir = DocumentFile.fromTreeUri(context, dirs.get(0).second);
if (fromDir == null || toDir == null)
continue;
Uri fromUri = fromDir.getUri();
if (listener != null) {
listener.onSearchProgress(fromUri.getPath());
}
CheapDocument[] documents = FileUtil.listFiles(context, fromUri);
for (CheapDocument document : documents) {
String filename = document.getFilename();
if (document.isDirectory()) {
DocumentFile target = toDir.findFile(filename);
if (target == null || !target.exists()) {
target = toDir.createDirectory(filename);
}
if (target == null)
continue;
dirs.add(new Pair<>(document.getUri(), target.getUri()));
} else {
DocumentFile target = toDir.findFile(filename);
if (target == null || !target.exists()) {
target =
toDir.createFile(document.getMimeType(), document.getFilename());
}
if (target == null)
continue;
files.add(new Pair<>(document, target));
}
}
dirs.remove(0);
}
int total = files.size();
int progress = 0;
for (Pair<CheapDocument, DocumentFile> file : files) {
DocumentFile to = file.second;
Uri toUri = to.getUri();
String filename = getFilenameWithExtensions(toUri);
String toPath = toUri.getPath();
DocumentFile toParent = to.getParentFile();
if (toParent == null)
continue;
FileUtil.copyFile(context, file.first.getUri().toString(),
toParent.getUri().toString(), filename);
progress++;
if (listener != null) {
listener.onCopyProgress(toPath, progress, total);
}
}
if (listener != null) {
listener.onComplete();
}
} catch (Exception e) {
Log.error("[FileUtil]: Cannot copy directory, error: " + e.getMessage());
}
}
public static boolean renameFile(Context context, String path, String destinationFilename) {
try {
Uri uri = Uri.parse(path);
DocumentsContract.renameDocument(context.getContentResolver(), uri, destinationFilename);
return true;
} catch (Exception e) {
Log.error("[FileUtil]: Cannot rename file, error: " + e.getMessage());
}
return false;
}
public static boolean deleteDocument(Context context, String path) {
try {
Uri uri = Uri.parse(path);
DocumentsContract.deleteDocument(context.getContentResolver(), uri);
return true;
} catch (Exception e) {
Log.error("[FileUtil]: Cannot delete document, error: " + e.getMessage());
}
return false;
}
public static byte[] getBytesFromFile(File file) throws IOException {
final long length = file.length();
@ -21,8 +395,8 @@ public class FileUtil {
int numRead;
try (InputStream is = new FileInputStream(file)) {
while (offset < bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
while (offset < bytes.length &&
(numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
}
@ -34,4 +408,53 @@ public class FileUtil {
return bytes;
}
public static boolean isRootTreeUri(Uri uri) {
final List<String> paths = uri.getPathSegments();
return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
}
public static boolean isNativePath(String path) {
try {
return path.charAt(0) == '/';
} catch (StringIndexOutOfBoundsException e) {
Log.error("[FileUtil] Cannot determine the string is native path or not.");
}
return false;
}
public static String getFilenameWithExtensions(Uri uri) {
final String path = uri.getPath();
final int index = path.lastIndexOf('/');
return path.substring(index + 1);
}
public static double getFreeSpace(Context context, Uri uri) {
try {
Uri docTreeUri = DocumentsContract.buildDocumentUriUsingTree(
uri, DocumentsContract.getTreeDocumentId(uri));
ParcelFileDescriptor pfd =
context.getContentResolver().openFileDescriptor(docTreeUri, "r");
assert pfd != null;
StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor());
double spaceInGigaBytes = stats.f_bavail * stats.f_bsize / 1024.0 / 1024 / 1024;
pfd.close();
return spaceInGigaBytes;
} catch (Exception e) {
Log.error("[FileUtil] Cannot get storage size.");
}
return 0;
}
public static void closeQuietly(AutoCloseable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
}

View File

@ -27,7 +27,7 @@ public class ForegroundService extends Service {
private void showRunningNotification() {
// Intent is used to resume emulation if the notification is clicked
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, EmulationActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
.setSmallIcon(R.drawable.ic_stat_notification_logo)

View File

@ -13,12 +13,12 @@ import java.nio.IntBuffer;
public class GameIconRequestHandler extends RequestHandler {
@Override
public boolean canHandleRequest(Request data) {
return "iso".equals(data.uri.getScheme());
return "content".equals(data.uri.getScheme()) || data.uri.getScheme() == null;
}
@Override
public Result load(Request request, int networkPolicy) {
String url = request.uri.getHost() + request.uri.getPath();
String url = request.uri.toString();
int[] vector = NativeLibrary.GetIcon(url);
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));

View File

@ -0,0 +1,33 @@
package org.citra.citra_emu.utils;
import android.content.Context;
import android.content.res.Resources;
import android.view.ViewGroup;
import androidx.core.graphics.Insets;
import com.google.android.material.appbar.AppBarLayout;
public class InsetsHelper {
public static final int THREE_BUTTON_NAVIGATION = 0;
public static final int TWO_BUTTON_NAVIGATION = 1;
public static final int GESTURE_NAVIGATION = 2;
public static void insetAppBar(Insets insets, AppBarLayout appBarLayout)
{
ViewGroup.MarginLayoutParams mlpAppBar =
(ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
mlpAppBar.leftMargin = insets.left;
mlpAppBar.rightMargin = insets.right;
appBarLayout.setLayoutParams(mlpAppBar);
}
public static int getSystemGestureType(Context context) {
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android");
if (resourceId != 0) {
return resources.getInteger(resourceId);
}
return 0;
}
}

View File

@ -1,28 +1,32 @@
package org.citra.citra_emu.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import androidx.core.content.ContextCompat;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
public class PermissionsHandler {
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
public static final String CITRA_DIRECTORY = "CITRA_DIRECTORY";
public static final SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
// We use permissions acceptance as an indicator if this is a first boot for the user.
public static boolean isFirstBoot(final FragmentActivity activity) {
return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
public static boolean isFirstBoot(FragmentActivity activity) {
return !hasWriteAccess(activity.getApplicationContext());
}
@TargetApi(Build.VERSION_CODES.M)
public static boolean checkWritePermission(final FragmentActivity activity) {
public static boolean checkWritePermission(FragmentActivity activity,
ActivityResultLauncher<Uri> launcher) {
if (isFirstBoot(activity)) {
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_WRITE_PERMISSION);
launcher.launch(null);
return false;
}
@ -30,6 +34,31 @@ public class PermissionsHandler {
}
public static boolean hasWriteAccess(Context context) {
return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
try {
Uri uri = getCitraDirectory();
if (uri == null)
return false;
int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
DocumentFile root = DocumentFile.fromTreeUri(context, uri);
if (root != null && root.exists()) return true;
context.getContentResolver().releasePersistableUriPermission(uri, takeFlags);
} catch (Exception e) {
Log.error("[PermissionsHandler]: Cannot check citra data directory permission, error: " + e.getMessage());
}
return false;
}
@Nullable
public static Uri getCitraDirectory() {
String directoryString = mPreferences.getString(CITRA_DIRECTORY, "");
if (directoryString.isEmpty()) {
return null;
}
return Uri.parse(directoryString);
}
public static boolean setCitraDirectory(String uriString) {
return mPreferences.edit().putString(CITRA_DIRECTORY, uriString).commit();
}
}

View File

@ -31,7 +31,7 @@ public class PicassoUtils {
public static void loadGameIcon(ImageView imageView, String gamePath) {
Picasso
.get()
.load(Uri.parse("iso:/" + gamePath))
.load(Uri.parse(gamePath))
.fit()
.centerInside()
.config(Bitmap.Config.RGB_565)

View File

@ -1,19 +1,23 @@
package org.citra.citra_emu.utils;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
public final class StartupHandler {
private static void handlePermissionsCheck(FragmentActivity parent) {
private static void handlePermissionsCheck(FragmentActivity parent,
ActivityResultLauncher<Uri> launcher) {
// Ask the user to grant write permission if it's not already granted
PermissionsHandler.checkWritePermission(parent);
PermissionsHandler.checkWritePermission(parent, launcher);
String start_file = "";
Bundle extras = parent.getIntent().getExtras();
@ -30,16 +34,23 @@ public final class StartupHandler {
}
}
public static void HandleInit(FragmentActivity parent) {
public static void HandleInit(FragmentActivity parent, ActivityResultLauncher<Uri> launcher) {
if (PermissionsHandler.isFirstBoot(parent)) {
// Prompt user with standard first boot disclaimer
new AlertDialog.Builder(parent)
AlertDialog dialog =
new MaterialAlertDialogBuilder(parent)
.setTitle(R.string.app_name)
.setIcon(R.mipmap.ic_launcher)
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
.setMessage(R.string.app_disclaimer)
.setPositiveButton(android.R.string.ok, null)
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
.setCancelable(false)
.setOnDismissListener(
dialogInterface -> handlePermissionsCheck(parent, launcher))
.show();
TextView textView = dialog.findViewById(android.R.id.message);
if (textView == null)
return;
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}

View File

@ -1,18 +1,32 @@
package org.citra.citra_emu.utils;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.color.MaterialColors;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
public class ThemeUtil {
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
private static void applyTheme(int designValue) {
public static final float NAV_BAR_ALPHA = 0.9f;
private static void applyTheme(int designValue, AppCompatActivity activity) {
switch (designValue) {
case 0:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
@ -26,9 +40,44 @@ public class ThemeUtil {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
break;
}
int systemReportedThemeMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
WindowInsetsControllerCompat windowController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView());
windowController.setAppearanceLightStatusBars(systemReportedThemeMode == Configuration.UI_MODE_NIGHT_NO);
windowController.setAppearanceLightNavigationBars(systemReportedThemeMode == Configuration.UI_MODE_NIGHT_NO);
setNavigationBarColor(activity, MaterialColors.getColor(activity.getWindow().getDecorView(), R.attr.colorSurface));
}
public static void applyTheme() {
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0));
public static void applyTheme(AppCompatActivity activity) {
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0), activity);
}
public static void setNavigationBarColor(@NonNull Activity activity, @ColorInt int color) {
int gestureType = InsetsHelper.getSystemGestureType(activity.getApplicationContext());
int orientation = activity.getResources().getConfiguration().orientation;
// Use a solid color when the navigation bar is on the left/right edge of the screen
if ((gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) &&
orientation == Configuration.ORIENTATION_LANDSCAPE) {
activity.getWindow().setNavigationBarColor(color);
} else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) {
// Use semi-transparent color when in portrait mode with three/two button navigation to
// partially see list items behind the navigation bar
activity.getWindow().setNavigationBarColor(ThemeUtil.getColorWithOpacity(color, NAV_BAR_ALPHA));
} else {
// Use transparent color when using gesture navigation
activity.getWindow().setNavigationBarColor(
ContextCompat.getColor(activity.getApplicationContext(),
android.R.color.transparent));
}
}
@ColorInt
public static int getColorWithOpacity(@ColorInt int color, float alphaFactor) {
return Color.argb(Math.round(alphaFactor * Color.alpha(color)), Color.red(color),
Color.green(color), Color.blue(color));
}
}

View File

@ -26,7 +26,11 @@
Config::Config() {
// TODO: Don't hardcode the path; let the frontend decide where to put the config files.
sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini";
sdl2_config = std::make_unique<INIReader>(sdl2_config_loc);
std::string ini_buffer;
FileUtil::ReadFileToString(true, sdl2_config_loc, ini_buffer);
if (!ini_buffer.empty()) {
sdl2_config = std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size());
}
Reload();
}
@ -35,12 +39,15 @@ Config::~Config() = default;
bool Config::LoadINI(const std::string& default_contents, bool retry) {
const std::string& location = this->sdl2_config_loc;
if (sdl2_config->ParseError() < 0) {
if (sdl2_config == nullptr || sdl2_config->ParseError() < 0) {
if (retry) {
LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location);
FileUtil::CreateFullPath(location);
FileUtil::WriteStringToFile(true, location, default_contents);
sdl2_config = std::make_unique<INIReader>(location); // Reopen file
std::string ini_buffer;
FileUtil::ReadFileToString(true, location, ini_buffer);
sdl2_config =
std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size()); // Reopen file
return LoadINI(default_contents, false);
}
@ -75,6 +82,30 @@ void Config::UpdateCFG() {
cfg->UpdateConfigNANDSavegame();
}
template <>
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault());
if (setting_value.empty()) {
setting_value = setting.GetDefault();
}
setting = std::move(setting_value);
}
template <>
void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) {
setting = sdl2_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
}
template <typename Type, bool ranged>
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
if constexpr (std::is_floating_point_v<Type>) {
setting = sdl2_config->GetReal(group, setting.GetLabel(), setting.GetDefault());
} else {
setting = static_cast<Type>(sdl2_config->GetInteger(
group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
}
}
void Config::ReadValues() {
// Controls
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
@ -105,39 +136,32 @@ void Config::ReadValues() {
InputCommon::CemuhookUDP::DEFAULT_PORT));
// Core
Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true);
Settings::values.cpu_clock_percentage =
static_cast<int>(sdl2_config->GetInteger("Core", "cpu_clock_percentage", 100));
ReadSetting("Core", Settings::values.use_cpu_jit);
ReadSetting("Core", Settings::values.cpu_clock_percentage);
// Premium
Settings::values.texture_filter_name =
sdl2_config->GetString("Premium", "texture_filter_name", "none");
ReadSetting("Premium", Settings::values.texture_filter_name);
// Renderer
Settings::values.use_gles = sdl2_config->GetBoolean("Renderer", "use_gles", true);
Settings::values.use_hw_renderer = sdl2_config->GetBoolean("Renderer", "use_hw_renderer", true);
Settings::values.use_hw_shader = sdl2_config->GetBoolean("Renderer", "use_hw_shader", true);
Settings::values.shaders_accurate_mul =
sdl2_config->GetBoolean("Renderer", "shaders_accurate_mul", false);
Settings::values.use_shader_jit = sdl2_config->GetBoolean("Renderer", "use_shader_jit", true);
Settings::values.resolution_factor =
static_cast<u16>(sdl2_config->GetInteger("Renderer", "resolution_factor", 1));
Settings::values.use_disk_shader_cache =
sdl2_config->GetBoolean("Renderer", "use_disk_shader_cache", true);
Settings::values.use_vsync_new = sdl2_config->GetBoolean("Renderer", "use_vsync_new", true);
ReadSetting("Renderer", Settings::values.graphics_api);
ReadSetting("Renderer", Settings::values.use_hw_shader);
ReadSetting("Renderer", Settings::values.use_shader_jit);
ReadSetting("Renderer", Settings::values.resolution_factor);
ReadSetting("Renderer", Settings::values.use_disk_shader_cache);
ReadSetting("Renderer", Settings::values.use_vsync_new);
// Work around to map Android setting for enabling the frame limiter to the format Citra expects
if (sdl2_config->GetBoolean("Renderer", "use_frame_limit", true)) {
Settings::values.frame_limit =
static_cast<u16>(sdl2_config->GetInteger("Renderer", "frame_limit", 100));
ReadSetting("Renderer", Settings::values.frame_limit);
} else {
Settings::values.frame_limit = 0;
}
Settings::values.render_3d = static_cast<Settings::StereoRenderOption>(
sdl2_config->GetInteger("Renderer", "render_3d", 0));
Settings::values.factor_3d =
static_cast<u8>(sdl2_config->GetInteger("Renderer", "factor_3d", 0));
ReadSetting("Renderer", Settings::values.render_3d);
ReadSetting("Renderer", Settings::values.factor_3d);
std::string default_shader = "none (builtin)";
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph)
default_shader = "dubois (builtin)";
@ -145,70 +169,49 @@ void Config::ReadValues() {
default_shader = "horizontal (builtin)";
Settings::values.pp_shader_name =
sdl2_config->GetString("Renderer", "pp_shader_name", default_shader);
Settings::values.filter_mode = sdl2_config->GetBoolean("Renderer", "filter_mode", true);
ReadSetting("Renderer", Settings::values.filter_mode);
Settings::values.bg_red = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_red", 0.0));
Settings::values.bg_green =
static_cast<float>(sdl2_config->GetReal("Renderer", "bg_green", 0.0));
Settings::values.bg_blue = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_blue", 0.0));
ReadSetting("Renderer", Settings::values.bg_red);
ReadSetting("Renderer", Settings::values.bg_green);
ReadSetting("Renderer", Settings::values.bg_blue);
// Layout
Settings::values.layout_option = static_cast<Settings::LayoutOption>(sdl2_config->GetInteger(
"Layout", "layout_option", static_cast<int>(Settings::LayoutOption::MobileLandscape)));
Settings::values.custom_layout = sdl2_config->GetBoolean("Layout", "custom_layout", false);
Settings::values.custom_top_left =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_left", 0));
Settings::values.custom_top_top =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_top", 0));
Settings::values.custom_top_right =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_right", 400));
Settings::values.custom_top_bottom =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_bottom", 240));
Settings::values.custom_bottom_left =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_left", 40));
Settings::values.custom_bottom_top =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_top", 240));
Settings::values.custom_bottom_right =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_right", 360));
Settings::values.custom_bottom_bottom =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_bottom", 480));
Settings::values.cardboard_screen_size =
static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_screen_size", 85));
Settings::values.cardboard_x_shift =
static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_x_shift", 0));
Settings::values.cardboard_y_shift =
static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_y_shift", 0));
ReadSetting("Layout", Settings::values.custom_layout);
ReadSetting("Layout", Settings::values.custom_top_left);
ReadSetting("Layout", Settings::values.custom_top_top);
ReadSetting("Layout", Settings::values.custom_top_right);
ReadSetting("Layout", Settings::values.custom_top_bottom);
ReadSetting("Layout", Settings::values.custom_bottom_left);
ReadSetting("Layout", Settings::values.custom_bottom_top);
ReadSetting("Layout", Settings::values.custom_bottom_right);
ReadSetting("Layout", Settings::values.custom_bottom_bottom);
ReadSetting("Layout", Settings::values.cardboard_screen_size);
ReadSetting("Layout", Settings::values.cardboard_x_shift);
ReadSetting("Layout", Settings::values.cardboard_y_shift);
// Utility
Settings::values.dump_textures = sdl2_config->GetBoolean("Utility", "dump_textures", false);
Settings::values.custom_textures = sdl2_config->GetBoolean("Utility", "custom_textures", false);
Settings::values.preload_textures =
sdl2_config->GetBoolean("Utility", "preload_textures", false);
ReadSetting("Utility", Settings::values.dump_textures);
ReadSetting("Utility", Settings::values.custom_textures);
ReadSetting("Utility", Settings::values.preload_textures);
// Audio
Settings::values.audio_emulation =
static_cast<Settings::AudioEmulation>(sdl2_config->GetInteger(
"Audio", "audio_emulation", static_cast<int>(Settings::AudioEmulation::HLE)));
Settings::values.sink_id = sdl2_config->GetString("Audio", "output_engine", "auto");
Settings::values.enable_audio_stretching =
sdl2_config->GetBoolean("Audio", "enable_audio_stretching", true);
Settings::values.audio_device_id = sdl2_config->GetString("Audio", "output_device", "auto");
Settings::values.volume = static_cast<float>(sdl2_config->GetReal("Audio", "volume", 1));
Settings::values.mic_input_device =
sdl2_config->GetString("Audio", "mic_input_device", "Default");
Settings::values.mic_input_type =
static_cast<Settings::MicInputType>(sdl2_config->GetInteger("Audio", "mic_input_type", 1));
ReadSetting("Audio", Settings::values.audio_emulation);
ReadSetting("Audio", Settings::values.sink_id);
ReadSetting("Audio", Settings::values.enable_audio_stretching);
ReadSetting("Audio", Settings::values.audio_device_id);
ReadSetting("Audio", Settings::values.volume);
ReadSetting("Audio", Settings::values.mic_input_device);
ReadSetting("Audio", Settings::values.mic_input_type);
// Data Storage
Settings::values.use_virtual_sd =
sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true);
ReadSetting("Data Storage", Settings::values.use_virtual_sd);
// System
Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", true);
Settings::values.region_value =
sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT);
Settings::values.init_clock =
static_cast<Settings::InitClock>(sdl2_config->GetInteger("System", "init_clock", 0));
ReadSetting("System", Settings::values.is_new_3ds);
ReadSetting("System", Settings::values.region_value);
ReadSetting("System", Settings::values.init_clock);
{
std::tm t;
t.tm_sec = 1;
@ -229,10 +232,8 @@ void Config::ReadValues() {
std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch())
.count();
}
Settings::values.plugin_loader_enabled =
sdl2_config->GetBoolean("System", "plugin_loader", false);
Settings::values.allow_plugin_loader =
sdl2_config->GetBoolean("System", "allow_plugin_loader", true);
ReadSetting("System", Settings::values.plugin_loader_enabled);
ReadSetting("System", Settings::values.allow_plugin_loader);
// Camera
using namespace Service::CAM;
@ -256,14 +257,14 @@ void Config::ReadValues() {
sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0);
// Miscellaneous
Settings::values.log_filter = sdl2_config->GetString("Miscellaneous", "log_filter", "*:Info");
ReadSetting("Miscellaneous", Settings::values.log_filter);
// Debugging
Settings::values.record_frame_times =
sdl2_config->GetBoolean("Debugging", "record_frame_times", false);
Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false);
Settings::values.gdbstub_port =
static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689));
ReadSetting("Debugging", Settings::values.renderer_debug);
ReadSetting("Debugging", Settings::values.use_gdbstub);
ReadSetting("Debugging", Settings::values.gdbstub_port);
for (const auto& service_module : Service::service_module_map) {
bool use_lle = sdl2_config->GetBoolean("Debugging", "LLE\\" + service_module.name, false);

View File

@ -6,6 +6,7 @@
#include <memory>
#include <string>
#include "common/settings.h"
class INIReader;
@ -23,4 +24,14 @@ public:
~Config();
void Reload();
private:
/**
* Applies a value read from the sdl2_config to a Setting.
*
* @param group The name of the INI group
* @param setting The yuzu setting to modify
*/
template <typename Type, bool ranged>
void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
};

View File

@ -98,13 +98,9 @@ use_cpu_jit =
cpu_clock_percentage =
[Renderer]
# Whether to render using GLES or OpenGL
# 0: OpenGL, 1 (default): GLES
use_gles =
# Whether to use software or hardware rendering.
# 0: Software, 1 (default): Hardware
use_hw_renderer =
# Whether to render using OpenGL
# 1: OpenGLES (default)
graphics_api =
# Whether to use hardware shaders to emulate 3DS shaders
# 0: Software, 1 (default): Hardware
@ -118,10 +114,6 @@ separable_shader =
# 0: Off (Default. Faster, but causes issues in some games) 1: On (Slower, but correct)
shaders_accurate_mul =
# Enable asynchronous GPU emulation
# 0: Off (Slower, but more accurate) 1: On (Default. Faster, but may cause issues in some games)
use_asynchronous_gpu_emulation =
# Whether to use the Just-In-Time (JIT) compiler for shader emulation
# 0: Interpreter (slow), 1 (default): JIT (fast)
use_shader_jit =
@ -168,9 +160,12 @@ factor_3d =
# The name of the post processing shader to apply.
# Loaded from shaders if render_3d is off or side by side.
# Loaded from shaders/anaglyph if render_3d is anaglyph
pp_shader_name =
# The name of the shader to apply when render_3d is anaglyph.
# Loaded from shaders/anaglyph
anaglyph_shader_name =
# Whether to enable linear filtering or not
# This is required for some shaders to work correctly
# 0: Nearest, 1 (default): Linear
@ -322,9 +317,15 @@ log_filter = *:Info
[Debugging]
# Record frame time data, can be found in the log directory. Boolean value
record_frame_times =
# Whether to enable additional debugging information during emulation
# 0 (default): Off, 1: On
renderer_debug =
# Port for listening to GDB connections.
use_gdbstub=false
gdbstub_port=24689
# To LLE a service module add "LLE\<module name>=true"
[WebService]

View File

@ -2,6 +2,7 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/android_storage.h"
#include "common/common_paths.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
@ -159,10 +160,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
log_filter.ParseFilterString(Settings::values.log_filter.GetValue());
Log::SetGlobalFilter(log_filter);
Log::AddBackend(std::make_unique<Log::LogcatBackend>());
FileUtil::CreateFullPath(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
Log::AddBackend(std::make_unique<Log::FileBackend>(
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
LOG_INFO(Frontend, "Logging backend initialised");
// Initialize misc classes
s_savestate_info_class = reinterpret_cast<jclass>(
@ -230,6 +227,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
MiiSelector::InitJNI(env);
SoftwareKeyboard::InitJNI(env);
Camera::StillImage::InitJNI(env);
AndroidStorage::InitJNI(env, s_native_library_class);
return JNI_VERSION;
}
@ -254,6 +252,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
MiiSelector::CleanupJNI(env);
SoftwareKeyboard::CleanupJNI(env);
Camera::StillImage::CleanupJNI(env);
AndroidStorage::CleanupJNI();
}
#ifdef __cplusplus

View File

@ -3,12 +3,19 @@
// Refer to the license.txt file included.
#include <lodepng.h>
#include "common/file_util.h"
#include "common/logging/log.h"
#include "jni/lodepng_image_interface.h"
bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
const std::string& path) {
u32 lodepng_ret = lodepng::decode(dst, width, height, path);
FileUtil::IOFile file(path, "rb");
size_t read_size = file.GetSize();
std::vector<u8> in(read_size);
if (file.ReadBytes(&in[0], read_size) != read_size) {
LOG_CRITICAL(Frontend, "Failed to decode {}", path);
}
u32 lodepng_ret = lodepng::decode(dst, width, height, in);
if (lodepng_ret) {
LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path,
lodepng_error_text(lodepng_ret));
@ -19,11 +26,19 @@ bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& hei
bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src,
u32 width, u32 height) {
u32 lodepng_ret = lodepng::encode(path, src, width, height);
std::vector<u8> out;
u32 lodepng_ret = lodepng::encode(out, src, width, height);
if (lodepng_ret) {
LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path,
lodepng_error_text(lodepng_ret));
return false;
}
FileUtil::IOFile file(path, "wb");
if (file.WriteBytes(&out[0], out.size()) != out.size()) {
LOG_CRITICAL(Frontend, "Failed to save encode to path={}", path);
return false;
}
return true;
}

View File

@ -12,7 +12,9 @@
#include "audio_core/dsp_interface.h"
#include "common/aarch64/cpu_detect.h"
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/scm_rev.h"
@ -23,7 +25,6 @@
#include "core/frontend/applets/default_applets.h"
#include "core/frontend/camera/factory.h"
#include "core/frontend/mic.h"
#include "core/frontend/scope_acquire_context.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/nfc/nfc.h"
#include "core/savestate.h"
@ -44,6 +45,7 @@
#include "jni/ndk_motion.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
#include "video_core/video_core.h"
namespace {
@ -147,7 +149,15 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
return Core::System::ResultStatus::ErrorLoader;
}
window = std::make_unique<EmuWindow_Android>(s_surf);
const auto graphics_api = Settings::values.graphics_api.GetValue();
switch (graphics_api) {
case Settings::GraphicsAPI::OpenGL:
window = std::make_unique<EmuWindow_Android>(s_surf);
break;
default:
LOG_CRITICAL(Frontend, "Unknown graphics API {}, using OpenGL", graphics_api);
window = std::make_unique<EmuWindow_Android>(s_surf);
}
Core::System& system{Core::System::GetInstance()};
@ -319,6 +329,8 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetInstalledGamePaths(
path += '/';
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
} else {
if (!FileUtil::Exists(path))
return false;
auto loader = Loader::GetLoader(path);
if (loader) {
bool executable{};
@ -492,6 +504,23 @@ void Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
Config{};
}
void Java_org_citra_citra_1emu_NativeLibrary_CreateLogFile(JNIEnv* env,
[[maybe_unused]] jclass clazz) {
Log::RemoveBackend(Log::FileBackend::Name());
FileUtil::CreateFullPath(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
Log::AddBackend(std::make_unique<Log::FileBackend>(
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
LOG_INFO(Frontend, "Logging backend initialised");
}
void Java_org_citra_citra_1emu_NativeLibrary_LogUserDirectory(JNIEnv* env,
[[maybe_unused]] jclass clazz,
jstring j_path) {
std::string_view path = env->GetStringUTFChars(j_path, 0);
LOG_INFO(Frontend, "User directory path: {}", path);
env->ReleaseStringUTFChars(j_path, path.data());
}
jint Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
[[maybe_unused]] jclass clazz) {
return 0;
@ -713,8 +742,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz
}
void Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, jclass clazz) {
// TODO: Log the Common::g_build_fullname once the CI is setup for android
LOG_INFO(Frontend, "Citra Version: Android Beta | {}-{}", Common::g_scm_branch,
LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
Common::g_scm_desc);
LOG_INFO(Frontend, "Host CPU: {}", Common::GetCPUCaps().cpu_string);
// There is no decent way to get the OS version, so we log the API level instead.

View File

@ -83,6 +83,13 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetSysDirectory(J
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
jclass clazz);
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateLogFile(JNIEnv* env,
jclass clazz);
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogUserDirectory(JNIEnv* env,
jclass clazz,
jstring path);
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
jclass clazz);
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env,

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="-75" />
</set>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="-200"
android:toXDelta="0" />
</set>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="75" />
</set>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="200"
android:toXDelta="0" />
</set>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
</set>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="yFraction"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="1.0"
android:valueTo="0" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="translationZ"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="100.0"
android:valueTo="0" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="elevation"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="100.0"
android:valueTo="0" />
</set>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/accelerate_cubic"
android:propertyName="visibleness"
android:valueFrom="1.0f"
android:valueTo="0.6f"
android:valueType="floatType" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="translationZ"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="0"
android:valueTo="-100.0" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="elevation"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="0"
android:valueTo="-100.0" />
</set>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="visibleness"
android:valueFrom="0.6f"
android:valueTo="1.0f"
android:valueType="floatType" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="translationZ"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="-100.0"
android:valueTo="0" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="elevation"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="-100.0"
android:valueTo="0" />
</set>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/accelerate_cubic"
android:propertyName="yFraction"
android:valueFrom="0"
android:valueTo="1.0" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="translationZ"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="0.0"
android:valueTo="100" />
<objectAnimator
android:duration="@android:integer/config_mediumAnimTime"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="elevation"
android:startOffset="@android:integer/config_shortAnimTime"
android:valueFrom="0.0"
android:valueTo="100" />
</set>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 B

Some files were not shown because too many files have changed in this diff Show More