Compare commits

...

307 Commits

Author SHA1 Message Date
bunnei
5f1beee8aa
Merge pull request #2 from citra-emu/translations
android: Update app translations
2020-05-21 22:09:12 -04:00
FearlessTobi
084141a6d8 android: Update app translations
Adds Korean and some minor fixes to other languages.
2020-05-22 02:31:47 +02:00
bunnei
0587feb26b androuid: game_settings: Tighten asynch GPU timing.
- Fixes framerate issues in ZALBW.
2020-05-20 22:43:04 -04:00
SachinVin
cc1b5ba05c android: AndroidManifest.xml: bump up gles requirement to 3.2 2020-05-20 17:37:06 -04:00
SachinVin
ac190d1d1f android: disable support for split screen 2020-05-20 17:37:06 -04:00
Pengfei Zhu
373ab0e69a audio_core/cubeb_input: Set default value for latency_frames
On Android OpenSL, cubeb could not find the minimum latency. Therefore, this variable was left uninitialized and caused problems when opening the stream.
2020-05-20 17:37:06 -04:00
bunnei
000884fe0d android: BillingManager: Use real managed product for premium. 2020-05-20 17:37:06 -04:00
bunnei
1c187a0d8b android: submodules: Use github for dynarmic. 2020-05-20 17:37:06 -04:00
bunnei
4d71c1ecd4 android: audio: Audio stretching is only useful with lower framerates, disable it when fullspeed. 2020-05-20 17:37:05 -04:00
bunnei
bdc3d3b1c9 android: video_core: Add experimental asynchronous GPU option. 2020-05-20 17:37:05 -04:00
bunnei
272d745818 Revert "android: audio: Disable audio stretching by default."
This reverts commit 9ccbba07c824e0eca168fa2563509072b6b6793d.

# Conflicts:
#	src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
2020-05-20 17:37:05 -04:00
SachinVin
950f99d6f2 common/logging: Create a new backed for android's logcat 2020-05-20 17:37:05 -04:00
zhupengfei
ee3b0fe68c android: Microphone support
Based on Tobi's work. Created a new 'real Mic factory' which creates mic interfaces for real devices. There's a default factory set (cubeb/null, depending on whether cubeb exists) in order to avoid changing code of other frontends.

Created a factory for the Android frontend which requests mic permission and creates a CubebInput (or null). The permission requesting code is basically the same as camera's. If the user denied the permission, an alert dialog will be shown informing them of the reason.

For easier usage, by default the Audio Input Device is set to 'Real Device'.

Co-authored-by: FearlessTobi <thm.frey@gmail.com>
2020-05-20 17:37:05 -04:00
bunnei
7f85e5fa43 android: audio: Disable audio stretching by default. 2020-05-20 17:37:05 -04:00
bunnei
c09d62f487 android: MainActivity: Fix crash when game directory button is not present. 2020-05-20 17:37:05 -04:00
BreadFish64
3734e43c4e android: allow navigating to external storage 2020-05-20 17:37:05 -04:00
bunnei
6626a8e9e5 android: BillingManager: Hide premium actionbar button when premium is active. 2020-05-20 17:37:05 -04:00
bunnei
a1c3e3001f android: main: res: Add premium icon and fix folder size. 2020-05-20 17:37:04 -04:00
BreadFish64
e897bb67fd android: more theme changes
fix file picker dialog colors
make Citra text orange in dark mode
2020-05-20 17:37:04 -04:00
BreadFish64
5520fd0b67 android: inherit file-picker toolbar theme from DayNight 2020-05-20 17:37:04 -04:00
BreadFish64
de3549a6ad android: fix toolbar theme 2020-05-20 17:37:04 -04:00
BreadFish64
1b1a6903b0 android: fix garbled names in the game list 2020-05-20 17:37:04 -04:00
SachinVin
843225af03 android:game_info.cpp: correct grammar in comment 2020-05-20 17:37:04 -04:00
zhupengfei
b2cd8fca74 android/ndk_camera: Fix rotation
`width` and `height` are not necessarily swapped at this point
2020-05-20 17:37:04 -04:00
FearlessTobi
0dd1fffd50 android: Make toolbar text black 2020-05-20 17:37:04 -04:00
SachinVin
3bd66d6253 android:game_info.cpp: cleanup spammy log 2020-05-20 17:37:04 -04:00
zhupengfei
d042855546 still_image_camera: Fix incorrect JNI usage
As `jstring`s are also object references, if we would like to use them across native methods/threads, we would have to make a global reference.

We will need to delete this global reference explicitly. Since this string is shared across multiple Interfaces and also in Factory, I used shared_ptr to manage deletion. Added a fancy SharedGlobalRef to id_cache.h.

Also removed global reference creation for java/lang/String classes. Turns out that local references are guaranteed valid for the duration of the method, and I was just being too cautious.
2020-05-20 17:37:03 -04:00
FearlessTobi
e40f7f0946 android: Add dark theme icons 2020-05-20 17:37:03 -04:00
FearlessTobi
519a41acca android: Run final format 2020-05-20 17:37:03 -04:00
FearlessTobi
1dfcca496b android: Run clang-format 2020-05-20 17:37:03 -04:00
FearlessTobi
bc044da12e android: Format code
Finally makes us have consistent code format in the codebase.
2020-05-20 17:37:03 -04:00
zhupengfei
d5f8f75bf3 android/ndk_camera: A few fixes
1. Remove unused code
2. Avoid crashes when camera wasn't opened
3. Avoid potential data race
2020-05-20 17:37:03 -04:00
zhupengfei
8ba7844d3f android: Fix camera settings
1. Fix settings crash when it gets treated as IntSetting
2. Properly report camera facing
2020-05-20 17:37:03 -04:00
bunnei
2240b2a600 android: GameDatabase: Fix typo in rebase. 2020-05-20 17:37:03 -04:00
SachinVin
428f4e2edb android: GameDatabase: dont add misc extensions from sub folders. 2020-05-20 17:37:03 -04:00
SachinVin
12e6c3925e android: Recursive dir
# Conflicts:
#	src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
#	src/android/app/src/main/jni/native.cpp

# Conflicts:
#	src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java
#	src/android/app/src/main/jni/native.cpp
2020-05-20 17:37:02 -04:00
FearlessTobi
263acc9ae8 android: Add finished translations from Transifex
This adds all languages that have at least translated 50% of the Android source file.
I hope we can automate this process in the future.
2020-05-20 17:37:02 -04:00
SachinVin
01fcf63b6e android/CustomFilePickerFragment: fixup call super.goUp() instead 2020-05-20 17:37:02 -04:00
BreadFish64
46a639d082 android/GameList: Scan for installed titles 2020-05-20 17:37:02 -04:00
SachinVin
0ea79b55b0 android/CustomFilePickerFragment: don't go up beyond the External Storage Directory ...
/storage/emulated/0/
2020-05-20 17:37:02 -04:00
bunnei
7cb8c89126 android: overlay: Tighten portrait A/B/X/Y buttons. 2020-05-20 17:37:02 -04:00
bunnei
7ce128b548 android: overlay: Tighten portrait input a bit, this feels more natural. 2020-05-20 17:37:02 -04:00
bunnei
4e02035351 android: InputOverlayDrawableJoystick: Fix off by 1 error with inner joystick. 2020-05-20 17:37:02 -04:00
bunnei
af5ababd6d android: BillingManager: Add a useful comment to onQuerySkuDetailsFinished. 2020-05-20 17:37:02 -04:00
bunnei
03be46f7df android: EmulationActivity: Fix a crash when controller is disconnected. 2020-05-20 17:37:01 -04:00
bunnei
1882dba04f android: BillingManager: Fix issue with onQuerySkuDetailsFinished null param.
- Happens when not associated with a Google account.
2020-05-20 17:37:01 -04:00
FearlessTobi
f1f56be77e android: Minor changes to theming 2020-05-20 17:37:01 -04:00
FearlessTobi
0993d7cf18 android/Settings: Set mStackCount to 0 when starting the Activity
Fixes a bug where you would have to click multiple times to get out of the settings after turning the screen off and on again.
2020-05-20 17:37:01 -04:00
SachinVin
481f8a8f8b Android: directory picker: Add archive extensions for consistency 2020-05-20 17:37:01 -04:00
FearlessTobi
ceacb1e053 android: Change theme colors and modify icons 2020-05-20 17:37:01 -04:00
FearlessTobi
7751fda5e8 android/MainPresenter: Add double click prevention for the buttons 2020-05-20 17:37:01 -04:00
bunnei
d80b90304e android: settings: Fix config issue with texture_filter_name for premium. 2020-05-20 17:37:01 -04:00
bunnei
f926d25f1c android: native: Fix crash on multiple ZIP boots. 2020-05-20 17:37:01 -04:00
bunnei
b247ca9b3d android: strings: Update for asynchronous GPU. 2020-05-20 17:37:01 -04:00
bunnei
75ddba3932 android: settings: Make texture filtering a premium setting. 2020-05-20 17:37:00 -04:00
bunnei
9fd0daaa0a settings: PremiumSingleChoiceSetting: Fix bug in getting/setting value. 2020-05-20 17:37:00 -04:00
bunnei
a85f3a4502 android: EmulationActivity: Fix merge issue with onActivityResult. 2020-05-20 17:37:00 -04:00
bunnei
13b8e206a9 Merge branch 'mii-selector' into 'master'
android/applets: Implement Mii Selector

See merge request CitraInternal/citra-android!33
2020-05-20 17:37:00 -04:00
bunnei
11386e59ff Merge branch 'amiibo' into 'master'
android: Add Amiibo file support

See merge request CitraInternal/citra-android!34
2020-05-20 17:37:00 -04:00
bunnei
089297f55b android: native: Ensure shutdown on exit. 2020-05-20 17:37:00 -04:00
bunnei
3eb1ca5339 android: EmulationActivity: Fix gamepad triggers. 2020-05-20 17:37:00 -04:00
bunnei
8313f00cf4 android: settings: Use more explicit ARG_MENU_TAG. 2020-05-20 17:37:00 -04:00
bunnei
2cd29650ae android: PremiumSingleChoiceSetting: Add null check to avoid a crash. 2020-05-20 17:37:00 -04:00
bunnei
c785282b26 android: settings: Disable 'System Default' theme for pre-Android 10.
- It's not officially supported.
2020-05-20 17:36:59 -04:00
bunnei
66cf6fefb8 android: settings: Store theme setting in shared preferences.
- Fixes some jankieness.
2020-05-20 17:36:59 -04:00
bunnei
b0cf51c7b6 Revert "Merge branch 'rt-android' into 'master'"
This reverts commit df9f831a915524e87bf6d63ce86d76589a3fcd6c, reversing
changes made to 1e11e0aecbfdc5ddb7ad835fe673366d68788bc6.

# Conflicts:
#	src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
2020-05-20 17:36:59 -04:00
SachinVin
714ab752bf android: create SingletonInstance for Picasso and add a place holder icon 2020-05-20 17:36:59 -04:00
FearlessTobi
013105b381 android: Also disable realtime audio in the settings presenter 2020-05-20 17:36:59 -04:00
SachinVin
f2cb2a885e Update dynarmic 2020-05-20 17:36:59 -04:00
zhupengfei
18201a1f65 Address review 2020-05-20 17:36:59 -04:00
zhupengfei
fea1a4681c ndk_camera: Fixes
Removed debug logs and unused code
Turned CaptureSession struct for simplicity
Added support for camera reload
Fixed ANativeWindow not released
2020-05-20 17:36:59 -04:00
zhupengfei
c6b2b825c5 android/camera: UX enhancements
1. Only request camera permissions once
2. Set the default settings to NDK camera
3. When camera device is not found, fall back to still image
4. Add 'Camera Device' configuration when one is found
5. Added a message when camera permissions are denied

For 4, I had to remove the use of the `config` field in StillImage camera.
2020-05-20 17:36:59 -04:00
zhupengfei
bdde3243a2 android: Add simple UI for camera configuration
The UI is subject to be changed. At least need to add a camera device selection. I also think we should make Device Camera the default
2020-05-20 17:36:58 -04:00
zhupengfei
8e90700c87 fixes to the NDK camera implementation 2020-05-20 17:36:58 -04:00
zhupengfei
00771e5610 android/camera: Implement image flipping
We use libyuv's Mirror function to handle horizontal flip. Regarding the vertical flip, libyuv doc states that 'just set a negative height'
2020-05-20 17:36:58 -04:00
zhupengfei
365613d3ad android: Add NDK camera implementation
Not tested yet as my device doesn't support camera2...
2020-05-20 17:36:58 -04:00
zhupengfei
becbb04785 android: Add a still image camera
Similar to what is in the Qt frontend, this camera takes a URI to a
picture file. When the config is empty, it will open up the gallery and
ask the user to pick a picture.

The image is then read and cropped from the Java side by the Picasso library,
and sent to the native code with android NDK Bitmap API (jnigraphics).
The native code handles the format conversion with libyuv.

Image flipping is yet to be implemented.
2020-05-20 17:36:58 -04:00
zhupengfei
87a0b488c1 externals: Add libyuv and jnigraphics 2020-05-20 17:36:58 -04:00
bunnei
6f277d5ae3 android: audio: Disable realtime audio by default. 2020-05-20 17:36:58 -04:00
bunnei
14110ff81e android: Apply correct theme at boot. 2020-05-20 17:36:58 -04:00
bunnei
b81c893ea8 android: Implement billing for Premium. 2020-05-20 17:36:58 -04:00
zhupengfei
72f4430eec Fix a typo in swkbd
That made the application crash when an @ is typed and the game prohibited it
2020-05-20 17:36:58 -04:00
BreadFish64
d332e72b9b memory: fix memory leak related to un-freed shared memory 2020-05-20 17:36:57 -04:00
BreadFish64
67c45b4d83 Revert "android: log: TrimSourcePath: Cannot be constexpr."
This reverts commit 21a75c52
2020-05-20 17:36:57 -04:00
FearlessTobi
7833c16294 GameDatabase: Don't rescan when upgraded
This otherwise causes a crash because the Database gets closed too early.
2020-05-20 17:36:57 -04:00
FearlessTobi
81ef144837 GameDatabase: Increase DB_VERSION
I forgot to do this in a previous PR.
2020-05-20 17:36:57 -04:00
bunnei
1670d509e4 android: MainActivity: Move theme setup to after settings initialization.
- Fixes a crash.
2020-05-20 17:36:57 -04:00
FearlessTobi
1a925312c0 android: Add premium section and Dark Theme setting 2020-05-20 17:36:57 -04:00
FearlessTobi
a35107b70d android/GameAdapter: Fix the getColor calls using the wrong context 2020-05-20 17:36:57 -04:00
FearlessTobi
3e35d37120 android/build: Update exifinterface to version 1.2.0 2020-05-20 17:36:57 -04:00
FearlessTobi
6dfc9219a3 android: Make adjustScale reflect changes without closing the dialog 2020-05-20 17:36:57 -04:00
FearlessTobi
bf7fd02e05 android: Clarify the warning in the Debug tab 2020-05-20 17:36:57 -04:00
FearlessTobi
796641fb12 android: Enable audio stretching by default
It helps eliminate stutter and won't have a huge perf penalty, hopefully.
2020-05-20 17:36:56 -04:00
zhupengfei
6645d68c65 gl_shader_util: Specify default precision for uimage2D
Otherwise, this causes the application to crash when compiling any shader, on both devices I tested.
2020-05-20 17:36:56 -04:00
FearlessTobi
090a64c33a android: Add game region to the GameDatabase 2020-05-20 17:36:56 -04:00
FearlessTobi
a8b2d23f60 android: Remove unused JNI functions and use better names for game icon variables 2020-05-20 17:36:56 -04:00
FearlessTobi
e2dc401264 android/settings: Make background color match the rest of the app 2020-05-20 17:36:56 -04:00
FearlessTobi
2b771c221f styles: Use Appcompat instead of MaterialComponents in order to avoid various issues
Fixes the AppBar being black in settings and emulation when using the dark theme.
(Dolphin does the same.)

Also cleans up our styles a bit.
2020-05-20 17:36:56 -04:00
FearlessTobi
b91d8b0e0d android: Fix the frame limiter
It was erroneously using the wrong config section and variable type.
2020-05-20 17:36:56 -04:00
BreadFish64
ddca33d8fe video_core: disable depth/stencil texture download on OpenGL ES 2020-05-20 17:36:56 -04:00
BreadFish64
989ccee021 android: disallow split screen 2020-05-20 17:36:56 -04:00
FearlessTobi
36cc482bc5 android/settings: Move VSync option to debug
Dolphin does the same and there's no real reason the users should mess with it.
2020-05-20 17:36:55 -04:00
FearlessTobi
266f021d10 android/NativeLibrary: Use Html.FROM_HTML_MODE_LEGACY
Fixes a deprecation warning.
2020-05-20 17:36:55 -04:00
FearlessTobi
65195bc434 android: Make more Strings translatable 2020-05-20 17:36:55 -04:00
FearlessTobi
ea494871b9 android: Fix the menu animations
Turns out those unused methods were actually used for something.
2020-05-20 17:36:55 -04:00
FearlessTobi
4228ddd575 AndroidManifest: Workaround the crash when changing Theme while in settings
Essentially extends upon https://github.com/dolphin-emu/dolphin/pull/8288.

Original description:
"The "correct way" would be to fully save and restore data on onSaveInstanceState and restore it back on onCreate. I tried to do that first.
As there is various conflicts with doing this without some form of refactor due to how the lifecycle diverges and is baked-in to the application, ignoring Activity recreation on orientation change should work well enough and we don't draw anything different in settings on landscape mode or ui mode anyway."
2020-05-20 17:36:55 -04:00
FearlessTobi
9bed188759 android: Show a hint when the gamelist is empty 2020-05-20 17:36:55 -04:00
FearlessTobi
ac79a7a114 Make the gamelist look nicer and indicate files with a wrong extension visually 2020-05-20 17:36:55 -04:00
FearlessTobi
0c3d8a31f6 android: Change plus icon to folder icon because we don't support multiple directories 2020-05-20 17:36:55 -04:00
FearlessTobi
ee2d64a93e android: Add realtime audio setting 2020-05-20 17:36:55 -04:00
FearlessTobi
d0a29f1543 audio_core: Add realtime audio 2020-05-20 17:36:54 -04:00
BreadFish64
82846fcfac video_core: bump swap chain size for GLES to reduce bottleneck 2020-05-20 17:36:54 -04:00
BreadFish64
19dfd8a1e6 video_core: implement optimized D24S8->RGBA8 reinterpreters 2020-05-20 17:36:54 -04:00
James Rowe
71e5d979d1 Use immutable storage when available 2020-05-20 17:36:54 -04:00
BreadFish64
05bf57ab9a android: disable OpenGL debug message again 2020-05-20 17:36:54 -04:00
BreadFish64
3d5607a3ee android: fix opening settings menu in-game 2020-05-20 17:36:54 -04:00
SachinVin
02d2110149 android: SettingsFragmentPresenter.java: correct the section used in debug tab
Fixes setting always showing the default value.
2020-05-20 17:36:54 -04:00
BreadFish64
245814f306 video_core: implement GLES depth/stencil downloads 2020-05-20 17:36:50 -04:00
FearlessTobi
dd63b2f09e android: Add debug tab 2020-05-20 17:35:25 -04:00
FearlessTobi
3ee3db6bf9 Optimize imports 2020-05-20 17:35:25 -04:00
FearlessTobi
1ea3cecc33 android: Complete the removal of ATV and address minor Linter warnings 2020-05-20 17:35:24 -04:00
FearlessTobi
ed32bfd28d android: Remove obsolete AndroidTV mode and its resources 2020-05-20 17:35:24 -04:00
BreadFish64
312dc5e821 android: add texture filter setting 2020-05-20 17:35:24 -04:00
BreadFish64
20b63ac08e android: Add StringSingleChoiceSetting 2020-05-20 17:35:24 -04:00
BreadFish64
ff5fdf6331 video_core: fix texture filters in GLES 2020-05-20 17:35:24 -04:00
zhupengfei
52fbdab999 android/swkbd: Properly set uncancelable
When using a DialogFragment you have to set this property on the DialogFragment itself.
2020-05-20 17:35:24 -04:00
FearlessTobi
470c0e806a Android: Disable automatic backup
Since we don't have proper confuguration file of what to include/exclude
in the backup, this better be disabled because it will lead to unexpected
state. This will solve any issue that was keep hapenning even after fresh
install of the emulator until you manually clear the app data.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:24 -04:00
FearlessTobi
5fbaea9a5e build: Fix abiFilter 2020-05-20 17:35:23 -04:00
FearlessTobi
e0d336cd7c jni/config: Set is_new_3ds to true by default 2020-05-20 17:35:23 -04:00
zhupengfei
8c0342511d res: Add Chinese (zh) translation 2020-05-20 17:35:23 -04:00
FearlessTobi
ca85bff8d2 android/strings: Add German translation 2020-05-20 17:35:23 -04:00
FearlessTobi
11c9050e53 Android: Reflect the settings that is being used by the emulator on the UI
Per-Game settings now load the settings in this order
1. Emulator Settings
2. Default Settings of the Game that we ship with Dolphin
3. Custom game settings the user have

where the later always overides the former in case of collision, then
we show that on the UI to make it clear to the user which settings being
used.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:23 -04:00
FearlessTobi
0094dd0359 strings: Add the performance warning to the translatable strings 2020-05-20 17:35:23 -04:00
FearlessTobi
de82dad544 Android: Fix custom game settings
Apparently there was a different section names used by the custom game
settings that caused Android to have those settings broken for
some sections like the graphics one. This adds the map between the generic
settings <> custom settings.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:23 -04:00
FearlessTobi
ef870fa2a6 Android: Refactor the settings managemnt
1. Create Settings class the encaupslate the loading/saving of all settings
2. Decouple the logic of saving the settings into 3 different config files
from the UI code.
2020-05-20 17:35:22 -04:00
FearlessTobi
32c619bd41 Android: Start structure the project around features instead of data types
This only moves the settings feature, the rest will be moved slowely later.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:22 -04:00
zhupengfei
c324cc24be android/swkbd: Fix rotation crash
Create a new DialogFragment to manage the dialog's state.
Also replaced AlertDialog with the androidx one which arguably looks better.
2020-05-20 17:35:22 -04:00
FearlessTobi
d414b0485f Clean up unused resources and resolve linter warnings 2020-05-20 17:35:22 -04:00
BreadFish64
b864007320 android/config: initialize cpu_clock_frequency
leaving it uninitialized broke some games for obvious reasons
2020-05-20 17:35:22 -04:00
BreadFish64
367471ddfb android: don't use ScopeAcquireContext in RunCitra
The context will be released when window is destroyed
2020-05-20 17:35:22 -04:00
zhupengfei
e01c8c5ca0 Fix incorrect import due to library change 2020-05-20 17:35:22 -04:00
zhupengfei
a24befc325 android: SoftwareKeyboard implementation 2020-05-20 17:35:21 -04:00
BreadFish64
2206b27ceb android/ndk_motion: remove ALooper_release from ndk_motion destructor
We didn't acquire the looper from another thread so this is not correct
2020-05-20 17:35:21 -04:00
BreadFish64
9930fdbf28 android: jni cleanup 2020-05-20 17:35:21 -04:00
FearlessTobi
03568fdb93 Remove unused code 2020-05-20 17:35:21 -04:00
FearlessTobi
4e3829b502 Fix a bunch of issues found by the Linter and rename Dolphin functions to Citra 2020-05-20 17:35:21 -04:00
FearlessTobi
48aa7cf902 Android: Set up Day/Night mode for system-compatible optional dark theme
Original commit by TheRealPSV for Dolphin-emu.
2020-05-20 17:35:21 -04:00
FearlessTobi
143c729c4d Android: Fix displaying checkbox settings with no description
bugs.dolphin-emu.org/issues/11904.

Original commit by JosJuice for Dolphin-emu.
2020-05-20 17:35:21 -04:00
FearlessTobi
dc9d9b1b6f Moves AlertDialogs imports to AndroidX and fix tabs background color
Original commit by rafaeltoledo for Dolphin-emu.
2020-05-20 17:35:21 -04:00
FearlessTobi
3f7cccb6a5 Bumps compile API to 29 (Q) removes unecessary casts and deprecated calls
This will help the Android client to evolve with the latest libraries (as the legacy support libs will not be shipped anymore with the com.android.support package).

This PR also makes the app compliant with the new API requirements to start later this year:
android-developers.googleblog.com/2019/02/expanding-target-api-level-requirements.html.

Original commit by rafaeltoledo for Dolphin-emu.
2020-05-20 17:35:21 -04:00
FearlessTobi
31c702b89b Migrate to AndroidX
Support Libraries are outdated and AndroidX is recommended instead. Read more here: developer.android.com/jetpack/androidx.

Original commit by Simonx22 for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
b2021d64a3 Android: Show files in the directory picker
People in the Google Play reviews still seem to be confused about
games not showing up in the directory picker, so let's show them
even though they can't be selected. (Either that or they haven't
realized that they need to extract their pirated games.)

Original commit by JosJuice for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
2dddb6fd9b Android: Expand the comment for NVidiaShieldWorkaroundView
This text has been taken from the message of the commit that added
the class. (I don't have an Nvidia Shield to reproduce the bug with.)

Original commit by JosJuice for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
c7533cf649 Remove unused code
Original commit by weihuoya for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
4df6661f70 Fix for crash when switching to landscape mode
bugs.dolphin-emu.org/issues/10815

Original commit by allanxp4 for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
33781258aa Android: Bunch of cleanups & Updates
1. Update Picasso to latest version
2. Remove some unused code

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
8ed83140e7 Android: fix/ignore L2/R2 buttons
L2/R2 will trigger a key press and an axis event if the trigger is pressed fully down
Was incorrectly ignoring L1/R1 key presses.

Original commit by zackhow for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
70431dc86f Android: don't try to pause emulation when not running
Forcing landscape at emulation start revealed a bug where if the activity was
recreated before emulation started then it would get stuck in a paused state.

Original commit by zackhow for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
8a4c9934d9 Android: Remove the cancel button from the file browser
Fix the regresion from dolphin-emu/dolphin#7520, also it applies the change
to the directory picker only.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:20 -04:00
FearlessTobi
3c289087c2 Android: Change the file browser dialog ok button title
I hope this will make it more clear to users that they are suppose to
select the dircetory that has the games.

Original commit by mahdihijazi for Dolphin-emu.
2020-05-20 17:35:19 -04:00
FearlessTobi
a6ef15d0c6 android: remove enter and exit transition
Originally done by weihuoya for Dolphin-emu.
2020-05-20 17:35:19 -04:00
FearlessTobi
e42dffba64 Android: Run Directory Initialization as a thread instead of service
Two reasons for this change. First, it appears that some android launchers do some sort of call into
the application when long pressing the app icon, which in turn calls the DirectoryInit service. This
was ok to do prior to Oreo but will cause crashes with the new restrictions on services running
in the background. Which leads to the second reason that DirectoryInit doesn't need to be a service
at all since these actions are required for dolphin to function and shouldn't be a scheduled action.
So we instead just kick this off in a new thread and send the broadcast when done.

Original commit by zackhow for Dolphin-emu
2020-05-20 17:35:19 -04:00
Marshall Mohror
09e886325f android: implement motion controls via device sensors 2020-05-20 17:35:19 -04:00
SachinVin
6bfabb89a9 android: jni: Remove unused ndk_helper after EGL migration + cleanup jni/CMakeLists.txt 2020-05-20 17:35:19 -04:00
BreadFish64
517640da96 android: allow changing settings while game is running 2020-05-20 17:35:19 -04:00
BreadFish64
015ec0f33a video_core: move buffer resize before GLES check
preventing the assert in FlushGLBuffer
2020-05-20 17:35:19 -04:00
BreadFish64
c76da1b975 video_core: disable glTextureBarrier for GLES
GLES doesn't support it
2020-05-20 17:35:19 -04:00
bunnei
209bf41ea9 android: frontend: Limit game list to a single directory. 2020-05-20 17:35:18 -04:00
bunnei
699602aa78 android: frontend: MainActivity: Fix issues with declining app permissions. 2020-05-20 17:35:18 -04:00
bunnei
8eb9267b9b android: build.gradle: Bump minimum version to 26. 2020-05-20 17:35:18 -04:00
bunnei
1fdc9eab84 android: Rename app package from citra_android to citra_emu.
- There is already a conflicting name on the Play Store.
2020-05-20 17:35:18 -04:00
bunnei
280c86c941 android: emu_window: Fix surface width/height.
- Fixes a bug when resuming the app.
2020-05-20 17:35:18 -04:00
bunnei
d0880cdf10 android: AndroidManifest: Use singleTop mode, not singleInstance.
- Fixes launching the app without restarting the emulation activity.
2020-05-20 17:35:18 -04:00
bunnei
afb23572b5 android: frontend: game_list: Move add directory button to top bar. 2020-05-20 17:35:18 -04:00
bunnei
716df842e4 android: frontend: game_list: Tweak game cards. 2020-05-20 17:35:17 -04:00
bunnei
62bad16dbf android: frontend: game_list: Rounded icons and other UX improvements. 2020-05-20 17:35:17 -04:00
bunnei
7e5860dd17 android: frontend: Add vsync to the settings now that it is supported. 2020-05-20 17:35:17 -04:00
bunnei
459072362b android: emu_window: Adapt for use with split presenter thread. 2020-05-20 17:35:17 -04:00
bunnei
fce13f0ed0 android: Update .gitignore for CXX compile cache. 2020-05-20 17:35:17 -04:00
bunnei
9f81e0eb17 android: renderer_opengl: Various fixes for GLES. 2020-05-20 17:35:17 -04:00
bunnei
4fbdda4daa android: gradle: Update. 2020-05-20 17:35:17 -04:00
bunnei
cc31257862 android: log: TrimSourcePath: Cannot be constexpr. 2020-05-20 17:35:17 -04:00
SachinVin
45f4532c39 Android: frontend: use android.R.string.ok instead of literal 2020-05-20 17:35:17 -04:00
SachinVin
bee8b0fc0a Android: Settings: Center the setting title when no setting description is provided. 2020-05-20 17:35:17 -04:00
bunnei
577b3884a0 android: dsp_dsp: Move a noisy log statement to trace. 2020-05-20 17:35:16 -04:00
bunnei
91cfa6fab9 android: video_core: Enable HW shadows and texture barrier on GLES. 2020-05-20 17:35:16 -04:00
bunnei
a2ec3d7b11 video_core: gl_shader_manager: Only set shader samplers on fragment shader.
- Avoids unnecessary uniform errors.
2020-05-20 17:35:16 -04:00
bunnei
9671238965 android: video_core: Require GLES 3.2 in shaders. 2020-05-20 17:35:16 -04:00
bunnei
9144c9d285 android: native: Change order of EmuWindow teardown.
- This needs to happen after system shutdown for asynchronous GPU.
2020-05-20 17:35:16 -04:00
bunnei
70d6b1e66c android: video_core: gl_rasterizer_cache: Make cache access thread safe. 2020-05-20 17:35:16 -04:00
bunnei
04ad2002a3 android: audio_core: Remove noisy log. 2020-05-20 17:35:16 -04:00
bunnei
f4b5520730 android: core: frontend: Port yuzu's code for scope acquire window context. 2020-05-20 17:35:16 -04:00
bunnei
9c2acd84f3 android: res: Update the launcher icon. 2020-05-20 17:35:16 -04:00
bunnei
1537492d89 android: frontend: FPS overlay: Add some minor margin. 2020-05-20 17:35:15 -04:00
bunnei
1249648e34 android: jni: Migrate EmuWindow_Android class to EGL.
- This enables us to use shared contexts more easily.
2020-05-20 17:35:15 -04:00
bunnei
30ef4a1b7c android: frontend: Add errors for unsupported ROM formats. 2020-05-20 17:35:15 -04:00
bunnei
abfa49a6d7 android: renderer_opengl: Partially implement glLogicOp on GLES. 2020-05-20 17:35:15 -04:00
SachinVin
9c6448460b android/GameDatabase.java: remove duplicate ".3ds" from allowedExtensions 2020-05-20 17:35:15 -04:00
bunnei
69ec58391f android: renderer_opengl: Optimize GetTexImageOES and fix bugs. 2020-05-20 17:35:15 -04:00
bunnei
4fc6b7ce12 android: settings: Add system language setting. 2020-05-20 17:35:15 -04:00
bunnei
eaed3f4b55 android: settings: Fix bug where changing speed limiter will slow game down. 2020-05-20 17:35:15 -04:00
bunnei
dde1ca7d84 android: frontend: Support 18.5:9 aspect ratio. 2020-05-20 17:35:14 -04:00
bunnei
a0c4c702ab android: frontend: Further simplify show FPS overlay text. 2020-05-20 17:35:14 -04:00
bunnei
803b999a57 android: frontend: Fix several issues with running notification.
- Priority should be low without sound/vibration.
- Notification should restore app.
2020-05-20 17:35:14 -04:00
bunnei
a3d6c072e9 android: frontend: Use color white for FPS overlay. 2020-05-20 17:35:14 -04:00
bunnei
e8e162c163 android: frontend: settings: Temporarily disable V-Sync. 2020-05-20 17:35:14 -04:00
bunnei
92f7bf2352 android: frontend: Rename settings hint to "Settings". 2020-05-20 17:35:14 -04:00
bunnei
580915db1b android: frontend: New and improved launcher icon. 2020-05-20 17:35:14 -04:00
bunnei
67b62dcc35 android: frontend: startup: Improve startup and permissions handling.
- Fixes a first boot crash.
2020-05-20 17:35:14 -04:00
bunnei
b50b4f4983 android: frontend: settings: Disable audio stretching by default. 2020-05-20 17:35:14 -04:00
bunnei
1d0c62598c android: frontend: settings: Accurate shader multiplication should be disabled. 2020-05-20 17:35:14 -04:00
bunnei
5656a1205f android: frontend: settings: Simplify sliders. 2020-05-20 17:35:13 -04:00
bunnei
d43cca1c5a android: frontend: menu: Improve in game options, make check boxes. 2020-05-20 17:35:13 -04:00
bunnei
4ab3aa1dd2 android: frontend: Implement persistent notification while emulator is running. 2020-05-20 17:35:13 -04:00
bunnei
f740eb6e6f android: Rename main entry class to CitraApplication. 2020-05-20 17:35:13 -04:00
bunnei
36a15cbbd5 android: frontend: Implement basic software keyboard applet. 2020-05-20 17:35:13 -04:00
bunnei
781913f230 android: frontend: Use android builtin ok/yes/no/cancel strings where possible. 2020-05-20 17:35:13 -04:00
bunnei
ad4fdc814e android: frontend: Add "Cancel" and "Default" buttons to reset overlay. 2020-05-20 17:35:13 -04:00
bunnei
4d519c0548 android: frontend: Fix bug with reset overlay scale. 2020-05-20 17:35:13 -04:00
bunnei
195b07d31a android: frontend: Cleanup perf stats. 2020-05-20 17:35:13 -04:00
bunnei
82e8936c02 android: frontend: card_game: Tighten padding a little bit. 2020-05-20 17:35:12 -04:00
bunnei
ae211ca371 android: frontend: gamelist: Save PlatformGamesFragment state.
- Fixes weird duplication of game list on rotation.
2020-05-20 17:35:12 -04:00
bunnei
576d65bc1d android: frontend: gamelist: Fix weird scroll behavior with action bar. 2020-05-20 17:35:12 -04:00
bunnei
9d17ad81b5 android: jni: Improve management of core emulation state.
- Furthermore fixes bug where audio crackling could bleed out of the app.
2020-05-20 17:35:12 -04:00
bunnei
368f6e24e5 android: frontend: settings: Add a back button to the navigation bar. 2020-05-20 17:35:12 -04:00
bunnei
70c744ade9 android: frontend: gamelist: Add swipe to refresh gesture. 2020-05-20 17:35:12 -04:00
bunnei
2528eceb33 android: frontend: settings: Disable configuring D-pad as buttons. 2020-05-20 17:35:12 -04:00
bunnei
840cccafc4 android: frontend: settings: Rename "controller" section to "gamepad". 2020-05-20 17:35:12 -04:00
bunnei
1a8dec0d5e android: frontend: menu_settings: Remove save button. 2020-05-20 17:35:12 -04:00
bunnei
bb14b8db3a android: frontend: fragment_settings: Remove margins. 2020-05-20 17:35:12 -04:00
bunnei
ba2e3423b4 android: frontend: settings: Simply save toast. 2020-05-20 17:35:11 -04:00
bunnei
25f7e88d14 android: frontend: EmulationActivity: Remove citra icon from game prompt. 2020-05-20 17:35:11 -04:00
bunnei
69eb3ea826 gl_rasterizer_cache: Remove redundant GLES check. 2020-05-20 17:35:11 -04:00
bunnei
d7dcf30f44 android: gl_rasterizer_cache: Skip costly shutdown procedure. 2020-05-20 17:35:11 -04:00
bunnei
a0b83256da android: jni: Fix how we handle orientation changes.
- This previously broke Jave to C++ bindings.
2020-05-20 17:35:11 -04:00
bunnei
a473b8ce51 core: Reset cpu_core after kernel.
- Fixes a crash on Android.
2020-05-20 17:35:11 -04:00
bunnei
5f6e391a77 android: jni: config: Disable shaders_accurate_mul by default (it's too slow). 2020-05-20 17:35:11 -04:00
bunnei
4304704627 gl_shader_decompiler: Improve performance of accurate_mul on Android. 2020-05-20 17:35:11 -04:00
bunnei
f5c9d06621 android: jni: Add IDCache to cache Java methods. 2020-05-20 17:35:11 -04:00
bunnei
9aa50b91b7 android: Picasso: Use newer version and some minor cleanup. 2020-05-20 17:35:11 -04:00
bunnei
9f802ffa26 android: AndroidManifest.xml: Require GLES 3.1 and AEP. 2020-05-20 17:35:10 -04:00
bunnei
4def69efe2 android: native: Reset old EmuWindow before constructing a new one.
- Enforces that touch is unregistered at the right time, ensuring it works for subsequent runs.
2020-05-20 17:35:10 -04:00
bunnei
ed094d5cbe android: native: Use actual array length for banner copy. 2020-05-20 17:35:10 -04:00
bunnei
95179be381 android: frontend: Add config option for linear filtering. 2020-05-20 17:35:10 -04:00
bunnei
91c0bfceac android: settings: Add/update missing config options from latest master. 2020-05-20 17:35:10 -04:00
bunnei
6e60a0b053 android: settings: Remove shaders_accurate_gs and toggle_3d.
- shaders_accurate_gs no longer exists.
- toggle_3d was renamed, but is not used on Android.
2020-05-20 17:35:10 -04:00
bunnei
d1576e68dc android: input: Add support for gamepads. 2020-05-20 17:35:10 -04:00
bunnei
fb1428b85a android: frontend: SettingsFragmentPresenter: Add config options for joypad controls. 2020-05-20 17:35:10 -04:00
bunnei
1707027259 android: frontend: MotionAlertDialog: Merge latest Dolphin code. 2020-05-20 17:35:10 -04:00
Weiyi Wang
10e2c693b9 Add reset button to slider setting 2020-05-20 17:35:10 -04:00
Weiyi Wang
66b1edc33c move speed limiter to general 2020-05-20 17:35:09 -04:00
bunnei
1c18677f08 android: frontend: SettingsFile: Add button strings. 2020-05-20 17:35:09 -04:00
bunnei
d9fa0e4563 android: frontend: strings: Cleanup for controller input. 2020-05-20 17:35:09 -04:00
bunnei
dd0a222351 android: app: Add method to get global context. 2020-05-20 17:35:09 -04:00
bunnei
ce459d0dd3 android: jni: Remove unnecessary code. 2020-05-20 17:35:09 -04:00
Weiyi Wang
1e864c170a remove redundant code 2020-05-20 17:35:09 -04:00
Weiyi Wang
d1c295f372 Also fix default value for speed limiter & audio stretcher 2020-05-20 17:35:09 -04:00
Weiyi Wang
080bcf5192 Accurate GS is default to true in config 2020-05-20 17:35:09 -04:00
Weiyi Wang
b0de07c75c fix system clock default value 2020-05-20 17:35:09 -04:00
Weiyi Wang
19dd006636 fix time picker not saving/loading time 2020-05-20 17:35:09 -04:00
Weiyi Wang
d0f63a2247 Fix TimePicker style 2020-05-20 17:35:08 -04:00
Weiyi Wang
e6198cf13d move "select game folder" string to resource 2020-05-20 17:35:08 -04:00
Weiyi Wang
2df789a92d Add title to file picker 2020-05-20 17:35:08 -04:00
bunnei
530440b357 android: config: Enable accurate multiplication by default. 2020-05-20 17:35:08 -04:00
bunnei
b01dfbed3a android: frontend: Update circle pad icon resources. 2020-05-20 17:35:08 -04:00
bunnei
e043b559f2 android: frontend: Swap select and start buttons to match 3DS. 2020-05-20 17:35:08 -04:00
bunnei
85f676788d android: frontend: settings: String cleanup & minor improvements. 2020-05-20 17:35:08 -04:00
bunnei
62fd471bfe android: native: Ensure game config is re-loaded before starting. 2020-05-20 17:35:08 -04:00
bunnei
37f140a50a android: frontend: Remove Home button for now, as it does not do anything. 2020-05-20 17:35:07 -04:00
bunnei
50cf743ba8 android: frontend: Add a confirmation dialog on game exit. 2020-05-20 17:35:07 -04:00
bunnei
f299c55817 android: frontend: Update controls overlay placement for new icon resources. 2020-05-20 17:35:07 -04:00
bunnei
68b7df84a8 android: frontend: Update to new icon resources. 2020-05-20 17:35:07 -04:00
bunnei
685b987c43 android: frontend: settings: Add performance warnings for relevant settings. 2020-05-20 17:35:07 -04:00
bunnei
a758bdd3ce android: frontend: Remove "Toggle All" option from toggle controls.
- This was broken, and is not terribly useful as-is.
2020-05-20 17:35:07 -04:00
bunnei
01516f61ba android: frontend: Decrease spacing between game cards. 2020-05-20 17:35:06 -04:00
bunnei
87dd8888a3 android: jni: Fix management of core emulation state and various cleanups.
- Fixes a shutdown crash.
2020-05-20 17:35:06 -04:00
bunnei
70c0f60d38 android: frontend: Fix bug where games could be double-clicked. 2020-05-20 17:35:06 -04:00
bunnei
d6e210640b android: jni: Sanitize analog stick inputs.
- Fixes bug where joystick sometimes is unresponsive.
2020-05-20 17:35:06 -04:00
bunnei
a992286886 android: frontend: Fix default state for toggle controls, enable D-pad by default. 2020-05-20 17:35:06 -04:00
bunnei
19c8f28aff android: frontend: Add persistent changeable layout option for landscape mode. 2020-05-20 17:35:06 -04:00
bunnei
d514c81d16 android: frontend: Track screen layout separately for orientation. 2020-05-20 17:35:06 -04:00
bunnei
9574dee267 android: frontend: Add MobileLandscape layout profile for mobile devices. 2020-05-20 17:35:06 -04:00
James Rowe
e0d00fd634 Adds in missing changes to gradle file and updates dynarmic 2020-05-20 17:35:06 -04:00
bunnei
9f0ae10e44 android: jni: button_manager: Fix circle pad on subsequent game launch.
- We were missing an UnregisterFactory call for AnalogDevice.
2020-05-20 17:35:05 -04:00
bunnei
12a3c226b5 android: frontend: MainPresenter: Refresh game directory on app boot. 2020-05-20 17:35:05 -04:00
bunnei
78527f6063 android: jni: game_info: Fix crash on banner load for missing title. 2020-05-20 17:35:05 -04:00
bunnei
219b65af1c android: jni: config: Fix bug preventing creation of config.ini. 2020-05-20 17:35:05 -04:00
bunnei
7a5e441136 android: frontend: StartupHandler: Add an intro sequence to pick game dir. 2020-05-20 17:35:05 -04:00
bunnei
d9570bffc5 android: frontend: SettingsFragmentPresenter: Default resolution scale to 1X.
- Because android phones aren't very fast.
2020-05-20 17:35:05 -04:00
bunnei
f45a7ab462 android: core: Prepare for ARM64 dynarmic support. 2020-05-20 17:35:05 -04:00
bunnei
04a2b89f65 android: frontend: Settings: Section categories must match INI settings. 2020-05-20 17:35:05 -04:00
bunnei
fb4d39c20e android: frontend: Settings: Various updates and preserve single choice text in UI. 2020-05-20 17:35:05 -04:00
bunnei
a349bc547d android: frontend: SettingsFragmentPresenter: Move speed limit to Graphics, give proper strings. 2020-05-20 17:35:04 -04:00
bunnei
981e4a7f63 android: frontend: Enlarge icon for launcher. 2020-05-20 17:35:04 -04:00
bunnei
7dfca5ddbb android: frontend: SettingsFragmentPresenter: Organize settings into logical categories. 2020-05-20 17:35:04 -04:00
bunnei
772630f904 android: frontend: SettingsFragmentPresenter: Remove unnecessary settings, add JIT setting. 2020-05-20 17:35:04 -04:00
bunnei
18622b3a14 android: frontend: InputOverlay: Fix setting save for portrait mode. 2020-05-20 17:35:04 -04:00
bunnei
b53bfcc6de android: frontend: Fix settings slider cancel button. 2020-05-20 17:35:04 -04:00
bunnei
0011a432bf android: frontend: auto-reformat all code for consistent style. 2020-05-20 17:35:04 -04:00
bunnei
59b4d4dda7 android: dynarmic: Use internal dynarmic submodule. 2020-05-20 17:35:04 -04:00
James Rowe
817b68f59f Perf: Remove more breakpoint checking in the interpreter. Move filtering earlier in the logging chain 2020-05-20 17:35:03 -04:00
bunnei
f0f60fc9f8 (jroweboy) Remove existing code in src/android
Move src/citra_android to src/android/app/src/main/jni
Disable gdbstub breakpoints on android (could be done better)
Disable LOD_BIAS for GLES (not support on gles)
2020-05-20 17:35:03 -04:00
bunnei
3ed6e66eeb android: frontend: Implement MobilePortrait layout, which makes more sense for mobile. 2020-05-20 17:35:03 -04:00
bunnei
0afe3d986e android: native: Remove several unused hooks. 2020-05-20 17:35:03 -04:00
bunnei
aaf01b0c68 android: native: Add hooks for SwitchScreenLayout and SwapScreens. 2020-05-20 17:35:02 -04:00
bunnei
8d04cac55c android: config: Update to reflect latest settings. 2020-05-20 17:35:02 -04:00
bunnei
24aa429488 android: frontend: Add base project. 2020-05-20 17:35:02 -04:00
428 changed files with 19406 additions and 1589 deletions

5
.gitmodules vendored
View File

@ -12,7 +12,7 @@
url = https://github.com/philsquared/Catch.git url = https://github.com/philsquared/Catch.git
[submodule "dynarmic"] [submodule "dynarmic"]
path = externals/dynarmic path = externals/dynarmic
url = https://github.com/MerryMage/dynarmic.git url = https://github.com/citra-emu/dynarmic-android
[submodule "xbyak"] [submodule "xbyak"]
path = externals/xbyak path = externals/xbyak
url = https://github.com/herumi/xbyak.git url = https://github.com/herumi/xbyak.git
@ -49,3 +49,6 @@
[submodule "zstd"] [submodule "zstd"]
path = externals/zstd path = externals/zstd
url = https://github.com/facebook/zstd.git url = https://github.com/facebook/zstd.git
[submodule "libyuv"]
path = externals/libyuv
url = https://github.com/lemenkov/libyuv.git

View File

@ -31,14 +31,12 @@ target_include_directories(catch-single-include INTERFACE catch/single_include)
add_subdirectory(cryptopp) add_subdirectory(cryptopp)
# Dynarmic # Dynarmic
if (ARCHITECTURE_x86_64) # Dynarmic will skip defining xbyak if it's already defined, we then define it below
# Dynarmic will skip defining xbyak if it's already defined, we then define it below add_library(xbyak INTERFACE)
add_library(xbyak INTERFACE) option(DYNARMIC_TESTS OFF)
option(DYNARMIC_TESTS OFF) set(DYNARMIC_NO_BUNDLED_FMT ON)
set(DYNARMIC_NO_BUNDLED_FMT ON) set(DYNARMIC_FRONTENDS "A32")
set(DYNARMIC_FRONTENDS "A32") add_subdirectory(dynarmic)
add_subdirectory(dynarmic)
endif()
# libfmt # libfmt
add_subdirectory(fmt) add_subdirectory(fmt)
@ -131,4 +129,8 @@ if (ENABLE_WEB_SERVICE)
endif() endif()
# lodepng # lodepng
add_subdirectory(lodepng) add_subdirectory(lodepng)
# libyuv
add_subdirectory(libyuv)
target_include_directories(yuv INTERFACE ./libyuv/include)

2
externals/dynarmic vendored

@ -1 +1 @@
Subproject commit 8d1699ba2db216e569e998ea318d5cde47720e97 Subproject commit b80bde7c0d70e030b3487e6fc70818955402729a

1
externals/libyuv vendored Submodule

@ -0,0 +1 @@
Subproject commit 45f1f2b201672b699b35da20267a5f2c41318264

View File

@ -110,8 +110,10 @@ endif()
if (ENABLE_QT) if (ENABLE_QT)
add_subdirectory(citra_qt) add_subdirectory(citra_qt)
endif() endif()
if (ANDROID) if (ANDROID)
add_subdirectory(android/app/src/main/cpp) include_directories(android/app/src/main)
add_subdirectory(android/app/src/main/jni)
else() else()
add_subdirectory(dedicated_room) add_subdirectory(dedicated_room)
endif() endif()

View File

@ -1,10 +1,62 @@
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml *.iml
.gradle .idea/
/local.properties
/.idea/libraries # Keystore files
/.idea/modules.xml # Uncomment the following line if you do not want to check your keystore files in.
/.idea/workspace.xml #*.jks
.DS_Store
/build # External native build folder generated in Android Studio 2.2 and later
/captures
.externalNativeBuild .externalNativeBuild
# CXX compile cache
app/.cxx
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

View File

@ -1,8 +1,16 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
/**
* Use the number of seconds/10 since Jan 1 2016 as the versionCode.
* This lets us upload a new build at most every 10 seconds for the
* next 680 years.
*/
def autoVersion = (int) (((new Date().getTime() / 1000) - 1451606400) / 10)
def buildType
def abiFilter = "arm64-v8a" //, "x86"
android { android {
compileSdkVersion 26 compileSdkVersion 29
buildToolsVersion '28.0.3'
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -13,34 +21,53 @@ android {
// This is important as it will run lint but not abort on error // This is important as it will run lint but not abort on error
// Lint has some overly obnoxious "errors" that should really be warnings // Lint has some overly obnoxious "errors" that should really be warnings
abortOnError false abortOnError false
//Uncomment disable lines for test builds...
//disable 'MissingTranslation'bin
//disable 'ExtraTranslation'
} }
defaultConfig { defaultConfig {
applicationId "org.citra_emu" // TODO If this is ever modified, change application_id in strings.xml
minSdkVersion 21 applicationId "org.citra.citra_emu"
targetSdkVersion 26 minSdkVersion 26
targetSdkVersion 28
versionCode(getBuildVersionCode()) versionCode autoVersion
ndk.abiFilters abiFilter
versionName "${getVersion()}"
} }
signingConfigs { signingConfigs {
release { //release {
if (project.hasProperty('keystore')) { // storeFile file('')
storeFile file(project.property('keystore')) // storePassword System.getenv('ANDROID_KEYPASS')
storePassword project.property('storepass') // keyAlias = 'key0'
keyAlias project.property('keyalias') // keyPassword System.getenv('ANDROID_KEYPASS')
keyPassword project.property('keypass') //}
} }
}
applicationVariants.all { variant ->
buildType = variant.buildType.name // sets the current build type
} }
// Define build types, which are orthogonal to product flavors. // Define build types, which are orthogonal to product flavors.
buildTypes { buildTypes {
// Signed by release key, allowing for upload to Play Store. // Signed by release key, allowing for upload to Play Store.
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.debug
}
// builds a release build that doesn't need signing
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
relWithDebInfo {
initWith release
applicationIdSuffix ".debug"
versionNameSuffix '-debug'
signingConfig signingConfigs.debug
minifyEnabled false
testCoverageEnabled false
debuggable true
jniDebuggable true
} }
// Signed by debug key disallowing distribution on Play Store. // Signed by debug key disallowing distribution on Play Store.
@ -49,13 +76,14 @@ android {
// TODO If this is ever modified, change application_id in debug/strings.xml // TODO If this is ever modified, change application_id in debug/strings.xml
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
versionNameSuffix '-debug' versionNameSuffix '-debug'
debuggable true
jniDebuggable true jniDebuggable true
} }
} }
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version getCmakeVersion() version "3.10.2"
path "../../../CMakeLists.txt" path "../../../CMakeLists.txt"
} }
} }
@ -65,76 +93,32 @@ android {
cmake { cmake {
arguments "-DENABLE_QT=0", // Don't use QT arguments "-DENABLE_QT=0", // Don't use QT
"-DENABLE_SDL2=0", // Don't use SDL "-DENABLE_SDL2=0", // Don't use SDL
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work "-DENABLE_WEB_SERVICE=0", // Don't use telemetry
"-DENABLE_CUBEB=0", "-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work
"-DANDROID_STL=c++_shared"
abiFilters "arm64-v8a" abiFilters abiFilter
targets "citra-android"
} }
} }
} }
} }
ext {
androidSupportVersion = '26.1.0'
}
dependencies { dependencies {
implementation "com.android.support:support-v13:$androidSupportVersion" implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "com.android.support:cardview-v7:$androidSupportVersion" implementation 'androidx.exifinterface:exifinterface:1.2.0'
implementation "com.android.support:recyclerview-v7:$androidSupportVersion" implementation 'androidx.cardview:cardview:1.0.0'
implementation "com.android.support:design:$androidSupportVersion" implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
// Android TV UI libraries. // For loading huge screenshots from the disk.
implementation "com.android.support:leanback-v17:$androidSupportVersion" implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.android.support.constraint:constraint-layout:1.1.0' // 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:1.1.3'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
testImplementation "com.android.support.test:runner:1.0.2" implementation 'com.android.billingclient:billing:2.2.0'
androidTestImplementation "com.android.support.test:runner:1.0.1"
}
def getVersion() {
def versionNumber = '0.0'
try {
versionNumber = 'git describe --always --long'.execute([], project.rootDir).text
.trim()
.replaceAll(/(-0)?-[^-]+$/, "")
} catch (Exception e) {
logger.error('Cannot find git, defaulting to dummy version number')
}
return versionNumber
}
def getBuildVersionCode() {
try {
def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text
.trim()
return Integer.valueOf(versionNumber)
} catch (Exception e) {
logger.error('Cannot find git, defaulting to dummy version number')
}
return 0
}
def getCmakeVersion() {
try {
// Tokenized form of the output will be - ["cmake", "version", "M.m.p-rcx"], the version number
// will be at index 2
def version_string = 'cmake -version'.execute([], project.rootDir).text
.trim().tokenize()[2]
return version_string
}
catch(Exception e) {
logger.error('Cannot find Cmake, using default Cmake')
}
return null
} }

View File

@ -1,4 +1,4 @@
package org.citra_emu.citra; package org.citra.citra_emu;
import android.content.Context; import android.content.Context;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
// Context of the app under test. // Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext(); Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("org.citra_emu.citra_android", appContext.getPackageName()); assertEquals("org.citra.citra_emu", appContext.getPackageName());
} }
} }

View File

@ -1,39 +1,89 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.citra_emu.citra"> package="org.citra.citra_emu">
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
<uses-feature <uses-feature
android:name="android.hardware.gamepad" android:name="android.hardware.gamepad"
android:required="false"/> android:required="false"/>
<uses-feature android:glEsVersion="0x00030001" /> <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.camera.any"
android:required="false" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application <application
android:name="org.citra_emu.citra.CitraApplication" android:name="org.citra.citra_emu.CitraApplication"
android:label="Citra" android:label="@string/app_name"
android:icon="@mipmap/ic_citra" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="false"
android:supportsRtl="true" android:supportsRtl="true"
android:isGame="true" android:isGame="true"
android:banner="@mipmap/ic_citra"> android:banner="@mipmap/ic_launcher">
<meta-data android:name="android.max_aspect" android:value="2.1" />
<activity <activity
android:name=".ui.main.MainActivity" android:name="org.citra.citra_emu.ui.main.MainActivity"
android:theme="@style/CitraBase"> android:theme="@style/CitraBase"
android:resizeableActivity="false">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="org.citra.citra_emu.features.settings.ui.SettingsActivity"
android:configChanges="orientation|screenSize|uiMode"
android:theme="@style/CitraSettingsBase"
android:label="@string/preferences_settings"/>
<activity
android:name="org.citra.citra_emu.activities.EmulationActivity"
android:resizeableActivity="false"
android:theme="@style/CitraEmulationBase"
android:launchMode="singleTop"/>
<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>
<service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
<provider
android:name="org.citra.citra_emu.model.GameProvider"
android:authorities="${applicationId}.provider"
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> </application>
</manifest> </manifest>

View File

@ -1,16 +0,0 @@
cmake_minimum_required(VERSION 3.8)
add_library(citra-android SHARED
logging/log.cpp
logging/logcat_backend.cpp
logging/logcat_backend.h
native_interface.cpp
native_interface.h
ui/main/main_activity.cpp
)
# find Android's log library
find_library(log-lib log)
target_link_libraries(citra-android ${log-lib} core common inih)
target_include_directories(citra-android PRIVATE "../../../../../" "./")

View File

@ -1,15 +0,0 @@
#include "common/logging/log.h"
#include "native_interface.h"
namespace Log {
extern "C" {
JNICALL void Java_org_citra_1emu_citra_LOG_logEntry(JNIEnv* env, jclass type, jint level,
jstring file_name, jint line_number,
jstring function, jstring msg) {
using CitraJNI::GetJString;
FmtLogMessage(Class::Frontend, static_cast<Level>(level), GetJString(env, file_name).data(),
static_cast<unsigned int>(line_number), GetJString(env, function).data(),
GetJString(env, msg).data());
}
}
} // namespace Log

View File

@ -1,38 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <android/log.h>
#include "common/assert.h"
#include "common/logging/text_formatter.h"
#include "logcat_backend.h"
namespace Log {
void LogcatBackend::Write(const Entry& entry) {
android_LogPriority priority;
switch (entry.log_level) {
case Level::Trace:
priority = ANDROID_LOG_VERBOSE;
break;
case Level::Debug:
priority = ANDROID_LOG_DEBUG;
break;
case Level::Info:
priority = ANDROID_LOG_INFO;
break;
case Level::Warning:
priority = ANDROID_LOG_WARN;
break;
case Level::Error:
priority = ANDROID_LOG_ERROR;
break;
case Level::Critical:
priority = ANDROID_LOG_FATAL;
break;
case Level::Count:
UNREACHABLE();
}
__android_log_print(priority, "citra", "%s\n", FormatLogMessage(entry).c_str());
}
} // namespace Log

View File

@ -1,22 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "common/logging/backend.h"
namespace Log {
class LogcatBackend : public Backend {
public:
static const char* Name() {
return "Logcat";
}
const char* GetName() const override {
return Name();
}
void Write(const Entry& entry) override;
};
} // namespace Log

View File

@ -1,22 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "native_interface.h"
namespace CitraJNI {
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return JNI_VERSION_1_6;
}
std::string GetJString(JNIEnv* env, jstring jstr) {
std::string result = "";
if (!jstr)
return result;
const char* s = env->GetStringUTFChars(jstr, nullptr);
result = s;
env->ReleaseStringUTFChars(jstr, s);
return result;
}
} // namespace CitraJNI

View File

@ -1,16 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <jni.h>
namespace CitraJNI {
extern "C" {
jint JNI_OnLoad(JavaVM* vm, void* reserved);
}
std::string GetJString(JNIEnv* env, jstring jstr);
} // namespace CitraJNI

View File

@ -1,31 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
#include "core/settings.h"
#include "logging/logcat_backend.h"
#include "native_interface.h"
namespace MainActivity {
extern "C" {
JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initUserPath(JNIEnv* env, jclass type,
jstring path) {
FileUtil::SetUserPath(CitraJNI::GetJString(env, path) + '/');
}
JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initLogging(JNIEnv* env, jclass type) {
Log::Filter log_filter(Log::Level::Debug);
log_filter.ParseFilterString(Settings::values.log_filter);
Log::SetGlobalFilter(log_filter);
const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir);
FileUtil::CreateFullPath(log_dir);
Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
Log::AddBackend(std::make_unique<Log::LogcatBackend>());
}
};
}; // namespace MainActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,55 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import org.citra.citra_emu.model.GameDatabase;
import org.citra.citra_emu.utils.DirectoryInitialization;
import org.citra.citra_emu.utils.PermissionsHandler;
public class CitraApplication extends Application {
public static GameDatabase databaseHelper;
private static CitraApplication application;
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.app_notification_channel_name);
String description = getString(R.string.app_notification_channel_description);
NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW);
channel.setDescription(description);
channel.setSound(null, null);
channel.setVibrationPattern(null);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
@Override
public void onCreate() {
super.onCreate();
application = this;
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
DirectoryInitialization.start(getApplicationContext());
}
createNotificationChannel();
databaseHelper = new GameDatabase(this);
}
public static Context getAppContext() {
return application.getApplicationContext();
}
}

View File

@ -0,0 +1,524 @@
/*
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.Surface;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.utils.EmulationMenuSettings;
import org.citra.citra_emu.utils.Log;
import org.citra.citra_emu.utils.PermissionsHandler;
import java.lang.ref.WeakReference;
import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.RECORD_AUDIO;
/**
* Class which contains methods that interact
* with the native side of the Citra code.
*/
public final class NativeLibrary {
/**
* Default touchscreen device
*/
public static final String TouchScreenDevice = "Touchscreen";
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
private static boolean alertResult = false;
private static String alertPromptResult = "";
private static int alertPromptButton = 0;
private static final Object alertPromptLock = new Object();
private static boolean alertPromptInProgress = false;
private static String alertPromptCaption = "";
private static int alertPromptButtonConfig = 0;
private static EditText alertPromptEditText = null;
static {
try {
System.loadLibrary("main");
} catch (UnsatisfiedLinkError ex) {
Log.error("[NativeLibrary] " + ex.toString());
}
}
private NativeLibrary() {
// Disallows instantiation.
}
/**
* Handles button press events for a gamepad.
*
* @param Device The input descriptor of the gamepad.
* @param Button Key code identifying which button was pressed.
* @param Action Mask identifying which action is happening (button pressed down, or button released).
* @return If we handled the button press.
*/
public static native boolean onGamePadEvent(String Device, int Button, int Action);
/**
* Handles gamepad movement events.
*
* @param Device The device ID of the gamepad.
* @param Axis The axis ID
* @param x_axis The value of the x-axis represented by the given ID.
* @param y_axis The value of the y-axis represented by the given ID
*/
public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
/**
* Handles gamepad movement events.
*
* @param Device The device ID of the gamepad.
* @param Axis_id The axis ID
* @param axis_val The value of the axis represented by the given ID.
*/
public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
/**
* Handles touch events.
*
* @param x_axis The value of the x-axis.
* @param y_axis The value of the y-axis
* @param pressed To identify if the touch held down or released.
*/
public static native void onTouchEvent(float x_axis, float y_axis, boolean pressed);
/**
* Handles touch movement.
*
* @param x_axis The value of the instantaneous x-axis.
* @param y_axis The value of the instantaneous y-axis.
*/
public static native void onTouchMoved(float x_axis, float y_axis);
public static native void ReloadSettings();
public static native String GetUserSetting(String gameID, String Section, String Key);
public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
public static native void InitGameIni(String gameID);
/**
* Gets the embedded icon within the given ROM.
*
* @param filename the file path to the ROM.
* @return an integer array containing the color data for the icon.
*/
public static native int[] GetIcon(String filename);
/**
* Gets the embedded title of the given ISO/ROM.
*
* @param filename The file path to the ISO/ROM.
* @return the embedded title of the ISO/ROM.
*/
public static native String GetTitle(String filename);
public static native String GetDescription(String filename);
public static native String GetGameId(String filename);
public static native String GetRegions(String filename);
public static native String GetCompany(String filename);
public static native String GetGitRevision();
/**
* Sets the current working user directory
* If not set, it auto-detects a location
*/
public static native void SetUserDirectory(String directory);
public static native String[] GetInstalledGamePaths();
// Create the config.ini file.
public static native void CreateConfigFile();
public static native int DefaultCPUCore();
/**
* Begins emulation.
*/
public static native void Run(String path);
public static native String[] GetTextureFilterNames();
/**
* Begins emulation from the specified savestate.
*/
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
// Surface Handling
public static native void SurfaceChanged(Surface surf);
public static native void SurfaceDestroyed();
/**
* Unpauses emulation from a paused state.
*/
public static native void UnPauseEmulation();
/**
* Pauses emulation.
*/
public static native void PauseEmulation();
/**
* Stops emulation.
*/
public static native void StopEmulation();
/**
* Returns true if emulation is running (or is paused).
*/
public static native boolean IsRunning();
/**
* Returns the performance stats for the current game
**/
public static native double[] GetPerfStats();
/**
* Notifies the core emulation that the orientation has changed.
*/
public static native void NotifyOrientationChange(int layout_option, int rotation);
/**
* Swaps the top and bottom screens.
*/
public static native void SwapScreens(boolean swap_screens, int rotation);
public static boolean isPortraitMode() {
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_PORTRAIT;
}
public static int landscapeScreenLayout() {
return EmulationMenuSettings.getLandscapeScreenLayout();
}
public static boolean displayAlertMsg(final String caption, final String text,
final boolean yesNo) {
Log.error("[NativeLibrary] Alert: " + text);
final EmulationActivity emulationActivity = sEmulationActivity.get();
boolean result = false;
if (emulationActivity == null) {
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
} else {
// Create object used for waiting.
final Object lock = new Object();
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
.setTitle(caption)
.setMessage(text);
// If not yes/no dialog just have one button that dismisses modal,
// otherwise have a yes and no button that sets alertResult accordingly.
if (!yesNo) {
builder
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
{
dialog.dismiss();
synchronized (lock) {
lock.notify();
}
});
} else {
alertResult = false;
builder
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
{
alertResult = true;
dialog.dismiss();
synchronized (lock) {
lock.notify();
}
})
.setNegativeButton(android.R.string.no, (dialog, whichButton) ->
{
alertResult = false;
dialog.dismiss();
synchronized (lock) {
lock.notify();
}
});
}
// Show the AlertDialog on the main thread.
emulationActivity.runOnUiThread(builder::show);
// Wait for the lock to notify that it is complete.
synchronized (lock) {
try {
lock.wait();
} catch (Exception e) {
}
}
if (yesNo)
result = alertResult;
}
return result;
}
public static void retryDisplayAlertPrompt() {
if (!alertPromptInProgress) {
return;
}
displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
}
public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
alertPromptCaption = caption;
alertPromptButtonConfig = buttonConfig;
alertPromptInProgress = true;
// Show the AlertDialog on the main thread
sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
// Wait for the lock to notify that it is complete
synchronized (alertPromptLock) {
try {
alertPromptLock.wait();
} catch (Exception e) {
}
}
alertPromptInProgress = false;
return alertPromptResult;
}
public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
final EmulationActivity emulationActivity = sEmulationActivity.get();
alertPromptResult = "";
alertPromptButton = 0;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
// Set up the input
alertPromptEditText = new EditText(CitraApplication.getAppContext());
alertPromptEditText.setText(text);
alertPromptEditText.setSingleLine();
alertPromptEditText.setLayoutParams(params);
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(alertPromptEditText);
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
.setTitle(caption)
.setView(container)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
{
alertPromptButton = buttonConfig;
alertPromptResult = alertPromptEditText.getText().toString();
synchronized (alertPromptLock) {
alertPromptLock.notifyAll();
}
})
.setOnDismissListener(dialogInterface ->
{
alertPromptResult = "";
synchronized (alertPromptLock) {
alertPromptLock.notifyAll();
}
});
if (buttonConfig > 0) {
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
{
alertPromptResult = "";
synchronized (alertPromptLock) {
alertPromptLock.notifyAll();
}
});
}
return builder;
}
public static int alertPromptButton() {
return alertPromptButton;
}
public static void exitEmulationActivity(int resultCode) {
final int Success = 0;
final int ErrorNotInitialized = 1;
final int ErrorGetLoader = 2;
final int ErrorSystemMode = 3;
final int ErrorLoader = 4;
final int ErrorLoader_ErrorEncrypted = 5;
final int ErrorLoader_ErrorInvalidFormat = 6;
final int ErrorSystemFiles = 7;
final int ErrorVideoCore = 8;
final int ErrorVideoCore_ErrorGenericDrivers = 9;
final int ErrorVideoCore_ErrorBelowGL33 = 10;
final int ShutdownRequested = 11;
final int ErrorUnknown = 12;
final EmulationActivity emulationActivity = sEmulationActivity.get();
if (emulationActivity == null) {
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
return;
}
int captionId = R.string.loader_error_invalid_format;
if (resultCode == ErrorLoader_ErrorEncrypted) {
captionId = R.string.loader_error_encrypted;
}
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
.setTitle(captionId)
.setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
.setOnDismissListener(dialogInterface -> emulationActivity.finish());
emulationActivity.runOnUiThread(() -> {
AlertDialog alert = builder.create();
alert.show();
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
});
}
public static void setEmulationActivity(EmulationActivity emulationActivity) {
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
sEmulationActivity = new WeakReference<>(emulationActivity);
}
public static void clearEmulationActivity() {
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
sEmulationActivity.clear();
}
private static final Object cameraPermissionLock = new Object();
private static boolean cameraPermissionGranted = false;
public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
public static boolean RequestCameraPermission() {
final EmulationActivity emulationActivity = sEmulationActivity.get();
if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present");
return false;
}
if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
// Permission already granted
return true;
}
emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
// Wait until result is returned
synchronized (cameraPermissionLock) {
try {
cameraPermissionLock.wait();
} catch (InterruptedException ignored) {
}
}
return cameraPermissionGranted;
}
public static void CameraPermissionResult(boolean granted) {
cameraPermissionGranted = granted;
synchronized (cameraPermissionLock) {
cameraPermissionLock.notify();
}
}
private static final Object micPermissionLock = new Object();
private static boolean micPermissionGranted = false;
public static final int REQUEST_CODE_NATIVE_MIC = 900;
public static boolean RequestMicPermission() {
final EmulationActivity emulationActivity = sEmulationActivity.get();
if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present");
return false;
}
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
// Permission already granted
return true;
}
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
// Wait until result is returned
synchronized (micPermissionLock) {
try {
micPermissionLock.wait();
} catch (InterruptedException ignored) {
}
}
return micPermissionGranted;
}
public static void MicPermissionResult(boolean granted) {
micPermissionGranted = granted;
synchronized (micPermissionLock) {
micPermissionLock.notify();
}
}
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
public static native void ReloadCameraDevices();
public static native boolean LoadAmiibo(byte[] bytes);
public static native void RemoveAmiibo();
/**
* Button type for use in onTouchEvent
*/
public static final class ButtonType {
public static final int BUTTON_A = 700;
public static final int BUTTON_B = 701;
public static final int BUTTON_X = 702;
public static final int BUTTON_Y = 703;
public static final int BUTTON_START = 704;
public static final int BUTTON_SELECT = 705;
public static final int BUTTON_HOME = 706;
public static final int BUTTON_ZL = 707;
public static final int BUTTON_ZR = 708;
public static final int DPAD_UP = 709;
public static final int DPAD_DOWN = 710;
public static final int DPAD_LEFT = 711;
public static final int DPAD_RIGHT = 712;
public static final int STICK_LEFT = 713;
public static final int STICK_LEFT_UP = 714;
public static final int STICK_LEFT_DOWN = 715;
public static final int STICK_LEFT_LEFT = 716;
public static final int STICK_LEFT_RIGHT = 717;
public static final int STICK_C = 718;
public static final int STICK_C_UP = 719;
public static final int STICK_C_DOWN = 720;
public static final int STICK_C_LEFT = 771;
public static final int STICK_C_RIGHT = 772;
public static final int TRIGGER_L = 773;
public static final int TRIGGER_R = 774;
public static final int DPAD = 780;
}
/**
* Button states
*/
public static final class ButtonState {
public static final int RELEASED = 0;
public static final int PRESSED = 1;
}
}

View File

@ -0,0 +1,38 @@
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

@ -0,0 +1,715 @@
package org.citra.citra_emu.activities;
import android.app.Activity;
import android.app.PendingIntent;
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.SparseIntArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.FragmentActivity;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.camera.StillImageCameraHelper;
import org.citra.citra_emu.fragments.EmulationFragment;
import org.citra.citra_emu.ui.main.MainActivity;
import org.citra.citra_emu.utils.ControllerMappingHelper;
import org.citra.citra_emu.utils.EmulationMenuSettings;
import org.citra.citra_emu.utils.FileBrowserHelper;
import org.citra.citra_emu.utils.FileUtil;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.util.Collections;
import java.util.List;
import static java.lang.annotation.RetentionPolicy.SOURCE;
public final class EmulationActivity extends AppCompatActivity {
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
public static final int MENU_ACTION_ADJUST_SCALE = 2;
public static final int MENU_ACTION_EXIT = 3;
public static final int MENU_ACTION_SHOW_FPS = 4;
public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
public static final int MENU_ACTION_SWAP_SCREENS = 9;
public static final int MENU_ACTION_RESET_OVERLAY = 10;
public static final int MENU_ACTION_SHOW_OVERLAY = 11;
public static final int MENU_ACTION_OPEN_SETTINGS = 12;
public static final int MENU_ACTION_LOAD_AMIIBO = 13;
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
public static final int REQUEST_SELECT_AMIIBO = 2;
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
static {
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
buttonsActionsMap
.append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
buttonsActionsMap.append(R.id.menu_emulation_show_fps,
EmulationActivity.MENU_ACTION_SHOW_FPS);
buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
buttonsActionsMap.append(R.id.menu_screen_layout_single,
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
EmulationActivity.MENU_ACTION_SWAP_SCREENS);
buttonsActionsMap
.append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
buttonsActionsMap
.append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
buttonsActionsMap
.append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
buttonsActionsMap
.append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
buttonsActionsMap
.append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
}
private View mDecorView;
private EmulationFragment mEmulationFragment;
private SharedPreferences mPreferences;
private ControllerMappingHelper mControllerMappingHelper;
private boolean activityRecreated;
private String mSelectedTitle;
private String mPath;
public static void launch(FragmentActivity activity, String path, String title) {
Intent launcher = new Intent(activity, EmulationActivity.class);
launcher.putExtra(EXTRA_SELECTED_GAME, path);
launcher.putExtra(EXTRA_SELECTED_TITLE, title);
activity.startActivity(launcher);
}
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);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
.setSmallIcon(R.drawable.ic_stat_notification_logo)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.app_notification_running))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setVibrate(null)
.setSound(null)
.setContentIntent(contentIntent);
NotificationManagerCompat.from(this).notify(EMULATION_RUNNING_NOTIFICATION, builder.build());
}
public static void tryDismissRunningNotification(Activity activity) {
NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
}
@Override
protected void onDestroy() {
tryDismissRunningNotification(this);
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
// Get params we were passed
Intent gameToEmulate = getIntent();
mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
activityRecreated = false;
} else {
activityRecreated = true;
restoreState(savedInstanceState);
}
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
mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
.findFragmentById(R.id.frame_emulation_fragment);
if (mEmulationFragment == null) {
mEmulationFragment = EmulationFragment.newInstance(mPath);
getSupportFragmentManager().beginTransaction()
.add(R.id.frame_emulation_fragment, mEmulationFragment)
.commit();
}
setTitle(mSelectedTitle);
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
showRunningNotification();
// Override Citra core INI with the one set by our in game menu
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
getWindowManager().getDefaultDisplay().getRotation());
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
outState.putString(EXTRA_SELECTED_GAME, mPath);
outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
super.onSaveInstanceState(outState);
}
protected void restoreState(Bundle savedInstanceState) {
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
// If an alert prompt was in progress when state was restored, retry displaying it
NativeLibrary.retryDisplayAlertPrompt();
}
@Override
public void onRestart() {
super.onRestart();
NativeLibrary.ReloadCameraDevices();
}
@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) ->
{
}).setOnDismissListener(dialogInterface ->
NativeLibrary.UnPauseEmulation())
.create()
.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
new AlertDialog.Builder(this)
.setTitle(R.string.camera)
.setMessage(R.string.camera_permission_needed)
.setPositiveButton(android.R.string.ok, null)
.show();
}
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
break;
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
new AlertDialog.Builder(this)
.setTitle(R.string.microphone)
.setMessage(R.string.microphone_permission_needed)
.setPositiveButton(android.R.string.ok, null)
.show();
}
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
private void enableFullscreenImmersive() {
// It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
mDecorView.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);
}
@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);
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
case EmulationMenuSettings.LayoutOption_SingleScreen:
layoutOptionMenuItem = R.id.menu_screen_layout_single;
break;
case EmulationMenuSettings.LayoutOption_SideScreen:
layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
break;
case EmulationMenuSettings.LayoutOption_MobilePortrait:
layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
break;
}
menu.findItem(layoutOptionMenuItem).setChecked(true);
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;
}
@SuppressWarnings("WrongConstant")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int action = buttonsActionsMap.get(item.getItemId(), -1);
switch (action) {
// Edit the placement of the controls
case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
editControlsPlacement();
break;
// Enable/Disable specific buttons or the entire input overlay.
case MENU_ACTION_TOGGLE_CONTROLS:
toggleControls();
break;
// Adjust the scale of the overlay controls.
case MENU_ACTION_ADJUST_SCALE:
adjustScale();
break;
// Toggle the visibility of the Performance stats TextView
case MENU_ACTION_SHOW_FPS: {
final boolean isEnabled = !EmulationMenuSettings.getShowFps();
EmulationMenuSettings.setShowFps(isEnabled);
item.setChecked(isEnabled);
mEmulationFragment.updateShowFpsOverlay();
break;
}
// Sets the screen layout to Landscape
case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
break;
// Sets the screen layout to Portrait
case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
break;
// Sets the screen layout to Single
case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
break;
// Sets the screen layout to Side by Side
case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
break;
// Swap the top and bottom screen locations
case MENU_ACTION_SWAP_SCREENS: {
final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
EmulationMenuSettings.setSwapScreens(isEnabled);
item.setChecked(isEnabled);
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
.getRotation());
break;
}
// Reset overlay placement
case MENU_ACTION_RESET_OVERLAY:
resetOverlay();
break;
// Show or hide overlay
case MENU_ACTION_SHOW_OVERLAY: {
final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
EmulationMenuSettings.setShowOverlay(isEnabled);
item.setChecked(isEnabled);
mEmulationFragment.refreshInputOverlay();
break;
}
case MENU_ACTION_EXIT:
mEmulationFragment.stopEmulation();
finish();
break;
case MENU_ACTION_OPEN_SETTINGS:
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
break;
case MENU_ACTION_LOAD_AMIIBO:
FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, R.string.select_amiibo, Collections.singletonList("bin"));
break;
case MENU_ACTION_REMOVE_AMIIBO:
RemoveAmiibo();
break;
}
return true;
}
private void changeScreenOrientation(int layoutOption, MenuItem item) {
item.setChecked(true);
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
.getRotation());
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
}
private void editControlsPlacement() {
if (mEmulationFragment.isConfiguringControls()) {
mEmulationFragment.stopConfiguringControls();
} else {
mEmulationFragment.startConfiguringControls();
}
}
// Gets button presses
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int action;
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
// Handling the case where the back button is pressed.
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
onBackPressed();
return true;
}
// Normal key events.
action = NativeLibrary.ButtonState.PRESSED;
break;
case KeyEvent.ACTION_UP:
action = NativeLibrary.ButtonState.RELEASED;
break;
default:
return false;
}
InputDevice input = event.getDevice();
if (input == null) {
// Controller was disconnected
return false;
}
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
}
@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;
}
}
private void onAmiiboSelected(String selectedFile) {
File file = new File(selectedFile);
boolean success = false;
try {
byte[] bytes = FileUtil.getBytesFromFile(file);
success = NativeLibrary.LoadAmiibo(bytes);
} catch (IOException e) {
e.printStackTrace();
}
if (!success) {
new AlertDialog.Builder(this)
.setTitle(R.string.amiibo_load_error)
.setMessage(R.string.amiibo_load_error_message)
.setPositiveButton(android.R.string.ok, null)
.create()
.show();
}
}
private void RemoveAmiibo() {
NativeLibrary.RemoveAmiibo();
}
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
boolean defaultValue = true;
switch (i) {
case 6: // ZL
case 7: // ZR
case 12: // C-stick
defaultValue = false;
break;
}
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();
}
private void adjustScale() {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.dialog_seekbar, null);
final SeekBar seekbar = view.findViewById(R.id.seekbar);
final TextView value = 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());
}
});
value.setText(String.valueOf(seekbar.getProgress() + 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);
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
private void setControlScale(int scale) {
SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("controlScale", scale);
editor.apply();
mEmulationFragment.refreshInputOverlay();
}
private void resetOverlay() {
new AlertDialog.Builder(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()
.show();
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
return super.dispatchGenericMotionEvent(event);
}
// Don't attempt to do anything if we are disconnecting a device.
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
return true;
}
InputDevice input = event.getDevice();
List<InputDevice.MotionRange> motions = input.getMotionRanges();
float[] axisValuesCirclePad = {0.0f, 0.0f};
float[] axisValuesCStick = {0.0f, 0.0f};
float[] axisValuesDPad = {0.0f, 0.0f};
boolean isTriggerPressedLMapped = false;
boolean isTriggerPressedRMapped = false;
boolean isTriggerPressedZLMapped = false;
boolean isTriggerPressedZRMapped = false;
boolean isTriggerPressedL = false;
boolean isTriggerPressedR = false;
boolean isTriggerPressedZL = false;
boolean isTriggerPressedZR = false;
for (InputDevice.MotionRange range : motions) {
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
if (nextMapping == -1 || guestOrientation == -1) {
// Axis is unmapped
continue;
}
if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
// Skip joystick wobble
value = 0.f;
}
if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
axisValuesCirclePad[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
axisValuesCStick[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
axisValuesDPad[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
isTriggerPressedLMapped = true;
isTriggerPressedL = value != 0.f;
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
isTriggerPressedRMapped = true;
isTriggerPressedR = value != 0.f;
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
isTriggerPressedZLMapped = true;
isTriggerPressedZL = value != 0.f;
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
isTriggerPressedZRMapped = true;
isTriggerPressedZR = value != 0.f;
}
}
// Circle-Pad and C-Stick status
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
// Triggers L/R and ZL/ZR
if (isTriggerPressedLMapped) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
}
if (isTriggerPressedRMapped) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
}
if (isTriggerPressedZLMapped) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
}
if (isTriggerPressedZRMapped) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
}
// Work-around to allow D-pad axis to be bound to emulated buttons
if (axisValuesDPad[0] == 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
}
if (axisValuesDPad[0] < 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
}
if (axisValuesDPad[0] > 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
}
if (axisValuesDPad[1] == 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
}
if (axisValuesDPad[1] < 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
}
if (axisValuesDPad[1] > 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
}
return true;
}
public boolean isActivityRecreated() {
return activityRecreated;
}
@Retention(SOURCE)
@IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
public @interface MenuAction {
}
}

View File

@ -0,0 +1,247 @@
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;
import android.view.View;
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 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.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;
/**
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
* large dataset.
*/
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
View.OnClickListener {
private Cursor mCursor;
private GameDataSetObserver mObserver;
private boolean mDatasetValid;
private long mLastClickTime = 0;
/**
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
* display no data until a Cursor is supplied by a CursorLoader.
*/
public GameAdapter() {
mDatasetValid = false;
mObserver = new GameDataSetObserver();
}
/**
* Called by the LayoutManager when it is necessary to create a new view.
*
* @param parent The RecyclerView (I think?) the created view will be thrown into.
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
* @return The created ViewHolder with references to all the child view's members.
*/
@Override
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Create a new view.
View gameCard = LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_game, parent, false);
gameCard.setOnClickListener(this);
// Use that view to create a ViewHolder.
return new GameViewHolder(gameCard);
}
/**
* Called by the LayoutManager when a new view is not necessary because we can recycle
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
* can use the view that just scrolled off the top instead of inflating a new one.)
*
* @param holder A ViewHolder representing the view we're recycling.
* @param position The position of the 'new' view in the dataset.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
if (mDatasetValid) {
if (mCursor.moveToPosition(position)) {
PicassoUtils.loadGameIcon(holder.imageIcon,
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textFileName.setText(gamePath.getFileName().toString());
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
final int backgroundColorId = isValidGame(holder.path) ? R.color.card_view_background : R.color.card_view_disabled;
View itemView = holder.getItemView();
itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
} else {
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
}
} else {
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
}
}
/**
* Called by the LayoutManager to find out how much data we have.
*
* @return Size of the dataset.
*/
@Override
public int getItemCount() {
if (mDatasetValid && mCursor != null) {
return mCursor.getCount();
}
Log.error("[GameAdapter] Dataset is not valid.");
return 0;
}
/**
* Return the contents of the _id column for a given row.
*
* @param position The row for which Android wants an ID.
* @return A valid ID from the database, or 0 if not available.
*/
@Override
public long getItemId(int position) {
if (mDatasetValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
}
}
Log.error("[GameAdapter] Dataset is not valid.");
return 0;
}
/**
* Tell Android whether or not each item in the dataset has a stable identifier.
* Which it does, because it's a database, so always tell Android 'true'.
*
* @param hasStableIds ignored.
*/
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
/**
* When a load is finished, call this to replace the existing data with the newly-loaded
* data.
*
* @param cursor The newly-loaded Cursor.
*/
public void swapCursor(Cursor cursor) {
// Sanity check.
if (cursor == mCursor) {
return;
}
// Before getting rid of the old cursor, disassociate it from the Observer.
final Cursor oldCursor = mCursor;
if (oldCursor != null && mObserver != null) {
oldCursor.unregisterDataSetObserver(mObserver);
}
mCursor = cursor;
if (mCursor != null) {
// Attempt to associate the new Cursor with the Observer.
if (mObserver != null) {
mCursor.registerDataSetObserver(mObserver);
}
mDatasetValid = true;
} else {
mDatasetValid = false;
}
notifyDataSetChanged();
}
/**
* Launches the game that was clicked on.
*
* @param view The card representing the game the user wants to play.
*/
@Override
public void onClick(View view) {
// Double-click prevention, using threshold of 1000 ms
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
return;
}
mLastClickTime = SystemClock.elapsedRealtime();
GameViewHolder holder = (GameViewHolder) view.getTag();
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
}
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));
}
private final class GameDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
mDatasetValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDatasetValid = false;
notifyDataSetChanged();
}
}
}

View File

@ -0,0 +1,124 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
public final class MiiSelector {
public static class MiiSelectorConfig implements java.io.Serializable {
public boolean enable_cancel_button;
public String title;
public long initially_selected_mii_index;
// List of Miis to display
public String[] mii_names;
}
public static class MiiSelectorData {
public long return_code;
public int index;
private MiiSelectorData(long return_code, int index) {
this.return_code = return_code;
this.index = index;
}
}
public static class MiiSelectorDialogFragment extends DialogFragment {
static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
Bundle args = new Bundle();
args.putSerializable("config", config);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = Objects.requireNonNull(getActivity());
MiiSelectorConfig config =
Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
.getSerializable("config"));
// Note: we intentionally leave out the Standard Mii in the native code so that
// the string can get translated
ArrayList<String> list = new ArrayList<>();
list.add(emulationActivity.getString(R.string.standard_mii));
list.addAll(Arrays.asList(config.mii_names));
final int initialIndex = config.initially_selected_mii_index < list.size()
? (int) config.initially_selected_mii_index
: 0;
data.index = initialIndex;
AlertDialog.Builder builder =
new AlertDialog.Builder(emulationActivity)
.setTitle(config.title.isEmpty()
? emulationActivity.getString(R.string.mii_selector)
: config.title)
.setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
(dialog, which) -> {
data.index = which;
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
data.return_code = 0;
synchronized (finishLock) {
finishLock.notifyAll();
}
});
if (config.enable_cancel_button) {
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
data.return_code = 1;
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
setCancelable(false);
return builder.create();
}
}
private static MiiSelectorData data;
private static final Object finishLock = new Object();
private static void ExecuteImpl(MiiSelectorConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new MiiSelectorData(0, 0);
MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
}
public static MiiSelectorData Execute(MiiSelectorConfig config) {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
synchronized (finishLock) {
try {
finishLock.wait();
} catch (Exception ignored) {
}
}
return data;
}
}

View File

@ -0,0 +1,264 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.Spanned;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.utils.Log;
import java.util.Objects;
public final class SoftwareKeyboard {
/// Corresponds to Frontend::ButtonConfig
private interface ButtonConfig {
int Single = 0; /// Ok button
int Dual = 1; /// Cancel | Ok buttons
int Triple = 2; /// Cancel | I Forgot | Ok buttons
int None = 3; /// No button (returned by swkbdInputText in special cases)
}
/// Corresponds to Frontend::ValidationError
public enum ValidationError {
None,
// Button Selection
ButtonOutOfRange,
// Configured Filters
MaxDigitsExceeded,
AtSignNotAllowed,
PercentNotAllowed,
BackslashNotAllowed,
ProfanityNotAllowed,
CallbackFailed,
// Allowed Input Type
FixedLengthRequired,
MaxLengthExceeded,
BlankInputNotAllowed,
EmptyInputNotAllowed,
}
public static class KeyboardConfig implements java.io.Serializable {
public int button_config;
public int max_text_length;
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
public String hint_text; /// Displayed in the field as a hint before
@Nullable
public String[] button_text; /// Contains the button text that the caller provides
}
/// Corresponds to Frontend::KeyboardData
public static class KeyboardData {
public int button;
public String text;
private KeyboardData(int button, String text) {
this.button = button;
this.text = text;
}
}
private static class Filter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) {
String text = new StringBuilder(dest)
.replace(dstart, dend, source.subSequence(start, end).toString())
.toString();
if (ValidateFilters(text) == ValidationError.None) {
return null; // Accept replacement
}
return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
}
}
public static class KeyboardDialogFragment extends DialogFragment {
static KeyboardDialogFragment newInstance(KeyboardConfig config) {
KeyboardDialogFragment frag = new KeyboardDialogFragment();
Bundle args = new Bundle();
args.putSerializable("config", config);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = getActivity();
assert emulationActivity != null;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin =
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
R.dimen.dialog_margin);
KeyboardConfig config = Objects.requireNonNull(
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
// Set up the input
EditText editText = new EditText(CitraApplication.getAppContext());
editText.setHint(config.hint_text);
editText.setSingleLine(!config.multiline_mode);
editText.setLayoutParams(params);
editText.setFilters(new InputFilter[]{
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText);
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setView(container);
setCancelable(false);
switch (config.button_config) {
case ButtonConfig.Triple: {
final String text = config.button_text == null
? emulationActivity.getString(R.string.i_forgot)
: config.button_text[1];
builder.setNeutralButton(text, null);
}
// fallthrough
case ButtonConfig.Dual: {
final String text = config.button_text == null
? emulationActivity.getString(android.R.string.cancel)
: config.button_text[0];
builder.setNegativeButton(text, null);
}
// fallthrough
case ButtonConfig.Single: {
final String text = config.button_text == null
? emulationActivity.getString(android.R.string.ok)
: config.button_text[config.button_config];
builder.setPositiveButton(text, null);
break;
}
}
final AlertDialog dialog = builder.create();
dialog.create();
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
data.button = config.button_config;
data.text = editText.getText().toString();
final ValidationError error = ValidateInput(data.text);
if (error != ValidationError.None) {
HandleValidationError(config, error);
return;
}
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
data.button = 1;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
data.button = 0;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
return dialog;
}
}
private static KeyboardData data;
private static final Object finishLock = new Object();
private static void ExecuteImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new KeyboardData(0, "");
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
}
private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
String message = "";
switch (error) {
case FixedLengthRequired:
message =
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
break;
case MaxLengthExceeded:
message =
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
break;
case BlankInputNotAllowed:
message = emulationActivity.getString(R.string.blank_input_not_allowed);
break;
case EmptyInputNotAllowed:
message = emulationActivity.getString(R.string.empty_input_not_allowed);
break;
}
new AlertDialog.Builder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();
}
public static KeyboardData Execute(KeyboardConfig config) {
if (config.button_config == ButtonConfig.None) {
Log.error("Unexpected button config None");
return new KeyboardData(0, "");
}
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
synchronized (finishLock) {
try {
finishLock.wait();
} catch (Exception ignored) {
}
}
return data;
}
public static void ShowError(String error) {
NativeLibrary.displayAlertMsg(
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
error, false);
}
private static native ValidationError ValidateFilters(String text);
private static native ValidationError ValidateInput(String text);
}

View File

@ -0,0 +1,65 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.camera;
import android.content.Intent;
import android.graphics.Bitmap;
import android.provider.MediaStore;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.utils.PicassoUtils;
import androidx.annotation.Nullable;
// Used in native code.
public final class StillImageCameraHelper {
public static final int REQUEST_CAMERA_FILE_PICKER = 1;
private static final Object filePickerLock = new Object();
private static @Nullable
String filePickerPath;
// Opens file picker for camera.
public static @Nullable
String OpenFilePicker() {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
// At this point, we are assuming that we already have permissions as they are
// needed to launch a game
emulationActivity.runOnUiThread(() -> {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
emulationActivity.startActivityForResult(
Intent.createChooser(intent,
emulationActivity.getString(R.string.camera_select_image)),
REQUEST_CAMERA_FILE_PICKER);
});
synchronized (filePickerLock) {
try {
filePickerLock.wait();
} catch (InterruptedException ignored) {
}
}
return filePickerPath;
}
// Called from EmulationActivity.
public static void OnFilePickerResult(Intent result) {
filePickerPath = result == null ? null : result.getDataString();
synchronized (filePickerLock) {
filePickerLock.notifyAll();
}
}
// Blocking call. Load image from file and crop/resize it to fit in width x height.
@Nullable
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
}
}

View File

@ -0,0 +1,140 @@
package org.citra.citra_emu.dialogs;
import android.content.Context;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
import java.util.List;
/**
* {@link AlertDialog} derivative that listens for
* motion events from controllers and joysticks.
*/
public final class MotionAlertDialog extends AlertDialog {
// The selected input preference
private final InputBindingSetting setting;
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
private int mPrevDeviceId = 0;
private boolean mWaitingForEvent = true;
/**
* Constructor
*
* @param context The current {@link Context}.
* @param setting The Preference to show this dialog for.
*/
public MotionAlertDialog(Context context, InputBindingSetting setting) {
super(context);
this.setting = setting;
}
public boolean onKeyEvent(int keyCode, KeyEvent event) {
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
switch (event.getAction()) {
case KeyEvent.ACTION_UP:
setting.onKeyInput(event);
dismiss();
// Even if we ignore the key, we still consume it. Thus return true regardless.
return true;
default:
return false;
}
}
@Override
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
return super.onKeyLongPress(keyCode, event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Handle this key if we care about it, otherwise pass it down the framework
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
}
@Override
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
// Handle this event if we care about it, otherwise pass it down the framework
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
}
private boolean onMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
return false;
if (event.getAction() != MotionEvent.ACTION_MOVE)
return false;
InputDevice input = event.getDevice();
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
if (input.getId() != mPrevDeviceId) {
mPreviousValues.clear();
}
mPrevDeviceId = input.getId();
boolean firstEvent = mPreviousValues.isEmpty();
int numMovedAxis = 0;
float axisMoveValue = 0.0f;
InputDevice.MotionRange lastMovedRange = null;
char lastMovedDir = '?';
if (mWaitingForEvent) {
for (int i = 0; i < motionRanges.size(); i++) {
InputDevice.MotionRange range = motionRanges.get(i);
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
if (firstEvent) {
mPreviousValues.add(value);
} else {
float previousValue = mPreviousValues.get(i);
// Only handle the axes that are not neutral (more than 0.5)
// but ignore any axis that has a constant value (e.g. always 1)
if (Math.abs(value) > 0.5f && value != previousValue) {
// It is common to have multiple axes with the same physical input. For example,
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
// To handle this, we ignore an axis motion that's the exact same as a motion
// we already saw. This way, we ignore axes with two names, but catch the case
// where a joystick is moved in two directions.
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
if (value != axisMoveValue) {
axisMoveValue = value;
numMovedAxis++;
lastMovedRange = range;
lastMovedDir = value < 0.0f ? '-' : '+';
}
}
// Special case for d-pads (axis value jumps between 0 and 1 without any values
// in between). Without this, the user would need to press the d-pad twice
// due to the first press being caught by the "if (firstEvent)" case further up.
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
numMovedAxis++;
lastMovedRange = range;
lastMovedDir = previousValue < 0.0f ? '-' : '+';
}
}
mPreviousValues.set(i, value);
}
// If only one axis moved, that's the winner.
if (numMovedAxis == 1) {
mWaitingForEvent = false;
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
dismiss();
}
}
return true;
}
}

View File

@ -0,0 +1,23 @@
package org.citra.citra_emu.features.settings.model;
public final class BooleanSetting extends Setting {
private boolean mValue;
public BooleanSetting(String key, String section, boolean value) {
super(key, section);
mValue = value;
}
public boolean getValue() {
return mValue;
}
public void setValue(boolean value) {
mValue = value;
}
@Override
public String getValueAsString() {
return mValue ? "True" : "False";
}
}

View File

@ -0,0 +1,23 @@
package org.citra.citra_emu.features.settings.model;
public final class FloatSetting extends Setting {
private float mValue;
public FloatSetting(String key, String section, float value) {
super(key, section);
mValue = value;
}
public float getValue() {
return mValue;
}
public void setValue(float value) {
mValue = value;
}
@Override
public String getValueAsString() {
return Float.toString(mValue);
}
}

View File

@ -0,0 +1,23 @@
package org.citra.citra_emu.features.settings.model;
public final class IntSetting extends Setting {
private int mValue;
public IntSetting(String key, String section, int value) {
super(key, section);
mValue = value;
}
public int getValue() {
return mValue;
}
public void setValue(int value) {
mValue = value;
}
@Override
public String getValueAsString() {
return Integer.toString(mValue);
}
}

View File

@ -0,0 +1,42 @@
package org.citra.citra_emu.features.settings.model;
/**
* Abstraction for a setting item as read from / written to Citra's configuration ini files.
* These files generally consist of a key/value pair, though the type of value is ambiguous and
* must be inferred at read-time. The type of value determines which child of this class is used
* to represent the Setting.
*/
public abstract class Setting {
private String mKey;
private String mSection;
/**
* Base constructor.
*
* @param key Everything to the left of the = in a line from the ini file.
* @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets.
*/
public Setting(String key, String section) {
mKey = key;
mSection = section;
}
/**
* @return The identifier used to write this setting to the ini file.
*/
public String getKey() {
return mKey;
}
/**
* @return The name of the header under which this Setting should be written in the ini file.
*/
public String getSection() {
return mSection;
}
/**
* @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
*/
public abstract String getValueAsString();
}

View File

@ -0,0 +1,55 @@
package org.citra.citra_emu.features.settings.model;
import java.util.HashMap;
/**
* A semantically-related group of Settings objects. These Settings are
* internally stored as a HashMap.
*/
public final class SettingSection {
private String mName;
private HashMap<String, Setting> mSettings = new HashMap<>();
/**
* Create a new SettingSection with no Settings in it.
*
* @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
*/
public SettingSection(String name) {
mName = name;
}
public String getName() {
return mName;
}
/**
* Convenience method; inserts a value directly into the backing HashMap.
*
* @param setting The Setting to be inserted.
*/
public void putSetting(Setting setting) {
mSettings.put(setting.getKey(), setting);
}
/**
* Convenience method; gets a value directly from the backing HashMap.
*
* @param key Used to retrieve the Setting.
* @return A Setting object (you should probably cast this before using)
*/
public Setting getSetting(String key) {
return mSettings.get(key);
}
public HashMap<String, Setting> getSettings() {
return mSettings;
}
public void mergeSection(SettingSection settingSection) {
for (Setting setting : settingSection.mSettings.values()) {
putSetting(setting);
}
}
}

View File

@ -0,0 +1,130 @@
package org.citra.citra_emu.features.settings.model;
import android.text.TextUtils;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Settings {
public static final String SECTION_PREMIUM = "Premium";
public static final String SECTION_CORE = "Core";
public static final String SECTION_SYSTEM = "System";
public static final String SECTION_CAMERA = "Camera";
public static final String SECTION_CONTROLS = "Controls";
public static final String SECTION_RENDERER = "Renderer";
public static final String SECTION_AUDIO = "Audio";
public static final String SECTION_DEBUG = "Debug";
private String gameId;
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
static {
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_AUDIO, SECTION_DEBUG));
}
/**
* A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
@Override
public SettingSection get(Object key) {
if (!(key instanceof String)) {
return null;
}
String stringKey = (String) key;
if (!super.containsKey(stringKey)) {
SettingSection section = new SettingSection(stringKey);
super.put(stringKey, section);
return section;
}
return super.get(key);
}
}
private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
public SettingSection getSection(String sectionName) {
return sections.get(sectionName);
}
public boolean isEmpty() {
return sections.isEmpty();
}
public HashMap<String, SettingSection> getSections() {
return sections;
}
public void loadSettings(SettingsActivityView view) {
sections = new Settings.SettingsSectionMap();
loadCitraSettings(view);
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId, view);
}
}
private void loadCitraSettings(SettingsActivityView view) {
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
String fileName = entry.getKey();
sections.putAll(SettingsFile.readFile(fileName, view));
}
}
private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
// custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
}
private void mergeSections(HashMap<String, SettingSection> updatedSections) {
for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
if (sections.containsKey(entry.getKey())) {
SettingSection originalSection = sections.get(entry.getKey());
SettingSection updatedSection = entry.getValue();
originalSection.mergeSection(updatedSection);
} else {
sections.put(entry.getKey(), entry.getValue());
}
}
}
public void loadSettings(String gameId, SettingsActivityView view) {
this.gameId = gameId;
loadSettings(view);
}
public void saveSettings(SettingsActivityView view) {
if (TextUtils.isEmpty(gameId)) {
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false);
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
String fileName = entry.getKey();
List<String> sectionNames = entry.getValue();
TreeMap<String, SettingSection> iniSections = new TreeMap<>();
for (String section : sectionNames) {
iniSections.put(section, sections.get(section));
}
SettingsFile.saveFile(fileName, iniSections, view);
}
} else {
// custom game settings
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false);
SettingsFile.saveCustomGameSettings(gameId, sections);
}
}
}

View File

@ -0,0 +1,23 @@
package org.citra.citra_emu.features.settings.model;
public final class StringSetting extends Setting {
private String mValue;
public StringSetting(String key, String section, String value) {
super(key, section);
mValue = value;
}
public String getValue() {
return mValue;
}
public void setValue(String value) {
mValue = value;
}
@Override
public String getValueAsString() {
return mValue;
}
}

View File

@ -0,0 +1,66 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
public final class CheckBoxSetting extends SettingsItem {
private boolean mDefaultValue;
private boolean mShowPerformanceWarning;
private SettingsFragmentView mView;
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
boolean defaultValue, Setting setting) {
super(key, section, setting, titleId, descriptionId);
mDefaultValue = defaultValue;
mShowPerformanceWarning = false;
}
public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) {
super(key, section, setting, titleId, descriptionId);
mDefaultValue = defaultValue;
mView = view;
mShowPerformanceWarning = show_performance_warning;
}
public boolean isChecked() {
if (getSetting() == null) {
return mDefaultValue;
}
IntSetting setting = (IntSetting) getSetting();
return setting.getValue() == 1;
}
/**
* Write a value to the backing boolean. If that boolean was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param checked Pretty self explanatory.
* @return null if overwritten successfully; otherwise, a newly created BooleanSetting.
*/
public IntSetting setChecked(boolean checked) {
// Show a performance warning if the setting has been disabled
if (mShowPerformanceWarning && !checked) {
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true);
}
if (getSetting() == null) {
IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0);
setSetting(setting);
return setting;
} else {
IntSetting setting = (IntSetting) getSetting();
setting.setValue(checked ? 1 : 0);
return null;
}
}
@Override
public int getType() {
return TYPE_CHECKBOX;
}
}

View File

@ -0,0 +1,40 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.StringSetting;
public final class DateTimeSetting extends SettingsItem {
private String mDefaultValue;
public DateTimeSetting(String key, String section, int titleId, int descriptionId,
String defaultValue, Setting setting) {
super(key, section, setting, titleId, descriptionId);
mDefaultValue = defaultValue;
}
public String getValue() {
if (getSetting() != null) {
StringSetting setting = (StringSetting) getSetting();
return setting.getValue();
} else {
return mDefaultValue;
}
}
public StringSetting setSelectedValue(String datetime) {
if (getSetting() == null) {
StringSetting setting = new StringSetting(getKey(), getSection(), datetime);
setSetting(setting);
return setting;
} else {
StringSetting setting = (StringSetting) getSetting();
setting.setValue(datetime);
return null;
}
}
@Override
public int getType() {
return TYPE_DATETIME_SETTING;
}
}

View File

@ -0,0 +1,14 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
public final class HeaderSetting extends SettingsItem {
public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) {
super(key, null, setting, titleId, descriptionId);
}
@Override
public int getType() {
return SettingsItem.TYPE_HEADER;
}
}

View File

@ -0,0 +1,382 @@
package org.citra.citra_emu.features.settings.model.view;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.widget.Toast;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
public final class InputBindingSetting extends SettingsItem {
private static final String INPUT_MAPPING_PREFIX = "InputMapping";
public InputBindingSetting(String key, String section, int titleId, Setting setting) {
super(key, section, setting, titleId, 0);
}
public String getValue() {
if (getSetting() == null) {
return "";
}
StringSetting setting = (StringSetting) getSetting();
return setting.getValue();
}
/**
* Returns true if this key is for the 3DS Circle Pad
*/
private boolean IsCirclePad() {
switch (getKey()) {
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
*/
public boolean IsHorizontalOrientation() {
switch (getKey()) {
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS C-Stick
*/
private boolean IsCStick() {
switch (getKey()) {
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS D-Pad
*/
private boolean IsDPad() {
switch (getKey()) {
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
* triggers on the 3DS, but we support them as such on a physical gamepad.
*/
public boolean IsTrigger() {
switch (getKey()) {
case SettingsFile.KEY_BUTTON_L:
case SettingsFile.KEY_BUTTON_R:
case SettingsFile.KEY_BUTTON_ZL:
case SettingsFile.KEY_BUTTON_ZR:
return true;
}
return false;
}
/**
* Returns true if a gamepad axis can be used to map this key.
*/
public boolean IsAxisMappingSupported() {
return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
}
/**
* Returns true if a gamepad button can be used to map this key.
*/
private boolean IsButtonMappingSupported() {
return !IsAxisMappingSupported() || IsTrigger();
}
/**
* Returns the Citra button code for the settings key.
*/
private int getButtonCode() {
switch (getKey()) {
case SettingsFile.KEY_BUTTON_A:
return NativeLibrary.ButtonType.BUTTON_A;
case SettingsFile.KEY_BUTTON_B:
return NativeLibrary.ButtonType.BUTTON_B;
case SettingsFile.KEY_BUTTON_X:
return NativeLibrary.ButtonType.BUTTON_X;
case SettingsFile.KEY_BUTTON_Y:
return NativeLibrary.ButtonType.BUTTON_Y;
case SettingsFile.KEY_BUTTON_L:
return NativeLibrary.ButtonType.TRIGGER_L;
case SettingsFile.KEY_BUTTON_R:
return NativeLibrary.ButtonType.TRIGGER_R;
case SettingsFile.KEY_BUTTON_ZL:
return NativeLibrary.ButtonType.BUTTON_ZL;
case SettingsFile.KEY_BUTTON_ZR:
return NativeLibrary.ButtonType.BUTTON_ZR;
case SettingsFile.KEY_BUTTON_SELECT:
return NativeLibrary.ButtonType.BUTTON_SELECT;
case SettingsFile.KEY_BUTTON_START:
return NativeLibrary.ButtonType.BUTTON_START;
case SettingsFile.KEY_BUTTON_UP:
return NativeLibrary.ButtonType.DPAD_UP;
case SettingsFile.KEY_BUTTON_DOWN:
return NativeLibrary.ButtonType.DPAD_DOWN;
case SettingsFile.KEY_BUTTON_LEFT:
return NativeLibrary.ButtonType.DPAD_LEFT;
case SettingsFile.KEY_BUTTON_RIGHT:
return NativeLibrary.ButtonType.DPAD_RIGHT;
}
return -1;
}
/**
* Returns the settings key for the specified Citra button code.
*/
private static String getButtonKey(int buttonCode) {
switch (buttonCode) {
case NativeLibrary.ButtonType.BUTTON_A:
return SettingsFile.KEY_BUTTON_A;
case NativeLibrary.ButtonType.BUTTON_B:
return SettingsFile.KEY_BUTTON_B;
case NativeLibrary.ButtonType.BUTTON_X:
return SettingsFile.KEY_BUTTON_X;
case NativeLibrary.ButtonType.BUTTON_Y:
return SettingsFile.KEY_BUTTON_Y;
case NativeLibrary.ButtonType.TRIGGER_L:
return SettingsFile.KEY_BUTTON_L;
case NativeLibrary.ButtonType.TRIGGER_R:
return SettingsFile.KEY_BUTTON_R;
case NativeLibrary.ButtonType.BUTTON_ZL:
return SettingsFile.KEY_BUTTON_ZL;
case NativeLibrary.ButtonType.BUTTON_ZR:
return SettingsFile.KEY_BUTTON_ZR;
case NativeLibrary.ButtonType.BUTTON_SELECT:
return SettingsFile.KEY_BUTTON_SELECT;
case NativeLibrary.ButtonType.BUTTON_START:
return SettingsFile.KEY_BUTTON_START;
case NativeLibrary.ButtonType.DPAD_UP:
return SettingsFile.KEY_BUTTON_UP;
case NativeLibrary.ButtonType.DPAD_DOWN:
return SettingsFile.KEY_BUTTON_DOWN;
case NativeLibrary.ButtonType.DPAD_LEFT:
return SettingsFile.KEY_BUTTON_LEFT;
case NativeLibrary.ButtonType.DPAD_RIGHT:
return SettingsFile.KEY_BUTTON_RIGHT;
}
return "";
}
/**
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
* settings on re-mapping or clearing of a setting.
*/
private String getReverseKey() {
String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
if (IsAxisMappingSupported() && !IsTrigger()) {
// Triggers are the only axis-supported mappings without orientation
reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
}
return reverseKey;
}
/**
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
*/
public void removeOldMapping() {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Try remove all possible keys we wrote for this setting
String oldKey = preferences.getString(getReverseKey(), "");
if (!oldKey.equals("")) {
editor.remove(getKey()); // Used for ui text
editor.remove(oldKey); // Used for button mapping
editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
editor.remove(oldKey + "_GuestButton"); // Used for axis button
}
// Apply changes
editor.apply();
}
/**
* Helper function to get the settings key for an gamepad button.
*/
public static String getInputButtonKey(int keyCode) {
return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
}
/**
* Helper function to get the settings key for an gamepad axis.
*/
public static String getInputAxisKey(int axis) {
return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
}
/**
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
*/
public static String getInputAxisButtonKey(int axis) {
return getInputAxisKey(axis) + "_GuestButton";
}
/**
* Helper function to get the settings key for an gamepad axis orientation.
*/
public static String getInputAxisOrientationKey(int axis) {
return getInputAxisKey(axis) + "_GuestOrientation";
}
/**
* Helper function to write a gamepad button mapping for the setting.
*/
private void WriteButtonMapping(String key) {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Remove mapping for another setting using this input
int oldButtonCode = preferences.getInt(key, -1);
if (oldButtonCode != -1) {
String oldKey = getButtonKey(oldButtonCode);
editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
}
// Cleanup old mapping for this setting
removeOldMapping();
// Write new mapping
editor.putInt(key, getButtonCode());
// Write next reverse mapping for future cleanup
editor.putString(getReverseKey(), key);
// Apply changes
editor.apply();
}
/**
* Helper function to write a gamepad axis mapping for the setting.
*/
private void WriteAxisMapping(int axis, int value) {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Cleanup old mapping
removeOldMapping();
// Write new mapping
editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
editor.putInt(getInputAxisButtonKey(axis), value);
// Write next reverse mapping for future cleanup
editor.putString(getReverseKey(), getInputAxisKey(axis));
// Apply changes
editor.apply();
}
/**
* Saves the provided key input setting as an Android preference.
*
* @param keyEvent KeyEvent of this key press.
*/
public void onKeyInput(KeyEvent keyEvent) {
if (!IsButtonMappingSupported()) {
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
return;
}
InputDevice device = keyEvent.getDevice();
WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
setUiString(uiString);
}
/**
* Saves the provided motion input setting as an Android preference.
*
* @param device InputDevice from which the input event originated.
* @param motionRange MotionRange of the movement
* @param axisDir Either '-' or '+' (currently unused)
*/
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
char axisDir) {
if (!IsAxisMappingSupported()) {
Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
return;
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
int button;
if (IsCirclePad()) {
button = NativeLibrary.ButtonType.STICK_LEFT;
} else if (IsCStick()) {
button = NativeLibrary.ButtonType.STICK_C;
} else if (IsDPad()) {
button = NativeLibrary.ButtonType.DPAD;
} else {
button = getButtonCode();
}
WriteAxisMapping(motionRange.getAxis(), button);
String uiString = device.getName() + ": Axis " + motionRange.getAxis();
setUiString(uiString);
editor.apply();
}
/**
* Sets the string to use in the configuration UI for the gamepad input.
*/
private StringSetting setUiString(String ui) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
if (getSetting() == null) {
StringSetting setting = new StringSetting(getKey(), getSection(), "");
setSetting(setting);
editor.putString(setting.getKey(), ui);
editor.apply();
return setting;
} else {
StringSetting setting = (StringSetting) getSetting();
editor.putString(setting.getKey(), ui);
editor.apply();
return null;
}
}
@Override
public int getType() {
return TYPE_INPUT_BINDING;
}
}

View File

@ -0,0 +1,14 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
public final class PremiumHeader extends SettingsItem {
public PremiumHeader() {
super(null, null, null, 0, 0);
}
@Override
public int getType() {
return SettingsItem.TYPE_PREMIUM;
}
}

View File

@ -0,0 +1,59 @@
package org.citra.citra_emu.features.settings.model.view;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
public final class PremiumSingleChoiceSetting extends SettingsItem {
private int mDefaultValue;
private int mChoicesId;
private int mValuesId;
private SettingsFragmentView mView;
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) {
super(key, section, setting, titleId, descriptionId);
mValuesId = valuesId;
mChoicesId = choicesId;
mDefaultValue = defaultValue;
mView = view;
}
public int getChoicesId() {
return mChoicesId;
}
public int getValuesId() {
return mValuesId;
}
public int getSelectedValue() {
return mPreferences.getInt(getKey(), mDefaultValue);
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return null if overwritten successfully otherwise; a newly created IntSetting.
*/
public void setSelectedValue(int selection) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt(getKey(), selection);
editor.apply();
mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false);
}
@Override
public int getType() {
return TYPE_SINGLE_CHOICE;
}
}

View File

@ -0,0 +1,107 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
/**
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
* Each one corresponds to a {@link Setting} object, so this class's subclasses
* should vaguely correspond to those subclasses. There are a few with multiple analogues
* and a few with none (Headers, for example, do not correspond to anything in the ini
* file.)
*/
public abstract class SettingsItem {
public static final int TYPE_HEADER = 0;
public static final int TYPE_CHECKBOX = 1;
public static final int TYPE_SINGLE_CHOICE = 2;
public static final int TYPE_SLIDER = 3;
public static final int TYPE_SUBMENU = 4;
public static final int TYPE_INPUT_BINDING = 5;
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
public static final int TYPE_DATETIME_SETTING = 7;
public static final int TYPE_PREMIUM = 8;
private String mKey;
private String mSection;
private Setting mSetting;
private int mNameId;
private int mDescriptionId;
private boolean mIsPremium;
/**
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
* is null; in which case, one can be constructed and saved using the key / section.
*
* @param key Identifier for the Setting represented by this Item.
* @param section Section to which the Setting belongs.
* @param setting A possibly-null backing Setting, to be modified on UI events.
* @param nameId Resource ID for a text string to be displayed as this setting's name.
* @param descriptionId Resource ID for a text string to be displayed as this setting's description.
*/
public SettingsItem(String key, String section, Setting setting, int nameId,
int descriptionId) {
mKey = key;
mSection = section;
mSetting = setting;
mNameId = nameId;
mDescriptionId = descriptionId;
mIsPremium = (section == Settings.SECTION_PREMIUM);
}
/**
* @return The identifier for the backing Setting.
*/
public String getKey() {
return mKey;
}
/**
* @return The header under which the backing Setting belongs.
*/
public String getSection() {
return mSection;
}
/**
* @return The backing Setting, possibly null.
*/
public Setting getSetting() {
return mSetting;
}
/**
* Replace the backing setting with a new one. Generally used in cases where
* the backing setting is null.
*
* @param setting A non-null Setting.
*/
public void setSetting(Setting setting) {
mSetting = setting;
}
/**
* @return A resource ID for a text string representing this Setting's name.
*/
public int getNameId() {
return mNameId;
}
public int getDescriptionId() {
return mDescriptionId;
}
public boolean isPremium() {
return mIsPremium;
}
/**
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
* method to determine which type of ViewHolder should be created.
*
* @return An integer (ideally, one of the constants defined in this file)
*/
public abstract int getType();
}

View File

@ -0,0 +1,60 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.Setting;
public final class SingleChoiceSetting extends SettingsItem {
private int mDefaultValue;
private int mChoicesId;
private int mValuesId;
public SingleChoiceSetting(String key, String section, int titleId, int descriptionId,
int choicesId, int valuesId, int defaultValue, Setting setting) {
super(key, section, setting, titleId, descriptionId);
mValuesId = valuesId;
mChoicesId = choicesId;
mDefaultValue = defaultValue;
}
public int getChoicesId() {
return mChoicesId;
}
public int getValuesId() {
return mValuesId;
}
public int getSelectedValue() {
if (getSetting() != null) {
IntSetting setting = (IntSetting) getSetting();
return setting.getValue();
} else {
return mDefaultValue;
}
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return null if overwritten successfully otherwise; a newly created IntSetting.
*/
public IntSetting setSelectedValue(int selection) {
if (getSetting() == null) {
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
setSetting(setting);
return setting;
} else {
IntSetting setting = (IntSetting) getSetting();
setting.setValue(selection);
return null;
}
}
@Override
public int getType() {
return TYPE_SINGLE_CHOICE;
}
}

View File

@ -0,0 +1,101 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.FloatSetting;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.utils.Log;
public final class SliderSetting extends SettingsItem {
private int mMin;
private int mMax;
private int mDefaultValue;
private String mUnits;
public SliderSetting(String key, String section, int titleId, int descriptionId,
int min, int max, String units, int defaultValue, Setting setting) {
super(key, section, setting, titleId, descriptionId);
mMin = min;
mMax = max;
mUnits = units;
mDefaultValue = defaultValue;
}
public int getMin() {
return mMin;
}
public int getMax() {
return mMax;
}
public int getDefaultValue() {
return mDefaultValue;
}
public int getSelectedValue() {
Setting setting = getSetting();
if (setting == null) {
return mDefaultValue;
}
if (setting instanceof IntSetting) {
IntSetting intSetting = (IntSetting) setting;
return intSetting.getValue();
} else if (setting instanceof FloatSetting) {
FloatSetting floatSetting = (FloatSetting) setting;
return Math.round(floatSetting.getValue());
} else {
Log.error("[SliderSetting] Error casting setting type.");
return -1;
}
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return null if overwritten successfully otherwise; a newly created IntSetting.
*/
public IntSetting setSelectedValue(int selection) {
if (getSetting() == null) {
IntSetting setting = new IntSetting(getKey(), getSection(), selection);
setSetting(setting);
return setting;
} else {
IntSetting setting = (IntSetting) getSetting();
setting.setValue(selection);
return null;
}
}
/**
* Write a value to the backing float. If that float was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the float.
* @return null if overwritten successfully otherwise; a newly created FloatSetting.
*/
public FloatSetting setSelectedValue(float selection) {
if (getSetting() == null) {
FloatSetting setting = new FloatSetting(getKey(), getSection(), selection);
setSetting(setting);
return setting;
} else {
FloatSetting setting = (FloatSetting) getSetting();
setting.setValue(selection);
return null;
}
}
public String getUnits() {
return mUnits;
}
@Override
public int getType() {
return TYPE_SLIDER;
}
}

View File

@ -0,0 +1,82 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.StringSetting;
public class StringSingleChoiceSetting extends SettingsItem {
private String mDefaultValue;
private String[] mChoicesId;
private String[] mValuesId;
public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
String[] choicesId, String[] valuesId, String defaultValue, Setting setting) {
super(key, section, setting, titleId, descriptionId);
mValuesId = valuesId;
mChoicesId = choicesId;
mDefaultValue = defaultValue;
}
public String[] getChoicesId() {
return mChoicesId;
}
public String[] getValuesId() {
return mValuesId;
}
public String getValueAt(int index) {
if (mValuesId == null)
return null;
if (index >= 0 && index < mValuesId.length) {
return mValuesId[index];
}
return "";
}
public String getSelectedValue() {
if (getSetting() != null) {
StringSetting setting = (StringSetting) getSetting();
return setting.getValue();
} else {
return mDefaultValue;
}
}
public int getSelectValueIndex() {
String selectedValue = getSelectedValue();
for (int i = 0; i < mValuesId.length; i++) {
if (mValuesId[i].equals(selectedValue)) {
return i;
}
}
return -1;
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return null if overwritten successfully otherwise; a newly created IntSetting.
*/
public StringSetting setSelectedValue(String selection) {
if (getSetting() == null) {
StringSetting setting = new StringSetting(getKey(), getSection(), selection);
setSetting(setting);
return setting;
} else {
StringSetting setting = (StringSetting) getSetting();
setting.setValue(selection);
return null;
}
}
@Override
public int getType() {
return TYPE_STRING_SINGLE_CHOICE;
}
}

View File

@ -0,0 +1,21 @@
package org.citra.citra_emu.features.settings.model.view;
import org.citra.citra_emu.features.settings.model.Setting;
public final class SubmenuSetting extends SettingsItem {
private String mMenuKey;
public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) {
super(key, null, setting, titleId, descriptionId);
mMenuKey = menuKey;
}
public String getMenuKey() {
return mMenuKey;
}
@Override
public int getType() {
return TYPE_SUBMENU;
}
}

View File

@ -0,0 +1,217 @@
package org.citra.citra_emu.features.settings.ui;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentTransaction;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.citra.citra_emu.R;
import org.citra.citra_emu.utils.DirectoryInitialization;
import org.citra.citra_emu.utils.DirectoryStateReceiver;
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
private static final String ARG_MENU_TAG = "menu_tag";
private static final String ARG_GAME_ID = "game_id";
private static final String FRAGMENT_TAG = "settings";
private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this);
private ProgressDialog dialog;
public static void launch(Context context, String menuTag, String gameId) {
Intent settings = new Intent(context, SettingsActivity.class);
settings.putExtra(ARG_MENU_TAG, menuTag);
settings.putExtra(ARG_GAME_ID, gameId);
context.startActivity(settings);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Intent launcher = getIntent();
String gameID = launcher.getStringExtra(ARG_GAME_ID);
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
mPresenter.onCreate(savedInstanceState, menuTag, gameID);
// Show "Back" button in the action bar for navigation
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_settings, menu);
return true;
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
// Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState);
mPresenter.saveState(outState);
}
@Override
protected void onStart() {
super.onStart();
mPresenter.onStart();
}
/**
* If this is called, the user has left the settings screen (potentially through the
* home button) and will expect their changes to be persisted. So we kick off an
* IntentService which will do so on a background thread.
*/
@Override
protected void onStop() {
super.onStop();
mPresenter.onStop(isFinishing());
}
@Override
public void onBackPressed() {
mPresenter.onBackPressed();
}
@Override
public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (addToStack) {
if (areSystemAnimationsEnabled()) {
transaction.setCustomAnimations(
R.animator.settings_enter,
R.animator.settings_exit,
R.animator.settings_pop_enter,
R.animator.setttings_pop_exit);
}
transaction.addToBackStack(null);
mPresenter.addToStack();
}
transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG);
transaction.commit();
}
private boolean areSystemAnimationsEnabled() {
float duration = Settings.Global.getFloat(
getContentResolver(),
Settings.Global.ANIMATOR_DURATION_SCALE, 1);
float transition = Settings.Global.getFloat(
getContentResolver(),
Settings.Global.TRANSITION_ANIMATION_SCALE, 1);
return duration != 0 && transition != 0;
}
@Override
public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) {
LocalBroadcastManager.getInstance(this).registerReceiver(
receiver,
filter);
DirectoryInitialization.start(this);
}
@Override
public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
}
@Override
public void showLoading() {
if (dialog == null) {
dialog = new ProgressDialog(this);
dialog.setMessage(getString(R.string.load_settings));
dialog.setIndeterminate(true);
}
dialog.show();
}
@Override
public void hideLoading() {
dialog.dismiss();
}
@Override
public void showPermissionNeededHint() {
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
}
@Override
public void showExternalStorageNotMountedHint() {
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
.show();
}
@Override
public org.citra.citra_emu.features.settings.model.Settings getSettings() {
return mPresenter.getSettings();
}
@Override
public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) {
mPresenter.setSettings(settings);
}
@Override
public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) {
SettingsFragmentView fragment = getFragment();
if (fragment != null) {
fragment.onSettingsFileLoaded(settings);
}
}
@Override
public void onSettingsFileNotFound() {
SettingsFragmentView fragment = getFragment();
if (fragment != null) {
fragment.loadDefaultSettings();
}
}
@Override
public void showToastMessage(String message, boolean is_long) {
Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
}
@Override
public void popBackStack() {
getSupportFragmentManager().popBackStackImmediate();
}
@Override
public void onSettingChanged() {
mPresenter.onSettingChanged();
}
private SettingsFragment getFragment() {
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
}
}

View File

@ -0,0 +1,140 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.IntentFilter;
import android.os.Bundle;
import android.text.TextUtils;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.DirectoryInitialization;
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
import org.citra.citra_emu.utils.DirectoryStateReceiver;
import org.citra.citra_emu.utils.Log;
import org.citra.citra_emu.utils.ThemeUtil;
import java.io.File;
public final class SettingsActivityPresenter {
private static final String KEY_SHOULD_SAVE = "should_save";
private SettingsActivityView mView;
private Settings mSettings = new Settings();
private int mStackCount;
private boolean mShouldSave;
private DirectoryStateReceiver directoryStateReceiver;
private String menuTag;
private String gameId;
public SettingsActivityPresenter(SettingsActivityView view) {
mView = view;
}
public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) {
if (savedInstanceState == null) {
this.menuTag = menuTag;
this.gameId = gameId;
} else {
mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE);
}
}
public void onStart() {
this.mStackCount = 0;
prepareCitraDirectoriesIfNeeded();
}
void loadSettingsUI() {
if (mSettings.isEmpty()) {
if (!TextUtils.isEmpty(gameId)) {
mSettings.loadSettings(gameId, mView);
} else {
mSettings.loadSettings(mView);
}
}
mView.showSettingsFragment(menuTag, false, gameId);
mView.onSettingsFileLoaded(mSettings);
}
private void prepareCitraDirectoriesIfNeeded() {
File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini");
if (!configFile.exists()) {
Log.error("Citra config file could not be found!");
}
if (DirectoryInitialization.areCitraDirectoriesReady()) {
loadSettingsUI();
} else {
mView.showLoading();
IntentFilter statusIntentFilter = new IntentFilter(
DirectoryInitialization.BROADCAST_ACTION);
directoryStateReceiver =
new DirectoryStateReceiver(directoryInitializationState ->
{
if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
mView.hideLoading();
loadSettingsUI();
} else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
mView.showPermissionNeededHint();
mView.hideLoading();
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
mView.showExternalStorageNotMountedHint();
mView.hideLoading();
}
});
mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter);
}
}
public void setSettings(Settings settings) {
mSettings = settings;
}
public Settings getSettings() {
return mSettings;
}
public void onStop(boolean finishing) {
if (directoryStateReceiver != null) {
mView.stopListeningToDirectoryInitializationService(directoryStateReceiver);
directoryStateReceiver = null;
}
if (mSettings != null && finishing && mShouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
mSettings.saveSettings(mView);
}
ThemeUtil.applyTheme();
NativeLibrary.ReloadSettings();
}
public void addToStack() {
mStackCount++;
}
public void onBackPressed() {
if (mStackCount > 0) {
mView.popBackStack();
mStackCount--;
} else {
mView.finish();
}
}
public void onSettingChanged() {
mShouldSave = true;
}
public void saveState(Bundle outState) {
outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
}
}

View File

@ -0,0 +1,108 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.IntentFilter;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.utils.DirectoryStateReceiver;
/**
* Abstraction for the Activity that manages SettingsFragments.
*/
public interface SettingsActivityView {
/**
* Show a new SettingsFragment.
*
* @param menuTag Identifier for the settings group that should be displayed.
* @param addToStack Whether or not this fragment should replace a previous one.
*/
void showSettingsFragment(String menuTag, boolean addToStack, String gameId);
/**
* Called by a contained Fragment to get access to the Setting HashMap
* loaded from disk, so that each Fragment doesn't need to perform its own
* read operation.
*
* @return A possibly null HashMap of Settings.
*/
Settings getSettings();
/**
* Used to provide the Activity with Settings HashMaps if a Fragment already
* has one; for example, if a rotation occurs, the Fragment will not be killed,
* but the Activity will, so the Activity needs to have its HashMaps resupplied.
*
* @param settings The ArrayList of all the Settings HashMaps.
*/
void setSettings(Settings settings);
/**
* Called when an asynchronous load operation completes.
*
* @param settings The (possibly null) result of the ini load operation.
*/
void onSettingsFileLoaded(Settings settings);
/**
* Called when an asynchronous load operation fails.
*/
void onSettingsFileNotFound();
/**
* Display a popup text message on screen.
*
* @param message The contents of the onscreen message.
* @param is_long Whether this should be a long Toast or short one.
*/
void showToastMessage(String message, boolean is_long);
/**
* Show the previous fragment.
*/
void popBackStack();
/**
* End the activity.
*/
void finish();
/**
* Called by a containing Fragment to tell the Activity that a setting was changed;
* unless this has been called, the Activity will not save to disk.
*/
void onSettingChanged();
/**
* Show loading dialog while loading the settings
*/
void showLoading();
/**
* Hide the loading the dialog
*/
void hideLoading();
/**
* Show a hint to the user that the app needs write to external storage access
*/
void showPermissionNeededHint();
/**
* Show a hint to the user that the app needs the external storage to be mounted
*/
void showExternalStorageNotMountedHint();
/**
* Start the DirectoryInitialization and listen for the result.
*
* @param receiver the broadcast receiver for the DirectoryInitialization
* @param filter the Intent broadcasts to be received.
*/
void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter);
/**
* Stop listening to the DirectoryInitialization.
*
* @param receiver The broadcast receiver to unregister.
*/
void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver);
}

View File

@ -0,0 +1,487 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.Context;
import android.content.DialogInterface;
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.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.dialogs.MotionAlertDialog;
import org.citra.citra_emu.features.settings.model.FloatSetting;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
import org.citra.citra_emu.ui.main.MainActivity;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener {
private SettingsFragmentView mView;
private Context mContext;
private ArrayList<SettingsItem> mSettings;
private SettingsItem mClickedItem;
private int mClickedPosition;
private int mSeekbarProgress;
private AlertDialog mDialog;
private TextView mTextSliderValue;
public SettingsAdapter(SettingsFragmentView view, Context context) {
mView = view;
mContext = context;
mClickedPosition = -1;
}
@Override
public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case SettingsItem.TYPE_HEADER:
view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
return new HeaderViewHolder(view, this);
case SettingsItem.TYPE_CHECKBOX:
view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
return new CheckBoxSettingViewHolder(view, this);
case SettingsItem.TYPE_SINGLE_CHOICE:
case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SingleChoiceViewHolder(view, this);
case SettingsItem.TYPE_SLIDER:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SliderViewHolder(view, this);
case SettingsItem.TYPE_SUBMENU:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SubmenuViewHolder(view, this);
case SettingsItem.TYPE_INPUT_BINDING:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new InputBindingSettingViewHolder(view, this, mContext);
case SettingsItem.TYPE_DATETIME_SETTING:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new DateTimeViewHolder(view, this);
case SettingsItem.TYPE_PREMIUM:
view = inflater.inflate(R.layout.premium_item_setting, parent, false);
return new PremiumViewHolder(view, this, mView);
default:
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
return null;
}
}
@Override
public void onBindViewHolder(SettingViewHolder holder, int position) {
holder.bind(getItem(position));
}
private SettingsItem getItem(int position) {
return mSettings.get(position);
}
@Override
public int getItemCount() {
if (mSettings != null) {
return mSettings.size();
} else {
return 0;
}
}
@Override
public int getItemViewType(int position) {
return getItem(position).getType();
}
public void setSettings(ArrayList<SettingsItem> settings) {
mSettings = settings;
notifyDataSetChanged();
}
public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
IntSetting setting = item.setChecked(checked);
notifyItemChanged(position);
if (setting != null) {
mView.putSetting(setting);
}
mView.onSettingChanged();
}
public void onSingleChoiceClick(PremiumSingleChoiceSetting item) {
mClickedItem = item;
int value = getSelectionForSingleChoiceValue(item);
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
public void onSingleChoiceClick(SingleChoiceSetting item) {
mClickedItem = item;
int value = getSelectionForSingleChoiceValue(item);
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
mClickedPosition = position;
if (!item.isPremium() || MainActivity.isPremiumActive()) {
// Setting is either not Premium, or the user has Premium
onSingleChoiceClick(item);
return;
}
// User needs Premium, invoke the billing flow
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
}
public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) {
mClickedPosition = position;
if (!item.isPremium() || MainActivity.isPremiumActive()) {
// Setting is either not Premium, or the user has Premium
onSingleChoiceClick(item);
return;
}
// User needs Premium, invoke the billing flow
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
}
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
mClickedItem = item;
AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity());
builder.setTitle(item.getNameId());
builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
mDialog = builder.show();
}
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
mClickedPosition = position;
if (!item.isPremium() || MainActivity.isPremiumActive()) {
// Setting is either not Premium, or the user has Premium
onStringSingleChoiceClick(item);
return;
}
// User needs Premium, invoke the billing flow
MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item));
}
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
public void onDateTimeClick(DateTimeSetting item, int position) {
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);
DatePicker dp = view.findViewById(R.id.date_picker);
TimePicker tp = view.findViewById(R.id.time_picker);
//set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
String settingValue = item.getValue();
dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
tp.setIs24HourView(true);
tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
DialogInterface.OnClickListener ok = (dialog, which) -> {
//set it
int year = dp.getYear();
if (year < 2000) {
year = 2000;
}
String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
StringSetting setting = item.setSelectedValue(datetime);
if (setting != null) {
mView.putSetting(setting);
}
mView.onSettingChanged();
mClickedItem = null;
closeDialog();
};
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, ok);
builder.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());
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.dialog_seekbar, null);
SeekBar seekbar = view.findViewById(R.id.seekbar);
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);
});
mDialog = builder.show();
mTextSliderValue = view.findViewById(R.id.text_value);
mTextSliderValue.setText(String.valueOf(mSeekbarProgress));
TextView units = view.findViewById(R.id.text_units);
units.setText(item.getUnits());
seekbar.setMin(item.getMin());
seekbar.setMax(item.getMax());
seekbar.setProgress(mSeekbarProgress);
seekbar.setOnSeekBarChangeListener(this);
}
public void onSubmenuClick(SubmenuSetting item) {
mView.loadSubMenu(item.getMenuKey());
}
public void onInputBindingClick(final InputBindingSetting item, final int position) {
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
dialog.setTitle(R.string.input_binding);
int messageResId = R.string.input_binding_description;
if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
// Use specialized message for axis left/right or up/down
if (item.IsHorizontalOrientation()) {
messageResId = R.string.input_binding_description_horizontal_axis;
} else {
messageResId = R.string.input_binding_description_vertical_axis;
}
}
dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
item.removeOldMapping());
dialog.setOnDismissListener(dialog1 ->
{
StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
notifyItemChanged(position);
mView.putSetting(setting);
mView.onSettingChanged();
});
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mClickedItem instanceof SingleChoiceSetting) {
SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
int value = getValueForSingleChoiceSelection(scSetting, which);
if (scSetting.getSelectedValue() != value) {
mView.onSettingChanged();
}
// Get the backing Setting, which may be null (if for example it was missing from the file)
IntSetting setting = scSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
closeDialog();
} else if (mClickedItem instanceof PremiumSingleChoiceSetting) {
PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem;
scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which));
closeDialog();
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
String value = scSetting.getValueAt(which);
if (!scSetting.getSelectedValue().equals(value))
mView.onSettingChanged();
StringSetting setting = scSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
closeDialog();
} else if (mClickedItem instanceof SliderSetting) {
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
if (sliderSetting.getSelectedValue() != mSeekbarProgress) {
mView.onSettingChanged();
}
if (sliderSetting.getSetting() instanceof FloatSetting) {
float value = (float) mSeekbarProgress;
FloatSetting setting = sliderSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
} else {
IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress);
if (setting != null) {
mView.putSetting(setting);
}
}
closeDialog();
}
mClickedItem = null;
mSeekbarProgress = -1;
}
public void closeDialog() {
if (mDialog != null) {
if (mClickedPosition != -1) {
notifyItemChanged(mClickedPosition);
mClickedPosition = -1;
}
mDialog.dismiss();
mDialog = null;
}
}
@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();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
return valuesArray[which];
} else {
return which;
}
}
private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) {
int valuesId = item.getValuesId();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
return valuesArray[which];
} else {
return which;
}
}
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
int value = item.getSelectedValue();
int valuesId = item.getValuesId();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
for (int index = 0; index < valuesArray.length; index++) {
int current = valuesArray[index];
if (current == value) {
return index;
}
}
} else {
return value;
}
return -1;
}
private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) {
int value = item.getSelectedValue();
int valuesId = item.getValuesId();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
for (int index = 0; index < valuesArray.length; index++) {
int current = valuesArray[index];
if (current == value) {
return index;
}
}
} else {
return value;
}
return -1;
}
}

View File

@ -0,0 +1,136 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.ui.DividerItemDecoration;
import java.util.ArrayList;
public final class SettingsFragment extends Fragment implements SettingsFragmentView {
private static final String ARGUMENT_MENU_TAG = "menu_tag";
private static final String ARGUMENT_GAME_ID = "game_id";
private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
private SettingsActivityView mActivity;
private SettingsAdapter mAdapter;
public static Fragment newInstance(String menuTag, String gameId) {
SettingsFragment fragment = new SettingsFragment();
Bundle arguments = new Bundle();
arguments.putString(ARGUMENT_MENU_TAG, menuTag);
arguments.putString(ARGUMENT_GAME_ID, gameId);
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mActivity = (SettingsActivityView) context;
mPresenter.onAttach();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
String gameId = getArguments().getString(ARGUMENT_GAME_ID);
mAdapter = new SettingsAdapter(this, getActivity());
mPresenter.onCreate(menuTag, gameId);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_settings, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
RecyclerView recyclerView = view.findViewById(R.id.list_settings);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(manager);
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
SettingsActivityView activity = (SettingsActivityView) getActivity();
mPresenter.onViewCreated(activity.getSettings());
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null;
if (mAdapter != null) {
mAdapter.closeDialog();
}
}
@Override
public void onSettingsFileLoaded(Settings settings) {
mPresenter.setSettings(settings);
}
@Override
public void passSettingsToActivity(Settings settings) {
if (mActivity != null) {
mActivity.setSettings(settings);
}
}
@Override
public void showSettingsList(ArrayList<SettingsItem> settingsList) {
mAdapter.setSettings(settingsList);
}
@Override
public void loadDefaultSettings() {
mPresenter.loadDefaultSettings();
}
@Override
public void loadSubMenu(String menuKey) {
mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
}
@Override
public void showToastMessage(String message, boolean is_long) {
mActivity.showToastMessage(message, is_long);
}
@Override
public void putSetting(Setting setting) {
mPresenter.putSetting(setting);
}
@Override
public void onSettingChanged() {
mActivity.onSettingChanged();
}
}

View File

@ -0,0 +1,388 @@
package org.citra.citra_emu.features.settings.ui;
import android.app.Activity;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.text.TextUtils;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.SettingSection;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
public final class SettingsFragmentPresenter {
private SettingsFragmentView mView;
private String mMenuTag;
private String mGameID;
private Settings mSettings;
private ArrayList<SettingsItem> mSettingsList;
public SettingsFragmentPresenter(SettingsFragmentView view) {
mView = view;
}
public void onCreate(String menuTag, String gameId) {
mGameID = gameId;
mMenuTag = menuTag;
}
public void onViewCreated(Settings settings) {
setSettings(settings);
}
/**
* If the screen is rotated, the Activity will forget the settings map. This fragment
* won't, though; so rather than have the Activity reload from disk, have the fragment pass
* the settings map back to the Activity.
*/
public void onAttach() {
if (mSettings != null) {
mView.passSettingsToActivity(mSettings);
}
}
public void putSetting(Setting setting) {
mSettings.getSection(setting.getSection()).putSetting(setting);
}
private StringSetting asStringSetting(Setting setting) {
if (setting == null) {
return null;
}
StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
putSetting(stringSetting);
return stringSetting;
}
public void loadDefaultSettings() {
loadSettingsList();
}
public void setSettings(Settings settings) {
if (mSettingsList == null && settings != null) {
mSettings = settings;
loadSettingsList();
} else {
mView.getActivity().setTitle(R.string.preferences_settings);
mView.showSettingsList(mSettingsList);
}
}
private void loadSettingsList() {
if (!TextUtils.isEmpty(mGameID)) {
mView.getActivity().setTitle("Game Settings: " + mGameID);
}
ArrayList<SettingsItem> sl = new ArrayList<>();
switch (mMenuTag) {
case SettingsFile.FILE_NAME_CONFIG:
addConfigSettings(sl);
break;
case Settings.SECTION_PREMIUM:
addPremiumSettings(sl);
break;
case Settings.SECTION_CORE:
addGeneralSettings(sl);
break;
case Settings.SECTION_SYSTEM:
addSystemSettings(sl);
break;
case Settings.SECTION_CAMERA:
addCameraSettings(sl);
break;
case Settings.SECTION_CONTROLS:
addInputSettings(sl);
break;
case Settings.SECTION_RENDERER:
addGraphicsSettings(sl);
break;
case Settings.SECTION_AUDIO:
addAudioSettings(sl);
break;
case Settings.SECTION_DEBUG:
addDebugSettings(sl);
break;
default:
mView.showToastMessage("Unimplemented menu", false);
return;
}
mSettingsList = sl;
mView.showSettingsList(mSettingsList);
}
private void addConfigSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_settings);
sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM));
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
}
private void addPremiumSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_premium);
SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
sl.add(new PremiumHeader());
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView));
} else {
// Pre-Android 10 does not support System Default
sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView));
}
String[] textureFilterNames = NativeLibrary.GetTextureFilterNames();
Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, R.string.texture_filter_description, textureFilterNames, textureFilterNames, "none", textureFilterName));
}
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_general);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
}
private void addSystemSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_system);
SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
}
private void addCameraSettings(ArrayList<SettingsItem> sl) {
final Activity activity = mView.getActivity();
activity.setTitle(R.string.preferences_camera);
// Get the camera IDs
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
ArrayList<String> supportedCameraNameList = new ArrayList<>();
ArrayList<String> supportedCameraIdList = new ArrayList<>();
if (cameraManager != null) {
try {
for (String id : cameraManager.getCameraIdList()) {
final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue; // Legacy cameras cannot be used with the NDK
}
supportedCameraIdList.add(id);
final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
int stringId = R.string.camera_facing_external;
switch (facing) {
case CameraCharacteristics.LENS_FACING_FRONT:
stringId = R.string.camera_facing_front;
break;
case CameraCharacteristics.LENS_FACING_BACK:
stringId = R.string.camera_facing_back;
break;
case CameraCharacteristics.LENS_FACING_EXTERNAL:
stringId = R.string.camera_facing_external;
break;
}
supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
}
} catch (CameraAccessException e) {
Log.error("Couldn't retrieve camera list");
e.printStackTrace();
}
}
// Create the names and values for display
ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
cameraDeviceNameList.addAll(supportedCameraNameList);
ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
cameraDeviceValueList.addAll(supportedCameraIdList);
final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
if (!haveCameraDevices) {
// Remove the last entry (ndk / Device Camera)
imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
}
final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
}
private void addInputSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_controls);
SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
// Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
// Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
// Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
// Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
// TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
}
private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_graphics);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
Setting useAsynchronousGpuEmulation = rendererSection.getSetting(SettingsFile.KEY_USE_ASYNCHRONOUS_GPU_EMULATION);
Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
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_USE_ASYNCHRONOUS_GPU_EMULATION, Settings.SECTION_RENDERER, R.string.asynchronous_gpu, R.string.asynchronous_gpu_description, true, useAsynchronousGpuEmulation));
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));
}
private void addAudioSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_audio);
SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE);
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_MIC_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 1, micInputType));
}
private void addDebugSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_debug);
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);
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));
}
}

View File

@ -0,0 +1,78 @@
package org.citra.citra_emu.features.settings.ui;
import androidx.fragment.app.FragmentActivity;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import java.util.ArrayList;
/**
* Abstraction for a screen showing a list of settings. Instances of
* this type of view will each display a layer of the setting hierarchy.
*/
public interface SettingsFragmentView {
/**
* Called by the containing Activity to notify the Fragment that an
* asynchronous load operation completed.
*
* @param settings The (possibly null) result of the ini load operation.
*/
void onSettingsFileLoaded(Settings settings);
/**
* Pass a settings HashMap to the containing activity, so that it can
* share the HashMap with other SettingsFragments; useful so that rotations
* do not require an additional load operation.
*
* @param settings An ArrayList containing all the settings HashMaps.
*/
void passSettingsToActivity(Settings settings);
/**
* Pass an ArrayList to the View so that it can be displayed on screen.
*
* @param settingsList The result of converting the HashMap to an ArrayList
*/
void showSettingsList(ArrayList<SettingsItem> settingsList);
/**
* Called by the containing Activity when an asynchronous load operation fails.
* Instructs the Fragment to load the settings screen with defaults selected.
*/
void loadDefaultSettings();
/**
* @return The Fragment's containing activity.
*/
FragmentActivity getActivity();
/**
* Tell the Fragment to tell the containing Activity to show a new
* Fragment containing a submenu of settings.
*
* @param menuKey Identifier for the settings group that should be shown.
*/
void loadSubMenu(String menuKey);
/**
* Tell the Fragment to tell the containing activity to display a toast message.
*
* @param message Text to be shown in the Toast
* @param is_long Whether this should be a long Toast or short one.
*/
void showToastMessage(String message, boolean is_long);
/**
* Have the fragment add a setting to the HashMap.
*
* @param setting The (possibly previously missing) new setting.
*/
void putSetting(Setting setting);
/**
* Have the fragment tell the containing Activity that a setting was modified.
*/
void onSettingChanged();
}

View File

@ -0,0 +1,48 @@
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

@ -0,0 +1,54 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class CheckBoxSettingViewHolder extends SettingViewHolder {
private CheckBoxSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
private CheckBox mCheckbox;
public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
mCheckbox = root.findViewById(R.id.checkbox);
}
@Override
public void bind(SettingsItem item) {
mItem = (CheckBoxSetting) item;
mTextSettingName.setText(item.getNameId());
if (item.getDescriptionId() > 0) {
mTextSettingDescription.setText(item.getDescriptionId());
mTextSettingDescription.setVisibility(View.VISIBLE);
} else {
mTextSettingDescription.setText("");
mTextSettingDescription.setVisibility(View.GONE);
}
mCheckbox.setChecked(mItem.isChecked());
}
@Override
public void onClick(View clicked) {
mCheckbox.toggle();
getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
}
}

View File

@ -0,0 +1,47 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
import org.citra.citra_emu.utils.Log;
public final class DateTimeViewHolder extends SettingViewHolder {
private DateTimeSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
public DateTimeViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
Log.error("test " + mTextSettingName);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
Log.error("test " + mTextSettingDescription);
}
@Override
public void bind(SettingsItem item) {
mItem = (DateTimeSetting) item;
mTextSettingName.setText(item.getNameId());
if (item.getDescriptionId() > 0) {
mTextSettingDescription.setText(item.getDescriptionId());
mTextSettingDescription.setVisibility(View.VISIBLE);
} else {
mTextSettingDescription.setVisibility(View.GONE);
}
}
@Override
public void onClick(View clicked) {
getAdapter().onDateTimeClick(mItem, getAdapterPosition());
}
}

View File

@ -0,0 +1,32 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class HeaderViewHolder extends SettingViewHolder {
private TextView mHeaderName;
public HeaderViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
itemView.setOnClickListener(null);
}
@Override
protected void findViews(View root) {
mHeaderName = root.findViewById(R.id.text_header_name);
}
@Override
public void bind(SettingsItem item) {
mHeaderName.setText(item.getNameId());
}
@Override
public void onClick(View clicked) {
// no-op
}
}

View File

@ -0,0 +1,55 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class InputBindingSettingViewHolder extends SettingViewHolder {
private InputBindingSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
private Context mContext;
public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) {
super(itemView, adapter);
mContext = context;
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
mItem = (InputBindingSetting) item;
mTextSettingName.setText(item.getNameId());
String key = sharedPreferences.getString(mItem.getKey(), "");
if (key != null && !key.isEmpty()) {
mTextSettingDescription.setText(key);
mTextSettingDescription.setVisibility(View.VISIBLE);
} else {
mTextSettingDescription.setVisibility(View.GONE);
}
}
@Override
public void onClick(View clicked) {
getAdapter().onInputBindingClick(mItem, getAdapterPosition());
}
}

View File

@ -0,0 +1,57 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
import org.citra.citra_emu.ui.main.MainActivity;
public final class PremiumViewHolder extends SettingViewHolder {
private TextView mHeaderName;
private TextView mTextDescription;
private SettingsFragmentView mView;
public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) {
super(itemView, adapter);
mView = view;
itemView.setOnClickListener(this);
}
@Override
protected void findViews(View root) {
mHeaderName = root.findViewById(R.id.text_setting_name);
mTextDescription = root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item) {
updateText();
}
@Override
public void onClick(View clicked) {
if (MainActivity.isPremiumActive()) {
return;
}
// Invoke billing flow if Premium is not already active, then refresh the UI to indicate
// the purchase has completed.
MainActivity.invokePremiumBilling(() -> updateText());
}
/**
* Update the text shown to the user, based on whether Premium is active
*/
private void updateText() {
if (MainActivity.isPremiumActive()) {
mHeaderName.setText(R.string.premium_settings_welcome);
mTextDescription.setText(R.string.premium_settings_welcome_description);
} else {
mHeaderName.setText(R.string.premium_settings_upsell);
mTextDescription.setText(R.string.premium_settings_upsell_description);
}
}
}

View File

@ -0,0 +1,49 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SettingsAdapter mAdapter;
public SettingViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView);
mAdapter = adapter;
itemView.setOnClickListener(this);
findViews(itemView);
}
protected SettingsAdapter getAdapter() {
return mAdapter;
}
/**
* Gets handles to all this ViewHolder's child views using their XML-defined identifiers.
*
* @param root The newly inflated top-level view.
*/
protected abstract void findViews(View root);
/**
* Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent.
*
* @param item The list item that should be represented by this ViewHolder.
*/
public abstract void bind(SettingsItem item);
/**
* Called when this ViewHolder's view is clicked on. Implementations should usually pass
* this event up to the adapter.
*
* @param clicked The view that was clicked on.
*/
public abstract void onClick(View clicked);
}

View File

@ -0,0 +1,76 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.content.res.Resources;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class SingleChoiceViewHolder extends SettingViewHolder {
private SettingsItem mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item) {
mItem = item;
mTextSettingName.setText(item.getNameId());
mTextSettingDescription.setVisibility(View.VISIBLE);
if (item.getDescriptionId() > 0) {
mTextSettingDescription.setText(item.getDescriptionId());
} else if (item instanceof SingleChoiceSetting) {
SingleChoiceSetting setting = (SingleChoiceSetting) item;
int selected = setting.getSelectedValue();
Resources resMgr = mTextSettingDescription.getContext().getResources();
String[] choices = resMgr.getStringArray(setting.getChoicesId());
int[] values = resMgr.getIntArray(setting.getValuesId());
for (int i = 0; i < values.length; ++i) {
if (values[i] == selected) {
mTextSettingDescription.setText(choices[i]);
}
}
} else if (item instanceof PremiumSingleChoiceSetting) {
PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item;
int selected = setting.getSelectedValue();
Resources resMgr = mTextSettingDescription.getContext().getResources();
String[] choices = resMgr.getStringArray(setting.getChoicesId());
int[] values = resMgr.getIntArray(setting.getValuesId());
for (int i = 0; i < values.length; ++i) {
if (values[i] == selected) {
mTextSettingDescription.setText(choices[i]);
}
}
} else {
mTextSettingDescription.setVisibility(View.GONE);
}
}
@Override
public void onClick(View clicked) {
int position = getAdapterPosition();
if (mItem instanceof SingleChoiceSetting) {
getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
} else if (mItem instanceof PremiumSingleChoiceSetting) {
getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position);
} else if (mItem instanceof StringSingleChoiceSetting) {
getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
}
}
}

View File

@ -0,0 +1,46 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class SliderViewHolder extends SettingViewHolder {
private SliderSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
public SliderViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item) {
mItem = (SliderSetting) item;
mTextSettingName.setText(item.getNameId());
if (item.getDescriptionId() > 0) {
mTextSettingDescription.setText(item.getDescriptionId());
mTextSettingDescription.setVisibility(View.VISIBLE);
} else {
mTextSettingDescription.setVisibility(View.GONE);
}
}
@Override
public void onClick(View clicked) {
getAdapter().onSliderClick(mItem, getAdapterPosition());
}
}

View File

@ -0,0 +1,45 @@
package org.citra.citra_emu.features.settings.ui.viewholder;
import android.view.View;
import android.widget.TextView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
public final class SubmenuViewHolder extends SettingViewHolder {
private SubmenuSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
public SubmenuViewHolder(View itemView, SettingsAdapter adapter) {
super(itemView, adapter);
}
@Override
protected void findViews(View root) {
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item) {
mItem = (SubmenuSetting) item;
mTextSettingName.setText(item.getNameId());
if (item.getDescriptionId() > 0) {
mTextSettingDescription.setText(item.getDescriptionId());
mTextSettingDescription.setVisibility(View.VISIBLE);
} else {
mTextSettingDescription.setVisibility(View.GONE);
}
}
@Override
public void onClick(View clicked) {
getAdapter().onSubmenuClick(mItem);
}
}

View File

@ -0,0 +1,331 @@
package org.citra.citra_emu.features.settings.utils;
import androidx.annotation.NonNull;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.FloatSetting;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.SettingSection;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
import org.citra.citra_emu.utils.BiMap;
import org.citra.citra_emu.utils.DirectoryInitialization;
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.util.HashMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Contains static methods for interacting with .ini files in which settings are stored.
*/
public final class SettingsFile {
public static final String FILE_NAME_CONFIG = "config";
public static final String KEY_CPU_JIT = "use_cpu_jit";
public static final String KEY_DESIGN = "design";
public static final String KEY_PREMIUM = "premium";
public static final String KEY_HW_RENDERER = "use_hw_renderer";
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";
public static final String KEY_USE_VSYNC = "use_vsync_new";
public static final String KEY_RESOLUTION_FACTOR = "resolution_factor";
public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit";
public static final String KEY_FRAME_LIMIT = "frame_limit";
public static final String KEY_BACKGROUND_RED = "bg_red";
public static final String KEY_BACKGROUND_BLUE = "bg_blue";
public static final String KEY_BACKGROUND_GREEN = "bg_green";
public static final String KEY_FACTOR_3D = "factor_3d";
public static final String KEY_FILTER_MODE = "filter_mode";
public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name";
public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation";
public static final String KEY_LAYOUT_OPTION = "layout_option";
public static final String KEY_SWAP_SCREEN = "swap_screen";
public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine";
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
public static final String KEY_VOLUME = "volume";
public static final String KEY_MIC_INPUT_TYPE = "mic_input_type";
public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
public static final String KEY_IS_NEW_3DS = "is_new_3ds";
public static final String KEY_REGION_VALUE = "region_value";
public static final String KEY_LANGUAGE = "language";
public static final String KEY_INIT_CLOCK = "init_clock";
public static final String KEY_INIT_TIME = "init_time";
public static final String KEY_BUTTON_A = "button_a";
public static final String KEY_BUTTON_B = "button_b";
public static final String KEY_BUTTON_X = "button_x";
public static final String KEY_BUTTON_Y = "button_y";
public static final String KEY_BUTTON_SELECT = "button_select";
public static final String KEY_BUTTON_START = "button_start";
public static final String KEY_BUTTON_UP = "button_up";
public static final String KEY_BUTTON_DOWN = "button_down";
public static final String KEY_BUTTON_LEFT = "button_left";
public static final String KEY_BUTTON_RIGHT = "button_right";
public static final String KEY_BUTTON_L = "button_l";
public static final String KEY_BUTTON_R = "button_r";
public static final String KEY_BUTTON_ZL = "button_zl";
public static final String KEY_BUTTON_ZR = "button_zr";
public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical";
public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal";
public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical";
public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal";
public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical";
public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal";
public static final String KEY_CIRCLEPAD_UP = "circlepad_up";
public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down";
public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left";
public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right";
public static final String KEY_CSTICK_UP = "cstick_up";
public static final String KEY_CSTICK_DOWN = "cstick_down";
public static final String KEY_CSTICK_LEFT = "cstick_left";
public static final String KEY_CSTICK_RIGHT = "cstick_right";
public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name";
public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config";
public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip";
public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name";
public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config";
public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip";
public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name";
public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config";
public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip";
public static final String KEY_LOG_FILTER = "log_filter";
private static BiMap<String, String> sectionsMap = new BiMap<>();
static {
//TODO: Add members to sectionsMap when game-specific settings are added
}
private SettingsFile() {
}
/**
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param ini The ini file to load the settings from
* @param isCustomGame
* @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) {
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(ini));
SettingSection current = null;
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith("[") && line.endsWith("]")) {
current = sectionFromLine(line, isCustomGame);
sections.put(current.getName(), current);
} else if ((current != null)) {
Setting setting = settingFromLine(current, line);
if (setting != null) {
current.putSetting(setting);
}
}
}
} catch (FileNotFoundException e) {
Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} catch (IOException e) {
Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage());
}
}
}
return sections;
}
public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) {
return readFile(getSettingsFile(fileName), false, view);
}
/**
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param gameId the id of the game to load it's settings.
* @param view The current view.
*/
public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) {
return readFile(getCustomGameSettingsFile(gameId), true, view);
}
/**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* telling why it failed.
*
* @param fileName The target filename without a path or extension.
* @param sections The HashMap containing the Settings we want to serialize.
* @param view The current view.
*/
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
SettingsActivityView view) {
File ini = getSettingsFile(fileName);
try {
Wini writer = new Wini(ini);
Set<String> keySet = sections.keySet();
for (String key : keySet) {
SettingSection section = sections.get(key);
writeSection(writer, section);
}
writer.store();
} 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);
}
}
public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) {
Set<String> sortedSections = new TreeSet<>(sections.keySet());
for (String sectionKey : sortedSections) {
SettingSection section = sections.get(sectionKey);
HashMap<String, Setting> settings = section.getSettings();
Set<String> sortedKeySet = new TreeSet<>(settings.keySet());
for (String settingKey : sortedKeySet) {
Setting setting = settings.get(settingKey);
NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString());
}
}
}
private static String mapSectionNameFromIni(String generalSectionName) {
if (sectionsMap.getForward(generalSectionName) != null) {
return sectionsMap.getForward(generalSectionName);
}
return generalSectionName;
}
private static String mapSectionNameToIni(String generalSectionName) {
if (sectionsMap.getBackward(generalSectionName) != null) {
return sectionsMap.getBackward(generalSectionName);
}
return generalSectionName;
}
@NonNull
private static File getSettingsFile(String fileName) {
return new File(
DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini");
}
private static File getCustomGameSettingsFile(String gameId) {
return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini");
}
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
String sectionName = line.substring(1, line.length() - 1);
if (isCustomGame) {
sectionName = mapSectionNameToIni(sectionName);
}
return new SettingSection(sectionName);
}
/**
* For a line of text, determines what type of data is being represented, and returns
* a Setting object containing this data.
*
* @param current The section currently being parsed by the consuming method.
* @param line The line of text being parsed.
* @return A typed Setting containing the key/value contained in the line.
*/
private static Setting settingFromLine(SettingSection current, String line) {
String[] splitLine = line.split("=");
if (splitLine.length != 2) {
Log.warning("Skipping invalid config line \"" + line + "\"");
return null;
}
String key = splitLine[0].trim();
String value = splitLine[1].trim();
if (value.isEmpty()) {
Log.warning("Skipping null value in config line \"" + line + "\"");
return null;
}
try {
int valueAsInt = Integer.parseInt(value);
return new IntSetting(key, current.getName(), valueAsInt);
} catch (NumberFormatException ex) {
}
try {
float valueAsFloat = Float.parseFloat(value);
return new FloatSetting(key, current.getName(), valueAsFloat);
} catch (NumberFormatException ex) {
}
return new StringSetting(key, current.getName(), value);
}
/**
* Writes the contents of a Section HashMap to disk.
*
* @param parser A Wini pointed at a file on disk.
* @param section A section containing settings to be written to the file.
*/
private static void writeSection(Wini parser, SettingSection section) {
// Write the section header.
String header = section.getName();
// Write this section's values.
HashMap<String, Setting> settings = section.getSettings();
Set<String> keySet = settings.keySet();
for (String key : keySet) {
Setting setting = settings.get(key);
parser.put(header, setting.getKey(), setting.getValueAsString());
}
}
}

View File

@ -0,0 +1,120 @@
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,367 @@
package org.citra.citra_emu.fragments;
import android.content.Context;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.overlay.InputOverlay;
import org.citra.citra_emu.utils.DirectoryInitialization;
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
import org.citra.citra_emu.utils.DirectoryStateReceiver;
import org.citra.citra_emu.utils.EmulationMenuSettings;
import org.citra.citra_emu.utils.Log;
public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback {
private static final String KEY_GAMEPATH = "gamepath";
private static final Handler perfStatsUpdateHandler = new Handler();
private SharedPreferences mPreferences;
private InputOverlay mInputOverlay;
private EmulationState mEmulationState;
private DirectoryStateReceiver directoryStateReceiver;
private EmulationActivity activity;
private TextView mPerfStats;
private Runnable perfStatsUpdater;
public static EmulationFragment newInstance(String gamePath) {
Bundle args = new Bundle();
args.putString(KEY_GAMEPATH, gamePath);
EmulationFragment fragment = new EmulationFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof EmulationActivity) {
activity = (EmulationActivity) context;
NativeLibrary.setEmulationActivity((EmulationActivity) context);
} else {
throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
}
}
/**
* Initialize anything that doesn't depend on the layout / views in here.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// So this fragment doesn't restart on configuration changes; i.e. rotation.
setRetainInstance(true);
mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
String gamePath = getArguments().getString(KEY_GAMEPATH);
mEmulationState = new EmulationState(gamePath);
}
/**
* Initialize the UI and start emulation in here.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation);
surfaceView.getHolder().addCallback(this);
mInputOverlay = contents.findViewById(R.id.surface_input_overlay);
mPerfStats = contents.findViewById(R.id.show_fps_text);
Button doneButton = contents.findViewById(R.id.done_control_config);
if (doneButton != null) {
doneButton.setOnClickListener(v -> stopConfiguringControls());
}
// Show/hide the "Show FPS" overlay
updateShowFpsOverlay();
// The new Surface created here will get passed to the native code via onSurfaceChanged.
return contents;
}
@Override
public void onResume() {
super.onResume();
if (DirectoryInitialization.areCitraDirectoriesReady()) {
mEmulationState.run(activity.isActivityRecreated());
} else {
setupCitraDirectoriesThenStartEmulation();
}
}
@Override
public void onPause() {
if (directoryStateReceiver != null) {
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
directoryStateReceiver = null;
}
if (mEmulationState.isRunning())
mEmulationState.pause();
super.onPause();
}
@Override
public void onDetach() {
NativeLibrary.clearEmulationActivity();
super.onDetach();
}
private void setupCitraDirectoriesThenStartEmulation() {
IntentFilter statusIntentFilter = new IntentFilter(
DirectoryInitialization.BROADCAST_ACTION);
directoryStateReceiver =
new DirectoryStateReceiver(directoryInitializationState ->
{
if (directoryInitializationState ==
DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
mEmulationState.run(activity.isActivityRecreated());
} else if (directoryInitializationState ==
DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
} else if (directoryInitializationState ==
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
Toast.LENGTH_SHORT)
.show();
}
});
// Registers the DirectoryStateReceiver and its intent filters
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
directoryStateReceiver,
statusIntentFilter);
DirectoryInitialization.start(getActivity());
}
public void refreshInputOverlay() {
mInputOverlay.refreshControls();
}
public void resetInputOverlay() {
// Reset button scale
SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("controlScale", 50);
editor.apply();
mInputOverlay.resetButtonPlacement();
}
public void updateShowFpsOverlay() {
if (EmulationMenuSettings.getShowFps()) {
final int SYSTEM_FPS = 0;
final int FPS = 1;
final int FRAMETIME = 2;
final int SPEED = 3;
perfStatsUpdater = () ->
{
final double[] perfStats = NativeLibrary.GetPerfStats();
if (perfStats[FPS] > 0) {
mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5),
(int) (perfStats[SPEED] * 100.0 + 0.5)));
}
perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000);
};
perfStatsUpdateHandler.post(perfStatsUpdater);
mPerfStats.setVisibility(View.VISIBLE);
} else {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
}
mPerfStats.setVisibility(View.GONE);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// We purposely don't do anything here.
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height);
mEmulationState.newSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mEmulationState.clearSurface();
}
public void stopEmulation() {
mEmulationState.stop();
}
public void startConfiguringControls() {
getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE);
mInputOverlay.setIsInEditMode(true);
}
public void stopConfiguringControls() {
getView().findViewById(R.id.done_control_config).setVisibility(View.GONE);
mInputOverlay.setIsInEditMode(false);
}
public boolean isConfiguringControls() {
return mInputOverlay.isInEditMode();
}
private static class EmulationState {
private final String mGamePath;
private State state;
private Surface mSurface;
private boolean mRunWhenSurfaceIsValid;
EmulationState(String gamePath) {
mGamePath = gamePath;
// Starting state is stopped.
state = State.STOPPED;
}
public synchronized boolean isStopped() {
return state == State.STOPPED;
}
// Getters for the current state
public synchronized boolean isPaused() {
return state == State.PAUSED;
}
public synchronized boolean isRunning() {
return state == State.RUNNING;
}
public synchronized void stop() {
if (state != State.STOPPED) {
Log.debug("[EmulationFragment] Stopping emulation.");
state = State.STOPPED;
NativeLibrary.StopEmulation();
} else {
Log.warning("[EmulationFragment] Stop called while already stopped.");
}
}
// State changing methods
public synchronized void pause() {
if (state != State.PAUSED) {
state = State.PAUSED;
Log.debug("[EmulationFragment] Pausing emulation.");
// Release the surface before pausing, since emulation has to be running for that.
NativeLibrary.SurfaceDestroyed();
NativeLibrary.PauseEmulation();
} else {
Log.warning("[EmulationFragment] Pause called while already paused.");
}
}
public synchronized void run(boolean isActivityRecreated) {
if (isActivityRecreated) {
if (NativeLibrary.IsRunning()) {
state = State.PAUSED;
}
} else {
Log.debug("[EmulationFragment] activity resumed or fresh start");
}
// If the surface is set, run now. Otherwise, wait for it to get set.
if (mSurface != null) {
runWithValidSurface();
} else {
mRunWhenSurfaceIsValid = true;
}
}
// Surface callbacks
public synchronized void newSurface(Surface surface) {
mSurface = surface;
if (mRunWhenSurfaceIsValid) {
runWithValidSurface();
}
}
public synchronized void clearSurface() {
if (mSurface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.");
} else {
mSurface = null;
Log.debug("[EmulationFragment] Surface destroyed.");
if (state == State.RUNNING) {
NativeLibrary.SurfaceDestroyed();
state = State.PAUSED;
} else if (state == State.PAUSED) {
Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
} else {
Log.warning("[EmulationFragment] Surface cleared while emulation stopped.");
}
}
}
private void runWithValidSurface() {
mRunWhenSurfaceIsValid = false;
if (state == State.STOPPED) {
Thread mEmulationThread = new Thread(() ->
{
NativeLibrary.SurfaceChanged(mSurface);
Log.debug("[EmulationFragment] Starting emulation thread.");
NativeLibrary.Run(mGamePath);
}, "NativeEmulation");
mEmulationThread.start();
} else if (state == State.PAUSED) {
Log.debug("[EmulationFragment] Resuming emulation.");
NativeLibrary.SurfaceChanged(mSurface);
NativeLibrary.UnPauseEmulation();
} else {
Log.debug("[EmulationFragment] Bug, run called while already running.");
}
state = State.RUNNING;
}
private enum State {
STOPPED, RUNNING, PAUSED
}
}
}

View File

@ -0,0 +1,76 @@
package org.citra.citra_emu.model;
import android.content.ContentValues;
import android.database.Cursor;
import java.nio.file.Paths;
public final class Game {
private String mTitle;
private String mDescription;
private String mPath;
private String mGameId;
private String mCompany;
private String mRegions;
public Game(String title, String description, String regions, String path,
String gameId, String company) {
mTitle = title;
mDescription = description;
mRegions = regions;
mPath = path;
mGameId = gameId;
mCompany = company;
}
public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) {
ContentValues values = new ContentValues();
if (gameId.isEmpty()) {
// Homebrew, etc. may not have a game ID, use filename as a unique identifier
gameId = Paths.get(path).getFileName().toString();
}
values.put(GameDatabase.KEY_GAME_TITLE, title);
values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
values.put(GameDatabase.KEY_GAME_REGIONS, regions);
values.put(GameDatabase.KEY_GAME_PATH, path);
values.put(GameDatabase.KEY_GAME_ID, gameId);
values.put(GameDatabase.KEY_GAME_COMPANY, company);
return values;
}
public static Game fromCursor(Cursor cursor) {
return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
}
public String getTitle() {
return mTitle;
}
public String getDescription() {
return mDescription;
}
public String getCompany() {
return mCompany;
}
public String getRegions() {
return mRegions;
}
public String getPath() {
return mPath;
}
public String getGameId() {
return mGameId;
}
}

View File

@ -0,0 +1,280 @@
package org.citra.citra_emu.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.utils.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import rx.Observable;
/**
* A helper class that provides several utilities simplifying interaction with
* the SQLite database.
*/
public final class GameDatabase extends SQLiteOpenHelper {
public static final int COLUMN_DB_ID = 0;
public static final int GAME_COLUMN_PATH = 1;
public static final int GAME_COLUMN_TITLE = 2;
public static final int GAME_COLUMN_DESCRIPTION = 3;
public static final int GAME_COLUMN_REGIONS = 4;
public static final int GAME_COLUMN_GAME_ID = 5;
public static final int GAME_COLUMN_COMPANY = 6;
public static final int FOLDER_COLUMN_PATH = 1;
public static final String KEY_DB_ID = "_id";
public static final String KEY_GAME_PATH = "path";
public static final String KEY_GAME_TITLE = "title";
public static final String KEY_GAME_DESCRIPTION = "description";
public static final String KEY_GAME_REGIONS = "regions";
public static final String KEY_GAME_ID = "game_id";
public static final String KEY_GAME_COMPANY = "company";
public static final String KEY_FOLDER_PATH = "path";
public static final String TABLE_NAME_FOLDERS = "folders";
public static final String TABLE_NAME_GAMES = "games";
private static final int DB_VERSION = 2;
private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
private static final String TYPE_INTEGER = " INTEGER";
private static final String TYPE_STRING = " TEXT";
private static final String CONSTRAINT_UNIQUE = " UNIQUE";
private static final String SEPARATOR = ", ";
private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
+ KEY_GAME_PATH + TYPE_STRING + SEPARATOR
+ KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
+ KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
+ KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
+ KEY_GAME_ID + TYPE_STRING + SEPARATOR
+ KEY_GAME_COMPANY + TYPE_STRING + ")";
private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
+ KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
+ KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
public GameDatabase(Context context) {
// Superclass constructor builds a database or uses an existing one.
super(context, "games.db", null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase database) {
Log.debug("[GameDatabase] GameDatabase - Creating database...");
execSqlAndLog(database, SQL_CREATE_GAMES);
execSqlAndLog(database, SQL_CREATE_FOLDERS);
}
@Override
public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) {
Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..");
execSqlAndLog(database, SQL_DELETE_FOLDERS);
execSqlAndLog(database, SQL_CREATE_FOLDERS);
execSqlAndLog(database, SQL_DELETE_GAMES);
execSqlAndLog(database, SQL_CREATE_GAMES);
}
@Override
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " +
newVersion);
// Delete all the games
execSqlAndLog(database, SQL_DELETE_GAMES);
execSqlAndLog(database, SQL_CREATE_GAMES);
}
public void resetDatabase(SQLiteDatabase database) {
execSqlAndLog(database, SQL_DELETE_FOLDERS);
execSqlAndLog(database, SQL_CREATE_FOLDERS);
execSqlAndLog(database, SQL_DELETE_GAMES);
execSqlAndLog(database, SQL_CREATE_GAMES);
}
public void scanLibrary(SQLiteDatabase database) {
// Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
Cursor fileCursor = database.query(TABLE_NAME_GAMES,
null, // Get all columns.
null, // Get all rows.
null,
null, // No grouping.
null,
null); // Order of games is irrelevant.
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
fileCursor.moveToPosition(-1);
while (fileCursor.moveToNext()) {
String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
File game = new File(gamePath);
if (!game.exists()) {
Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
gamePath);
database.delete(TABLE_NAME_GAMES,
KEY_DB_ID + " = ?",
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
}
}
// Get a cursor listing all the folders the user has added to the library.
Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
null, // Get all columns.
null, // Get all rows.
null,
null, // No grouping.
null,
null); // Order of folders is irrelevant.
Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app", ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz"));
// Possibly overly defensive, but ensures that moveToNext() does not skip a row.
folderCursor.moveToPosition(-1);
// Iterate through all results of the DB query (i.e. all folders in the library.)
while (folderCursor.moveToNext()) {
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
File folder = new File(folderPath);
// If the folder is empty because it no longer exists, remove it from the library.
if (!folder.exists()) {
Log.error(
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
database.delete(TABLE_NAME_FOLDERS,
KEY_DB_ID + " = ?",
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
}
addGamesRecursive(database, folder, allowedExtensions, 3);
}
fileCursor.close();
folderCursor.close();
Arrays.stream(NativeLibrary.GetInstalledGamePaths())
.forEach(filePath -> attemptToAddGame(database, filePath));
database.close();
}
private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
if (depth <= 0) {
return;
}
File[] children = parent.listFiles();
if (children != null) {
for (File file : children) {
if (file.isHidden()) {
continue;
}
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 = 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);
}
}
}
}
}
}
private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
String name = NativeLibrary.GetTitle(filePath);
// If the game's title field is empty, use the filename.
if (name.isEmpty()) {
name = filePath.substring(filePath.lastIndexOf("/") + 1);
}
String gameId = NativeLibrary.GetGameId(filePath);
// If the game's ID field is empty, use the filename without extension.
if (gameId.isEmpty()) {
gameId = filePath.substring(filePath.lastIndexOf("/") + 1,
filePath.lastIndexOf("."));
}
ContentValues game = Game.asContentValues(name,
NativeLibrary.GetDescription(filePath).replace("\n", " "),
NativeLibrary.GetRegions(filePath),
filePath,
gameId,
NativeLibrary.GetCompany(filePath));
// Try to update an existing game first.
int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update.
game,
// The values to fill the row with.
KEY_GAME_ID + " = ?",
// The WHERE clause used to find the right row.
new String[]{game.getAsString(
KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this,
// which is provided as an array because there
// could potentially be more than one argument.
// If update fails, insert a new game instead.
if (rowsMatched == 0) {
Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE));
database.insert(TABLE_NAME_GAMES, null, game);
} else {
Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE));
}
}
public Observable<Cursor> getGames() {
return Observable.create(subscriber ->
{
Log.info("[GameDatabase] Reading games list...");
SQLiteDatabase database = getReadableDatabase();
Cursor resultCursor = database.query(
TABLE_NAME_GAMES,
null,
null,
null,
null,
null,
KEY_GAME_TITLE + " ASC"
);
// Pass the result cursor to the consumer.
subscriber.onNext(resultCursor);
// Tell the consumer we're done; it will unsubscribe implicitly.
subscriber.onCompleted();
});
}
private void execSqlAndLog(SQLiteDatabase database, String sql) {
Log.verbose("[GameDatabase] Executing SQL: " + sql);
database.execSQL(sql);
}
}

View File

@ -0,0 +1,138 @@
package org.citra.citra_emu.model;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.citra.citra_emu.BuildConfig;
import org.citra.citra_emu.utils.Log;
/**
* Provides an interface allowing Activities to interact with the SQLite database.
* CRUD methods in this class can be called by Activities using getContentResolver().
*/
public final class GameProvider extends ContentProvider {
public static final String REFRESH_LIBRARY = "refresh";
public static final String RESET_LIBRARY = "reset";
public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
public static final Uri URI_FOLDER =
Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/");
public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/");
public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder";
public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game";
private GameDatabase mDbHelper;
@Override
public boolean onCreate() {
Log.info("[GameProvider] Creating Content Provider...");
mDbHelper = new GameDatabase(getContext());
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.info("[GameProvider] Querying URI: " + uri);
SQLiteDatabase db = mDbHelper.getReadableDatabase();
String table = uri.getLastPathSegment();
if (table == null) {
Log.error("[GameProvider] Badly formatted URI: " + uri);
return null;
}
Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
Log.verbose("[GameProvider] Getting MIME type for URI: " + uri);
String lastSegment = uri.getLastPathSegment();
if (lastSegment == null) {
Log.error("[GameProvider] Badly formatted URI: " + uri);
return null;
}
if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
return MIME_TYPE_FOLDER;
} else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) {
return MIME_TYPE_GAME;
}
Log.error("[GameProvider] Unknown MIME type for URI: " + uri);
return null;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
Log.info("[GameProvider] Inserting row at URI: " + uri);
SQLiteDatabase database = mDbHelper.getWritableDatabase();
String table = uri.getLastPathSegment();
if (table != null) {
if (table.equals(RESET_LIBRARY)) {
mDbHelper.resetDatabase(database);
return uri;
}
if (table.equals(REFRESH_LIBRARY)) {
Log.info(
"[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents...");
mDbHelper.scanLibrary(database);
return uri;
}
long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
// If insertion was successful...
if (id > 0) {
// If we just added a folder, add its contents to the game list.
if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) {
mDbHelper.scanLibrary(database);
}
// Notify the UI that its contents should be refreshed.
getContext().getContentResolver().notifyChange(uri, null);
uri = Uri.withAppendedPath(uri, Long.toString(id));
} else {
Log.error("[GameProvider] Row already exists: " + uri + " id: " + id);
}
} else {
Log.error("[GameProvider] Badly formatted URI: " + uri);
}
database.close();
return uri;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
Log.error("[GameProvider] Delete operations unsupported. URI: " + uri);
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.error("[GameProvider] Update operations unsupported. URI: " + uri);
return 0;
}
}

View File

@ -0,0 +1,808 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu.overlay;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnTouchListener;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.NativeLibrary.ButtonState;
import org.citra.citra_emu.NativeLibrary.ButtonType;
import org.citra.citra_emu.R;
import org.citra.citra_emu.utils.EmulationMenuSettings;
import java.util.HashSet;
import java.util.Set;
/**
* Draws the interactive input overlay on top of the
* {@link SurfaceView} that is rendering emulation.
*/
public final class InputOverlay extends SurfaceView implements OnTouchListener {
private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>();
private boolean mIsInEditMode = false;
private InputOverlayDrawableButton mButtonBeingConfigured;
private InputOverlayDrawableDpad mDpadBeingConfigured;
private InputOverlayDrawableJoystick mJoystickBeingConfigured;
private SharedPreferences mPreferences;
/**
* Constructor
*
* @param context The current {@link Context}.
* @param attrs {@link AttributeSet} for parsing XML attributes.
*/
public InputOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
if (!mPreferences.getBoolean("OverlayInit", false)) {
defaultOverlay();
}
// Load the controls.
refreshControls();
// Set the on touch listener.
setOnTouchListener(this);
// Force draw
setWillNotDraw(false);
// Request focus for the overlay so it has priority on presses.
requestFocus();
}
/**
* Resizes a {@link Bitmap} by a given scale factor
*
* @param context The current {@link Context}
* @param bitmap The {@link Bitmap} to scale.
* @param scale The scale factor for the bitmap.
* @return The scaled {@link Bitmap}
*/
public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) {
// Determine the button size based on the smaller screen dimension.
// This makes sure the buttons are the same size in both portrait and landscape.
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int minDimension = Math.min(dm.widthPixels, dm.heightPixels);
return Bitmap.createScaledBitmap(bitmap,
(int) (minDimension * scale),
(int) (minDimension * scale),
true);
}
/**
* Initializes an InputOverlayDrawableButton, given by resId, with all of the
* parameters set for it to be properly shown on the InputOverlay.
* <p>
* This works due to the way the X and Y coordinates are stored within
* the {@link SharedPreferences}.
* <p>
* In the input overlay configuration menu,
* once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
* the X and Y coordinates of the button at the END of its touch event
* (when you remove your finger/stylus from the touchscreen) are then stored
* within a SharedPreferences instance so that those values can be retrieved here.
* <p>
* This has a few benefits over the conventional way of storing the values
* (ie. within the Citra ini file).
* <ul>
* <li>No native calls</li>
* <li>Keeps Android-only values inside the Android environment</li>
* </ul>
* <p>
* Technically no modifications should need to be performed on the returned
* InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
* for Android to call the onDraw method.
*
* @param context The current {@link Context}.
* @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State).
* @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State).
* @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
* @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
*/
private static InputOverlayDrawableButton initializeOverlayButton(Context context,
int defaultResId, int pressedResId, int buttonId, String orientation) {
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on button ID and user preference
float scale;
switch (buttonId) {
case ButtonType.BUTTON_HOME:
case ButtonType.BUTTON_START:
case ButtonType.BUTTON_SELECT:
scale = 0.08f;
break;
case ButtonType.TRIGGER_L:
case ButtonType.TRIGGER_R:
case ButtonType.BUTTON_ZL:
case ButtonType.BUTTON_ZR:
scale = 0.18f;
break;
default:
scale = 0.11f;
break;
}
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableButton.
final Bitmap defaultStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
final Bitmap pressedStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale);
final InputOverlayDrawableButton overlayDrawable =
new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId);
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
String xKey;
String yKey;
xKey = buttonId + orientation + "-X";
yKey = buttonId + orientation + "-Y";
int drawableX = (int) sPrefs.getFloat(xKey, 0f);
int drawableY = (int) sPrefs.getFloat(yKey, 0f);
int width = overlayDrawable.getWidth();
int height = overlayDrawable.getHeight();
// Now set the bounds for the InputOverlayDrawableButton.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
/**
* Initializes an {@link InputOverlayDrawableDpad}
*
* @param context The current {@link Context}.
* @param defaultResId The {@link Bitmap} resource ID of the default sate.
* @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction.
* @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions.
* @param buttonUp Identifier for the up button.
* @param buttonDown Identifier for the down button.
* @param buttonLeft Identifier for the left button.
* @param buttonRight Identifier for the right button.
* @return the initialized {@link InputOverlayDrawableDpad}
*/
private static InputOverlayDrawableDpad initializeOverlayDpad(Context context,
int defaultResId,
int pressedOneDirectionResId,
int pressedTwoDirectionsResId,
int buttonUp,
int buttonDown,
int buttonLeft,
int buttonRight,
String orientation) {
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on button ID and user preference
float scale = 0.22f;
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableDpad.
final Bitmap defaultStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
final Bitmap pressedOneDirectionStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId),
scale);
final Bitmap pressedTwoDirectionsStateBitmap =
resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId),
scale);
final InputOverlayDrawableDpad overlayDrawable =
new InputOverlayDrawableDpad(res, defaultStateBitmap,
pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap,
buttonUp, buttonDown, buttonLeft, buttonRight);
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f);
int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f);
int width = overlayDrawable.getWidth();
int height = overlayDrawable.getHeight();
// Now set the bounds for the InputOverlayDrawableDpad.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
/**
* Initializes an {@link InputOverlayDrawableJoystick}
*
* @param context The current {@link Context}
* @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
* @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
* @param pressedResInner Resource ID for the pressed inner image of the joystick.
* @param joystick Identifier for which joystick this is.
* @return the initialized {@link InputOverlayDrawableJoystick}.
*/
private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context,
int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) {
// Resources handle for fetching the initial Drawable resource.
final Resources res = context.getResources();
// SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Decide scale based on user preference
float scale = 0.275f;
scale *= (sPrefs.getInt("controlScale", 50) + 50);
scale /= 100;
// Initialize the InputOverlayDrawableJoystick.
final Bitmap bitmapOuter =
resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale);
final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner);
final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner);
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f);
int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f);
// Decide inner scale based on joystick ID
float outerScale = 1.f;
if (joystick == ButtonType.STICK_C) {
outerScale = 2.f;
}
// Now set the bounds for the InputOverlayDrawableJoystick.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
int outerSize = bitmapOuter.getWidth();
Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale));
Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale));
// Send the drawableId to the joystick so it can be referenced when saving control position.
final InputOverlayDrawableJoystick overlayDrawable
= new InputOverlayDrawableJoystick(res, bitmapOuter,
bitmapInnerDefault, bitmapInnerPressed,
outerRect, innerRect, joystick);
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY);
return overlayDrawable;
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
for (InputOverlayDrawableButton button : overlayButtons) {
button.draw(canvas);
}
for (InputOverlayDrawableDpad dpad : overlayDpads) {
dpad.draw(canvas);
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
joystick.draw(canvas);
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isInEditMode()) {
return onTouchWhileEditing(event);
}
int pointerIndex = event.getActionIndex();
if (mPreferences.getBoolean("isTouchEnabled", true)) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true);
break;
case MotionEvent.ACTION_MOVE:
NativeLibrary.onTouchMoved(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// We dont really care where the touch has been released. We only care whether it has been
// released or not.
NativeLibrary.onTouchEvent(0, 0, false);
break;
}
}
for (InputOverlayDrawableButton button : overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If a pointer enters the bounds of a button, press that button.
if (button.getBounds()
.contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
button.setPressedState(true);
button.setTrackId(event.getPointerId(pointerIndex));
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
ButtonState.PRESSED);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// If a pointer ends, release the button it was pressing.
if (button.getTrackId() == event.getPointerId(pointerIndex)) {
button.setPressedState(false);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(),
ButtonState.RELEASED);
}
break;
}
}
for (InputOverlayDrawableDpad dpad : overlayDpads) {
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If a pointer enters the bounds of a button, press that button.
if (dpad.getBounds()
.contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
boolean up = false;
boolean down = false;
boolean left = false;
boolean right = false;
if (dpad.getBounds().top + (dpad.getHeight() / 3) > (int) event.getY(pointerIndex)) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0),
ButtonState.PRESSED);
up = true;
}
if (dpad.getBounds().bottom - (dpad.getHeight() / 3) < (int) event.getY(pointerIndex)) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1),
ButtonState.PRESSED);
down = true;
}
if (dpad.getBounds().left + (dpad.getWidth() / 3) > (int) event.getX(pointerIndex)) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2),
ButtonState.PRESSED);
left = true;
}
if (dpad.getBounds().right - (dpad.getWidth() / 3) < (int) event.getX(pointerIndex)) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3),
ButtonState.PRESSED);
right = true;
}
setDpadState(dpad, up, down, left, right);
dpad.setTrackId(event.getPointerId(pointerIndex));
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// If a pointer ends, release the buttons.
if (dpad.getTrackId() == event.getPointerId(pointerIndex)) {
for (int i = 0; i < 4; i++) {
dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i),
ButtonState.RELEASED);
}
}
break;
}
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
joystick.TrackEvent(event);
int axisID = joystick.getId();
float[] axises = joystick.getAxisValues();
NativeLibrary
.onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]);
}
invalidate();
return true;
}
public boolean onTouchWhileEditing(MotionEvent event) {
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
String orientation =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
"-Portrait" : "";
// Maybe combine Button and Joystick as subclasses of the same parent?
// Or maybe create an interface like IMoveableHUDControl?
for (InputOverlayDrawableButton button : overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If no button is being moved now, remember the currently touched button to move.
if (mButtonBeingConfigured == null &&
button.getBounds().contains(fingerPositionX, fingerPositionY)) {
mButtonBeingConfigured = button;
mButtonBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mButtonBeingConfigured != null) {
mButtonBeingConfigured.onConfigureTouch(event);
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mButtonBeingConfigured == button) {
// Persist button position by saving new place.
saveControlPosition(mButtonBeingConfigured.getId(),
mButtonBeingConfigured.getBounds().left,
mButtonBeingConfigured.getBounds().top, orientation);
mButtonBeingConfigured = null;
}
break;
}
}
for (InputOverlayDrawableDpad dpad : overlayDpads) {
// Determine the button state to apply based on the MotionEvent action flag.
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// If no button is being moved now, remember the currently touched button to move.
if (mButtonBeingConfigured == null &&
dpad.getBounds().contains(fingerPositionX, fingerPositionY)) {
mDpadBeingConfigured = dpad;
mDpadBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mDpadBeingConfigured != null) {
mDpadBeingConfigured.onConfigureTouch(event);
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mDpadBeingConfigured == dpad) {
// Persist button position by saving new place.
saveControlPosition(mDpadBeingConfigured.getId(0),
mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top,
orientation);
mDpadBeingConfigured = null;
}
break;
}
}
for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
if (mJoystickBeingConfigured == null &&
joystick.getBounds().contains(fingerPositionX, fingerPositionY)) {
mJoystickBeingConfigured = joystick;
mJoystickBeingConfigured.onConfigureTouch(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mJoystickBeingConfigured != null) {
mJoystickBeingConfigured.onConfigureTouch(event);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mJoystickBeingConfigured != null) {
saveControlPosition(mJoystickBeingConfigured.getId(),
mJoystickBeingConfigured.getBounds().left,
mJoystickBeingConfigured.getBounds().top, orientation);
mJoystickBeingConfigured = null;
}
break;
}
}
return true;
}
private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left,
boolean right) {
if (up) {
if (left)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT);
else if (right)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT);
else
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP);
} else if (down) {
if (left)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT);
else if (right)
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT);
else
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN);
} else if (left) {
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT);
} else if (right) {
dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT);
}
}
private void addOverlayControls(String orientation) {
if (mPreferences.getBoolean("buttonToggle0", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a,
R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation));
}
if (mPreferences.getBoolean("buttonToggle1", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b,
R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation));
}
if (mPreferences.getBoolean("buttonToggle2", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x,
R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation));
}
if (mPreferences.getBoolean("buttonToggle3", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y,
R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation));
}
if (mPreferences.getBoolean("buttonToggle4", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l,
R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation));
}
if (mPreferences.getBoolean("buttonToggle5", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r,
R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation));
}
if (mPreferences.getBoolean("buttonToggle6", false)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl,
R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation));
}
if (mPreferences.getBoolean("buttonToggle7", false)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr,
R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation));
}
if (mPreferences.getBoolean("buttonToggle8", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start,
R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation));
}
if (mPreferences.getBoolean("buttonToggle9", true)) {
overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select,
R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation));
}
if (mPreferences.getBoolean("buttonToggle10", true)) {
overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad,
R.drawable.dpad_pressed_one_direction,
R.drawable.dpad_pressed_two_directions,
ButtonType.DPAD_UP, ButtonType.DPAD_DOWN,
ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation));
}
if (mPreferences.getBoolean("buttonToggle11", true)) {
overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range,
R.drawable.stick_main, R.drawable.stick_main_pressed,
ButtonType.STICK_LEFT, orientation));
}
if (mPreferences.getBoolean("buttonToggle12", false)) {
overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range,
R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation));
}
}
public void refreshControls() {
// Remove all the overlay buttons from the HashSet.
overlayButtons.clear();
overlayDpads.clear();
overlayJoysticks.clear();
String orientation =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
"-Portrait" : "";
// Add all the enabled overlay items back to the HashSet.
if (EmulationMenuSettings.getShowOverlay()) {
addOverlayControls(orientation);
}
invalidate();
}
private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) {
final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor sPrefsEditor = sPrefs.edit();
sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x);
sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y);
sPrefsEditor.apply();
}
public void setIsInEditMode(boolean isInEditMode) {
mIsInEditMode = isInEditMode;
}
private void defaultOverlay() {
if (!mPreferences.getBoolean("OverlayInit", false)) {
// It's possible that a user has created their overlay before this was added
// Only change the overlay if the 'A' button is not in the upper corner.
if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) {
defaultOverlayLandscape();
}
if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) {
defaultOverlayPortrait();
}
}
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
sPrefsEditor.putBoolean("OverlayInit", true);
sPrefsEditor.apply();
}
public void resetButtonPlacement() {
boolean isLandscape =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
if (isLandscape) {
defaultOverlayLandscape();
} else {
defaultOverlayPortrait();
}
refreshControls();
}
private void defaultOverlayLandscape() {
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
// Get screen size
Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float maxX = outMetrics.heightPixels;
float maxY = outMetrics.widthPixels;
// Height and width changes depending on orientation. Use the larger value for height.
if (maxY > maxX) {
float tmp = maxX;
maxX = maxY;
maxY = tmp;
}
Resources res = getResources();
// Each value is a percent from max X/Y stored as an int. Have to bring that value down
// to a decimal before multiplying by MAX X/Y.
sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY));
// We want to commit right away, otherwise the overlay could load before this is saved.
sPrefsEditor.commit();
}
private void defaultOverlayPortrait() {
SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
// Get screen size
Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float maxX = outMetrics.heightPixels;
float maxY = outMetrics.widthPixels;
// Height and width changes depending on orientation. Use the larger value for height.
if (maxY < maxX) {
float tmp = maxX;
maxX = maxY;
maxY = tmp;
}
Resources res = getResources();
String portrait = "-Portrait";
// Each value is a percent from max X/Y stored as an int. Have to bring that value down
// to a decimal before multiplying by MAX X/Y.
sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX));
sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY));
// We want to commit right away, otherwise the overlay could load before this is saved.
sPrefsEditor.commit();
}
public boolean isInEditMode() {
return mIsInEditMode;
}
}

View File

@ -0,0 +1,122 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableButton {
// The ID identifying what type of button this Drawable represents.
private int mButtonType;
private int mTrackId;
private int mPreviousTouchX, mPreviousTouchY;
private int mControlPositionX, mControlPositionY;
private int mWidth;
private int mHeight;
private BitmapDrawable mDefaultStateBitmap;
private BitmapDrawable mPressedStateBitmap;
private boolean mPressedState = false;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable.
* @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable.
* @param buttonType Identifier for this type of button.
*/
public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap,
Bitmap pressedStateBitmap, int buttonType) {
mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap);
mButtonType = buttonType;
mWidth = mDefaultStateBitmap.getIntrinsicWidth();
mHeight = mDefaultStateBitmap.getIntrinsicHeight();
}
/**
* Gets this InputOverlayDrawableButton's button ID.
*
* @return this InputOverlayDrawableButton's button ID.
*/
public int getId() {
return mButtonType;
}
public int getTrackId() {
return mTrackId;
}
public void setTrackId(int trackId) {
mTrackId = trackId;
}
public boolean onConfigureTouch(MotionEvent event) {
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
mControlPositionX += fingerPositionX - mPreviousTouchX;
mControlPositionY += fingerPositionY - mPreviousTouchY;
setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
getHeight() + mControlPositionY);
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public void setPosition(int x, int y) {
mControlPositionX = x;
mControlPositionY = y;
}
public void draw(Canvas canvas) {
getCurrentStateBitmapDrawable().draw(canvas);
}
private BitmapDrawable getCurrentStateBitmapDrawable() {
return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap;
}
public void setBounds(int left, int top, int right, int bottom) {
mDefaultStateBitmap.setBounds(left, top, right, bottom);
mPressedStateBitmap.setBounds(left, top, right, bottom);
}
public Rect getBounds() {
return mDefaultStateBitmap.getBounds();
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
public void setPressedState(boolean isPressed) {
mPressedState = isPressed;
}
}

View File

@ -0,0 +1,190 @@
/**
* Copyright 2016 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableDpad {
public static final int STATE_DEFAULT = 0;
public static final int STATE_PRESSED_UP = 1;
public static final int STATE_PRESSED_DOWN = 2;
public static final int STATE_PRESSED_LEFT = 3;
public static final int STATE_PRESSED_RIGHT = 4;
public static final int STATE_PRESSED_UP_LEFT = 5;
public static final int STATE_PRESSED_UP_RIGHT = 6;
public static final int STATE_PRESSED_DOWN_LEFT = 7;
public static final int STATE_PRESSED_DOWN_RIGHT = 8;
// The ID identifying what type of button this Drawable represents.
private int[] mButtonType = new int[4];
private int mTrackId;
private int mPreviousTouchX, mPreviousTouchY;
private int mControlPositionX, mControlPositionY;
private int mWidth;
private int mHeight;
private BitmapDrawable mDefaultStateBitmap;
private BitmapDrawable mPressedOneDirectionStateBitmap;
private BitmapDrawable mPressedTwoDirectionsStateBitmap;
private int mPressState = STATE_DEFAULT;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param defaultStateBitmap {@link Bitmap} of the default state.
* @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction.
* @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction.
* @param buttonUp Identifier for the up button.
* @param buttonDown Identifier for the down button.
* @param buttonLeft Identifier for the left button.
* @param buttonRight Identifier for the right button.
*/
public InputOverlayDrawableDpad(Resources res,
Bitmap defaultStateBitmap,
Bitmap pressedOneDirectionStateBitmap,
Bitmap pressedTwoDirectionsStateBitmap,
int buttonUp, int buttonDown,
int buttonLeft, int buttonRight) {
mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap);
mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap);
mWidth = mDefaultStateBitmap.getIntrinsicWidth();
mHeight = mDefaultStateBitmap.getIntrinsicHeight();
mButtonType[0] = buttonUp;
mButtonType[1] = buttonDown;
mButtonType[2] = buttonLeft;
mButtonType[3] = buttonRight;
}
public void draw(Canvas canvas) {
int px = mControlPositionX + (getWidth() / 2);
int py = mControlPositionY + (getHeight() / 2);
switch (mPressState) {
case STATE_DEFAULT:
mDefaultStateBitmap.draw(canvas);
break;
case STATE_PRESSED_UP:
mPressedOneDirectionStateBitmap.draw(canvas);
break;
case STATE_PRESSED_RIGHT:
canvas.save();
canvas.rotate(90, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN:
canvas.save();
canvas.rotate(180, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_LEFT:
canvas.save();
canvas.rotate(270, px, py);
mPressedOneDirectionStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_UP_LEFT:
mPressedTwoDirectionsStateBitmap.draw(canvas);
break;
case STATE_PRESSED_UP_RIGHT:
canvas.save();
canvas.rotate(90, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN_RIGHT:
canvas.save();
canvas.rotate(180, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
case STATE_PRESSED_DOWN_LEFT:
canvas.save();
canvas.rotate(270, px, py);
mPressedTwoDirectionsStateBitmap.draw(canvas);
canvas.restore();
break;
}
}
/**
* Gets one of the InputOverlayDrawableDpad's button IDs.
*
* @return the requested InputOverlayDrawableDpad's button ID.
*/
public int getId(int direction) {
return mButtonType[direction];
}
public int getTrackId() {
return mTrackId;
}
public void setTrackId(int trackId) {
mTrackId = trackId;
}
public boolean onConfigureTouch(MotionEvent event) {
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
mControlPositionX += fingerPositionX - mPreviousTouchX;
mControlPositionY += fingerPositionY - mPreviousTouchY;
setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
getHeight() + mControlPositionY);
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public void setPosition(int x, int y) {
mControlPositionX = x;
mControlPositionY = y;
}
public void setBounds(int left, int top, int right, int bottom) {
mDefaultStateBitmap.setBounds(left, top, right, bottom);
mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom);
mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom);
}
public Rect getBounds() {
return mDefaultStateBitmap.getBounds();
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
public void setState(int pressState) {
mPressState = pressState;
}
}

View File

@ -0,0 +1,254 @@
/**
* Copyright 2013 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu.overlay;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
import org.citra.citra_emu.NativeLibrary.ButtonType;
/**
* Custom {@link BitmapDrawable} that is capable
* of storing it's own ID.
*/
public final class InputOverlayDrawableJoystick {
private final int[] axisIDs = {0, 0, 0, 0};
private final float[] axises = {0f, 0f};
private int trackId = -1;
private int mJoystickType;
private int mControlPositionX, mControlPositionY;
private int mPreviousTouchX, mPreviousTouchY;
private int mWidth;
private int mHeight;
private Rect mVirtBounds;
private Rect mOrigBounds;
private BitmapDrawable mOuterBitmap;
private BitmapDrawable mDefaultStateInnerBitmap;
private BitmapDrawable mPressedStateInnerBitmap;
private BitmapDrawable mBoundsBoxBitmap;
private boolean mPressedState = false;
/**
* Constructor
*
* @param res {@link Resources} instance.
* @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick.
* @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick.
* @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick.
* @param rectOuter {@link Rect} which represents the outer joystick bounds.
* @param rectInner {@link Rect} which represents the inner joystick bounds.
* @param joystick Identifier for which joystick this is.
*/
public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter,
Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed,
Rect rectOuter, Rect rectInner, int joystick) {
axisIDs[0] = joystick + 1; // Up
axisIDs[1] = joystick + 2; // Down
axisIDs[2] = joystick + 3; // Left
axisIDs[3] = joystick + 4; // Right
mJoystickType = joystick;
mOuterBitmap = new BitmapDrawable(res, bitmapOuter);
mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault);
mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed);
mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter);
mWidth = bitmapOuter.getWidth();
mHeight = bitmapOuter.getHeight();
setBounds(rectOuter);
mDefaultStateInnerBitmap.setBounds(rectInner);
mPressedStateInnerBitmap.setBounds(rectInner);
mVirtBounds = getBounds();
mOrigBounds = mOuterBitmap.copyBounds();
mBoundsBoxBitmap.setAlpha(0);
mBoundsBoxBitmap.setBounds(getVirtBounds());
SetInnerBounds();
}
/**
* Gets this InputOverlayDrawableJoystick's button ID.
*
* @return this InputOverlayDrawableJoystick's button ID.
*/
public int getId() {
return mJoystickType;
}
public void draw(Canvas canvas) {
mOuterBitmap.draw(canvas);
getCurrentStateBitmapDrawable().draw(canvas);
mBoundsBoxBitmap.draw(canvas);
}
public void TrackEvent(MotionEvent event) {
int pointerIndex = event.getActionIndex();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) {
mPressedState = true;
mOuterBitmap.setAlpha(0);
mBoundsBoxBitmap.setAlpha(255);
getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(),
(int) event.getY(pointerIndex) - getVirtBounds().centerY());
mBoundsBoxBitmap.setBounds(getVirtBounds());
trackId = event.getPointerId(pointerIndex);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (trackId == event.getPointerId(pointerIndex)) {
mPressedState = false;
axises[0] = axises[1] = 0.0f;
mOuterBitmap.setAlpha(255);
mBoundsBoxBitmap.setAlpha(0);
setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
mOrigBounds.bottom));
setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
mOrigBounds.bottom));
SetInnerBounds();
trackId = -1;
}
break;
}
if (trackId == -1)
return;
for (int i = 0; i < event.getPointerCount(); i++) {
if (trackId == event.getPointerId(i)) {
float touchX = event.getX(i);
float touchY = event.getY(i);
float maxY = getVirtBounds().bottom;
float maxX = getVirtBounds().right;
touchX -= getVirtBounds().centerX();
maxX -= getVirtBounds().centerX();
touchY -= getVirtBounds().centerY();
maxY -= getVirtBounds().centerY();
final float AxisX = touchX / maxX;
final float AxisY = touchY / maxY;
axises[0] = AxisX;
axises[1] = AxisY;
SetInnerBounds();
}
}
}
public boolean onConfigureTouch(MotionEvent event) {
int pointerIndex = event.getActionIndex();
int fingerPositionX = (int) event.getX(pointerIndex);
int fingerPositionY = (int) event.getY(pointerIndex);
int scale = 1;
if (mJoystickType == ButtonType.STICK_C) {
// C-stick is scaled down to be half the size of the circle pad
scale = 2;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = fingerPositionX - mPreviousTouchX;
int deltaY = fingerPositionY - mPreviousTouchY;
mControlPositionX += deltaX;
mControlPositionY += deltaY;
setBounds(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
setVirtBounds(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
SetInnerBounds();
setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY,
mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)));
mPreviousTouchX = fingerPositionX;
mPreviousTouchY = fingerPositionY;
break;
}
return true;
}
public float[] getAxisValues() {
return axises;
}
public int[] getAxisIDs() {
return axisIDs;
}
private void SetInnerBounds() {
int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2));
int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2));
if (mJoystickType == ButtonType.STICK_LEFT) {
X += 1;
Y += 1;
}
if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2))
X = getVirtBounds().centerX() + (getVirtBounds().width() / 2);
if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2))
X = getVirtBounds().centerX() - (getVirtBounds().width() / 2);
if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2))
Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2);
if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2))
Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2);
int width = mPressedStateInnerBitmap.getBounds().width() / 2;
int height = mPressedStateInnerBitmap.getBounds().height() / 2;
mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height);
mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds());
}
public void setPosition(int x, int y) {
mControlPositionX = x;
mControlPositionY = y;
}
private BitmapDrawable getCurrentStateBitmapDrawable() {
return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap;
}
public Rect getBounds() {
return mOuterBitmap.getBounds();
}
public void setBounds(Rect bounds) {
mOuterBitmap.setBounds(bounds);
}
private void setOrigBounds(Rect bounds) {
mOrigBounds = bounds;
}
private Rect getVirtBounds() {
return mVirtBounds;
}
private void setVirtBounds(Rect bounds) {
mVirtBounds = bounds;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
}

View File

@ -0,0 +1,130 @@
package org.citra.citra_emu.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* Implementation from:
* https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36
*/
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;
private boolean mShowFirstDivider = false;
private boolean mShowLastDivider = false;
public DividerItemDecoration(Context context, AttributeSet attrs) {
final TypedArray a = context
.obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider});
mDivider = a.getDrawable(0);
a.recycle();
}
public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider,
boolean showLastDivider) {
this(context, attrs);
mShowFirstDivider = showFirstDivider;
mShowLastDivider = showLastDivider;
}
public DividerItemDecoration(Drawable divider) {
mDivider = divider;
}
public DividerItemDecoration(Drawable divider, boolean showFirstDivider,
boolean showLastDivider) {
this(divider);
mShowFirstDivider = showFirstDivider;
mShowLastDivider = showLastDivider;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (mDivider == null) {
return;
}
if (parent.getChildAdapterPosition(view) < 1) {
return;
}
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
outRect.top = mDivider.getIntrinsicHeight();
} else {
outRect.left = mDivider.getIntrinsicWidth();
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
if (mDivider == null) {
super.onDrawOver(c, parent, state);
return;
}
// Initialization needed to avoid compiler warning
int left = 0, right = 0, top = 0, bottom = 0, size;
int orientation = getOrientation(parent);
int childCount = parent.getChildCount();
if (orientation == LinearLayoutManager.VERTICAL) {
size = mDivider.getIntrinsicHeight();
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
} else { //horizontal
size = mDivider.getIntrinsicWidth();
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
}
for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if (orientation == LinearLayoutManager.VERTICAL) {
top = child.getTop() - params.topMargin;
bottom = top + size;
} else { //horizontal
left = child.getLeft() - params.leftMargin;
right = left + size;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
// show last divider
if (mShowLastDivider && childCount > 0) {
View child = parent.getChildAt(childCount - 1);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if (orientation == LinearLayoutManager.VERTICAL) {
top = child.getBottom() + params.bottomMargin;
bottom = top + size;
} else { // horizontal
left = child.getRight() + params.rightMargin;
right = left + size;
}
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
private int getOrientation(RecyclerView parent) {
if (parent.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
return layoutManager.getOrientation();
} else {
throw new IllegalStateException(
"DividerItemDecoration can only be used with a LinearLayoutManager.");
}
}
}

View File

@ -0,0 +1,241 @@
package org.citra.citra_emu.ui.main;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
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.DirectoryInitialization;
import org.citra.citra_emu.utils.FileBrowserHelper;
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;
/**
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
* individually display a grid of available games for each Fragment, in a tabbed layout.
*/
public final class MainActivity extends AppCompatActivity implements MainView {
private Toolbar mToolbar;
private int mFrameLayoutId;
private PlatformGamesFragment mPlatformGamesFragment;
private MainPresenter mPresenter = new MainPresenter(this);
// Singleton to manage user billing state
private static BillingManager mBillingManager;
private static MenuItem mPremiumButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeUtil.applyTheme();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
setSupportActionBar(mToolbar);
mFrameLayoutId = R.id.games_platform_frame;
mPresenter.onCreate();
if (savedInstanceState == null) {
StartupHandler.HandleInit(this);
if (PermissionsHandler.hasWriteAccess(this)) {
mPlatformGamesFragment = new PlatformGamesFragment();
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
.commit();
}
} else {
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
}
PicassoUtils.init();
// Setup billing manager, so we can globally query for Premium status
mBillingManager = new BillingManager(this);
// Dismiss previous notifications (should not happen unless a crash occurred)
EmulationActivity.tryDismissRunningNotification(this);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (PermissionsHandler.hasWriteAccess(this)) {
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
}
}
@Override
protected void onResume() {
super.onResume();
mPresenter.addDirIfNeeded(new AddDirectoryHelper(this));
}
// TODO: Replace with a ButterKnife injection.
private void findViews() {
mToolbar = findViewById(R.id.toolbar_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_game_grid, menu);
mPremiumButton = menu.findItem(R.id.button_premium);
if (mBillingManager.isPremiumCached()) {
// User had premium in a previous session, hide upsell option
setPremiumButtonVisible(false);
}
return true;
}
static public void setPremiumButtonVisible(boolean isVisible) {
if (mPremiumButton != null) {
mPremiumButton.setVisible(isVisible);
}
}
/**
* MainView
*/
@Override
public void setVersionString(String version) {
mToolbar.setSubtitle(version);
}
@Override
public void refresh() {
getContentResolver().insert(GameProvider.URI_REFRESH, null);
refreshFragment();
}
@Override
public void launchSettingsActivity(String menuTag) {
if (PermissionsHandler.hasWriteAccess(this)) {
SettingsActivity.launch(this, menuTag, "");
} else {
PermissionsHandler.checkWritePermission(this);
}
}
@Override
public void launchFileListActivity() {
if (PermissionsHandler.hasWriteAccess(this)) {
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"));
} 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;
}
}
@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();
}
} else {
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
/**
* Called by the framework whenever any actionbar/toolbar icon is clicked.
*
* @param item The icon that was clicked on.
* @return True if the event was handled, false to bubble it up to the OS.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mPresenter.handleOptionSelection(item.getItemId());
}
private void refreshFragment() {
if (mPlatformGamesFragment != null) {
mPlatformGamesFragment.refresh();
}
}
@Override
protected void onDestroy() {
EmulationActivity.tryDismissRunningNotification(this);
super.onDestroy();
}
/**
* @return true if Premium subscription is currently active
*/
public static boolean isPremiumActive() {
return mBillingManager.isPremiumActive();
}
/**
* Invokes the billing flow for Premium
*
* @param callback Optional callback, called once, on completion of billing
*/
public static void invokePremiumBilling(Runnable callback) {
mBillingManager.invokePremiumBilling(callback);
}
}

View File

@ -0,0 +1,77 @@
package org.citra.citra_emu.ui.main;
import android.os.SystemClock;
import org.citra.citra_emu.BuildConfig;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.model.GameDatabase;
import org.citra.citra_emu.utils.AddDirectoryHelper;
public final class MainPresenter {
public static final int REQUEST_ADD_DIRECTORY = 1;
private final MainView mView;
private String mDirToAdd;
private long mLastClickTime = 0;
public MainPresenter(MainView view) {
mView = view;
}
public void onCreate() {
String versionName = BuildConfig.VERSION_NAME;
mView.setVersionString(versionName);
refeshGameList();
}
public void launchFileListActivity() {
if (mView != null) {
mView.launchFileListActivity();
}
}
public boolean handleOptionSelection(int itemId) {
// Double-click prevention, using threshold of 500 ms
if (SystemClock.elapsedRealtime() - mLastClickTime < 500) {
return false;
}
mLastClickTime = SystemClock.elapsedRealtime();
switch (itemId) {
case R.id.menu_settings_core:
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
return true;
case R.id.button_add_directory:
launchFileListActivity();
return true;
case R.id.button_premium:
mView.launchSettingsActivity(Settings.SECTION_PREMIUM);
return true;
}
return false;
}
public void addDirIfNeeded(AddDirectoryHelper helper) {
if (mDirToAdd != null) {
helper.addDirectory(mDirToAdd, mView::refresh);
mDirToAdd = null;
}
}
public void onDirectorySelected(String dir) {
mDirToAdd = dir;
}
private void refeshGameList() {
GameDatabase databaseHelper = CitraApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mView.refresh();
}
}

View File

@ -0,0 +1,25 @@
package org.citra.citra_emu.ui.main;
/**
* Abstraction for the screen that shows on application launch.
* Implementations will differ primarily to target touch-screen
* or non-touch screen devices.
*/
public interface MainView {
/**
* Pass the view the native library's version string. Displaying
* it is optional.
*
* @param version A string pulled from native code.
*/
void setVersionString(String version);
/**
* Tell the view to refresh its contents.
*/
void refresh();
void launchSettingsActivity(String menuTag);
void launchFileListActivity();
}

View File

@ -0,0 +1,86 @@
package org.citra.citra_emu.ui.platform;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.adapters.GameAdapter;
import org.citra.citra_emu.model.GameDatabase;
public final class PlatformGamesFragment extends Fragment implements PlatformGamesView {
private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this);
private GameAdapter mAdapter;
private RecyclerView mRecyclerView;
private TextView mTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_grid, container, false);
findViews(rootView);
mPresenter.onCreateView();
return rootView;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
int columns = getResources().getInteger(R.integer.game_grid_columns);
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns);
mAdapter = new GameAdapter();
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1));
// Add swipe down to refresh gesture
final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games);
pullToRefresh.setOnRefreshListener(() -> {
GameDatabase databaseHelper = CitraApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
refresh();
pullToRefresh.setRefreshing(false);
});
}
@Override
public void refresh() {
mPresenter.refresh();
updateTextView();
}
@Override
public void showGames(Cursor games) {
if (mAdapter != null) {
mAdapter.swapCursor(games);
}
updateTextView();
}
private void updateTextView() {
mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
private void findViews(View root) {
mRecyclerView = root.findViewById(R.id.grid_games);
mTextView = root.findViewById(R.id.gamelist_empty_text);
}
}

View File

@ -0,0 +1,42 @@
package org.citra.citra_emu.ui.platform;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.model.GameDatabase;
import org.citra.citra_emu.utils.Log;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public final class PlatformGamesPresenter {
private final PlatformGamesView mView;
public PlatformGamesPresenter(PlatformGamesView view) {
mView = view;
}
public void onCreateView() {
loadGames();
}
public void refresh() {
Log.debug("[PlatformGamesPresenter] : Refreshing...");
loadGames();
}
private void loadGames() {
Log.debug("[PlatformGamesPresenter] : Loading games...");
GameDatabase databaseHelper = CitraApplication.databaseHelper;
databaseHelper.getGames()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(games ->
{
Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...");
mView.showGames(games);
});
}
}

View File

@ -0,0 +1,21 @@
package org.citra.citra_emu.ui.platform;
import android.database.Cursor;
/**
* Abstraction for a screen representing a single platform's games.
*/
public interface PlatformGamesView {
/**
* Tell the view to refresh its contents.
*/
void refresh();
/**
* To be called when an asynchronous database read completes. Passes the
* result, in this case a {@link Cursor}, to the view.
*
* @param games A Cursor containing the games read from the database.
*/
void showGames(Cursor games);
}

View File

@ -0,0 +1,5 @@
package org.citra.citra_emu.utils;
public interface Action1<T> {
void call(T t);
}

View File

@ -0,0 +1,38 @@
package org.citra.citra_emu.utils;
import android.content.AsyncQueryHandler;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import org.citra.citra_emu.model.GameDatabase;
import org.citra.citra_emu.model.GameProvider;
public class AddDirectoryHelper {
private Context mContext;
public AddDirectoryHelper(Context context) {
this.mContext = context;
}
public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) {
AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
@Override
protected void onInsertComplete(int token, Object cookie, Uri uri) {
addDirectoryListener.onDirectoryAdded();
}
};
ContentValues file = new ContentValues();
file.put(GameDatabase.KEY_FOLDER_PATH, dir);
handler.startInsert(0, // We don't need to identify this call to the handler
null, // We don't need to pass additional data to the handler
GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
file);
}
public interface AddDirectoryListener {
void onDirectoryAdded();
}
}

View File

@ -0,0 +1,22 @@
package org.citra.citra_emu.utils;
import java.util.HashMap;
import java.util.Map;
public class BiMap<K, V> {
private Map<K, V> forward = new HashMap<K, V>();
private Map<V, K> backward = new HashMap<V, K>();
public synchronized void add(K key, V value) {
forward.put(key, value);
backward.put(value, key);
}
public synchronized V getForward(K key) {
return forward.get(key);
}
public synchronized K getBackward(V key) {
return backward.get(key);
}
}

View File

@ -0,0 +1,198 @@
package org.citra.citra_emu.utils;
import android.app.Activity;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.ui.main.MainActivity;
import java.util.ArrayList;
import java.util.List;
public class BillingManager implements PurchasesUpdatedListener {
private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium";
private final Activity mActivity;
private BillingClient mBillingClient;
private SkuDetails mSkuPremium;
private boolean mIsPremiumActive = false;
private boolean mIsServiceConnected = false;
private Runnable mUpdateBillingCallback;
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
public BillingManager(Activity activity) {
mActivity = activity;
mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();
querySkuDetails();
}
static public boolean isPremiumCached() {
return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false);
}
/**
* @return true if Premium subscription is currently active
*/
public boolean isPremiumActive() {
return mIsPremiumActive;
}
/**
* Invokes the billing flow for Premium
*
* @param callback Optional callback, called once, on completion of billing
*/
public void invokePremiumBilling(Runnable callback) {
if (mSkuPremium == null) {
return;
}
// Optional callback to refresh the UI for the caller when billing completes
mUpdateBillingCallback = callback;
// Invoke the billing flow
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(mSkuPremium)
.build();
mBillingClient.launchBillingFlow(mActivity, flowParams);
}
private void updatePremiumState(boolean isPremiumActive) {
mIsPremiumActive = isPremiumActive;
// Cache state for synchronous UI
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive);
editor.apply();
// No need to show button in action bar if Premium is active
MainActivity.setPremiumButtonVisible(!isPremiumActive);
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) {
if (purchaseList == null || purchaseList.isEmpty()) {
// Premium is not active, or billing is unavailable
updatePremiumState(false);
return;
}
Purchase premiumPurchase = null;
for (Purchase purchase : purchaseList) {
if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) {
premiumPurchase = purchase;
}
}
if (premiumPurchase != null) {
// Premium has been purchased
updatePremiumState(true);
if (mUpdateBillingCallback != null) {
try {
mUpdateBillingCallback.run();
} catch (Exception e) {
e.printStackTrace();
}
mUpdateBillingCallback = null;
}
}
}
private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) {
if (skuDetailsList == null) {
// This can happen when no user is signed in
return;
}
if (skuDetailsList.isEmpty()) {
return;
}
mSkuPremium = skuDetailsList.get(0);
queryPurchases();
}
private void querySkuDetails() {
Runnable queryToExecute = new Runnable() {
@Override
public void run() {
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
List<String> skuList = new ArrayList<>();
skuList.add(BILLING_SKU_PREMIUM);
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
(billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList));
}
};
executeServiceRequest(queryToExecute);
}
private void onQueryPurchasesFinished(PurchasesResult result) {
// Have we been disposed of in the meantime? If so, or bad result code, then quit
if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
updatePremiumState(false);
return;
}
// Update the UI and purchases inventory with new list of purchases
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
}
private void queryPurchases() {
Runnable queryToExecute = new Runnable() {
@Override
public void run() {
final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
onQueryPurchasesFinished(purchasesResult);
}
};
executeServiceRequest(queryToExecute);
}
private void startServiceConnection(final Runnable executeOnFinish) {
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
mIsServiceConnected = true;
}
if (executeOnFinish != null) {
executeOnFinish.run();
}
}
@Override
public void onBillingServiceDisconnected() {
mIsServiceConnected = false;
}
});
}
private void executeServiceRequest(Runnable runnable) {
if (mIsServiceConnected) {
runnable.run();
} else {
// If billing service was disconnected, we try to reconnect 1 time.
startServiceConnection(runnable);
}
}
}

View File

@ -0,0 +1,66 @@
package org.citra.citra_emu.utils;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
/**
* Some controllers have incorrect mappings. This class has special-case fixes for them.
*/
public class ControllerMappingHelper {
/**
* Some controllers report extra button presses that can be ignored.
*/
public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) {
if (isDualShock4(inputDevice)) {
// The two analog triggers generate analog motion events as well as a keycode.
// We always prefer to use the analog values, so throw away the button press
return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2;
}
return false;
}
/**
* Scale an axis to be zero-centered with a proper range.
*/
public float scaleAxis(InputDevice inputDevice, int axis, float value) {
if (isDualShock4(inputDevice)) {
// Android doesn't have correct mappings for this controller's triggers. It reports them
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
// Scale them to properly zero-centered with a range of [0.0, 1.0].
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
return (value + 1) / 2.0f;
}
} else if (isXboxOneWireless(inputDevice)) {
// Same as the DualShock 4, the mappings are missing.
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
return (value + 1) / 2.0f;
}
if (axis == MotionEvent.AXIS_GENERIC_1) {
// This axis is stuck at ~.5. Ignore it.
return 0.0f;
}
} else if (isMogaPro2Hid(inputDevice)) {
// This controller has a broken axis that reports a constant value. Ignore it.
if (axis == MotionEvent.AXIS_GENERIC_1) {
return 0.0f;
}
}
return value;
}
private boolean isDualShock4(InputDevice inputDevice) {
// Sony DualShock 4 controller
return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc;
}
private boolean isXboxOneWireless(InputDevice inputDevice) {
// Microsoft Xbox One controller
return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0;
}
private boolean isMogaPro2Hid(InputDevice inputDevice) {
// Moga Pro 2 HID
return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271;
}
}

View File

@ -0,0 +1,186 @@
/**
* Copyright 2014 Dolphin Emulator Project
* Licensed under GPLv2+
* Refer to the license.txt file included.
*/
package org.citra.citra_emu.utils;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.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;
/**
* A service that spawns its own thread in order to copy several binary and shader files
* from the Citra APK to the external file system.
*/
public final class DirectoryInitialization {
public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST";
public static final String EXTRA_STATE = "directoryState";
private static volatile DirectoryInitializationState directoryState = null;
private static String userPath;
private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false);
public static void start(Context context) {
// Can take a few seconds to run, so don't block UI thread.
//noinspection TrivialFunctionalExpressionUsage
((Runnable) () -> init(context)).run();
}
private static void init(Context context) {
if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true))
return;
if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) {
if (PermissionsHandler.hasWriteAccess(context)) {
if (setCitraUserDirectory()) {
initializeInternalStorage(context);
NativeLibrary.CreateConfigFile();
directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
} else {
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
}
} else {
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
}
}
isCitraDirectoryInitializationRunning.set(false);
sendBroadcastState(directoryState, context);
}
private static void deleteDirectoryRecursively(File file) {
if (file.isDirectory()) {
for (File child : file.listFiles())
deleteDirectoryRecursively(child);
}
file.delete();
}
public static boolean areCitraDirectoriesReady() {
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
}
public static String getUserDirectory() {
if (directoryState == null) {
throw new IllegalStateException("DirectoryInitialization has to run at least once!");
} else if (isCitraDirectoryInitializationRunning.get()) {
throw new IllegalStateException(
"DirectoryInitialization has to finish running first!");
}
return userPath;
}
private static native void SetSysDirectory(String path);
private static boolean setCitraUserDirectory() {
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;
}
}
return false;
}
private static void initializeInternalStorage(Context context) {
File sysDirectory = new File(context.getFilesDir(), "Sys");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String revision = NativeLibrary.GetGitRevision();
if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
// There is no extracted Sys directory, or there is a Sys directory from another
// version of Citra that might contain outdated files. Let's (re-)extract Sys.
deleteDirectoryRecursively(sysDirectory);
copyAssetFolder("Sys", sysDirectory, true, context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("sysDirectoryVersion", revision);
editor.apply();
}
// Let the native code know where the Sys directory is.
SetSysDirectory(sysDirectory.getPath());
}
private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
Intent localIntent =
new Intent(BROADCAST_ACTION)
.putExtra(EXTRA_STATE, state);
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
}
private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
try {
if (!output.exists() || overwrite) {
InputStream in = context.getAssets().open(asset);
OutputStream out = new FileOutputStream(output);
copyFile(in, out);
in.close();
out.close();
}
} catch (IOException e) {
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
e.getMessage());
}
}
private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
Context context) {
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
outputFolder);
try {
boolean createdFolder = false;
for (String file : context.getAssets().list(assetFolder)) {
if (!createdFolder) {
outputFolder.mkdir();
createdFolder = true;
}
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
overwrite, context);
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
context);
}
} catch (IOException e) {
Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
e.getMessage());
}
}
private static void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
public enum DirectoryInitializationState {
CITRA_DIRECTORIES_INITIALIZED,
EXTERNAL_STORAGE_PERMISSION_NEEDED,
CANT_FIND_EXTERNAL_STORAGE
}
}

View File

@ -0,0 +1,22 @@
package org.citra.citra_emu.utils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
public class DirectoryStateReceiver extends BroadcastReceiver {
Action1<DirectoryInitializationState> callback;
public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) {
this.callback = callback;
}
@Override
public void onReceive(Context context, Intent intent) {
DirectoryInitializationState state = (DirectoryInitializationState) intent
.getSerializableExtra(DirectoryInitialization.EXTRA_STATE);
callback.call(state);
}
}

View File

@ -0,0 +1,58 @@
package org.citra.citra_emu.utils;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import org.citra.citra_emu.CitraApplication;
public class EmulationMenuSettings {
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
// These must match what is defined in src/core/settings.h
public static final int LayoutOption_Default = 0;
public static final int LayoutOption_SingleScreen = 1;
public static final int LayoutOption_LargeScreen = 2;
public static final int LayoutOption_SideScreen = 3;
public static final int LayoutOption_MobilePortrait = 4;
public static final int LayoutOption_MobileLandscape = 5;
public static int getLandscapeScreenLayout() {
return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape);
}
public static void setLandscapeScreenLayout(int value) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value);
editor.apply();
}
public static boolean getShowFps() {
return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false);
}
public static void setShowFps(boolean value) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("EmulationMenuSettings_ShowFps", value);
editor.apply();
}
public static boolean getSwapScreens() {
return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false);
}
public static void setSwapScreens(boolean value) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("EmulationMenuSettings_SwapScreens", value);
editor.apply();
}
public static boolean getShowOverlay() {
return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true);
}
public static void setShowOverlay(boolean value) {
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("EmulationMenuSettings_ShowOverylay", value);
editor.apply();
}
}

View File

@ -0,0 +1,72 @@
package org.citra.citra_emu.utils;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.Utils;
import org.citra.citra_emu.activities.CustomFilePickerActivity;
import java.io.File;
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) {
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_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();
}
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;
}
return null;
}
}

View File

@ -0,0 +1,37 @@
package org.citra.citra_emu.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileUtil {
public static byte[] getBytesFromFile(File file) throws IOException {
final long length = file.length();
// You cannot create an array using a long type.
if (length > Integer.MAX_VALUE) {
// File is too large
throw new IOException("File is too large!");
}
byte[] bytes = new byte[(int) length];
int offset = 0;
int numRead;
try (InputStream is = new FileInputStream(file)) {
while (offset < bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
}
// Ensure all the bytes have been read in
if (offset < bytes.length) {
throw new IOException("Could not completely read file " + file.getName());
}
return bytes;
}
}

View File

@ -0,0 +1,27 @@
package org.citra.citra_emu.utils;
import android.graphics.Bitmap;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Request;
import com.squareup.picasso.RequestHandler;
import org.citra.citra_emu.NativeLibrary;
import java.nio.IntBuffer;
public class GameIconRequestHandler extends RequestHandler {
@Override
public boolean canHandleRequest(Request data) {
return "iso".equals(data.uri.getScheme());
}
@Override
public Result load(Request request, int networkPolicy) {
String url = request.uri.getHost() + request.uri.getPath();
int[] vector = NativeLibrary.GetIcon(url);
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
return new Result(bitmap, Picasso.LoadedFrom.DISK);
}
}

View File

@ -0,0 +1,39 @@
package org.citra.citra_emu.utils;
import org.citra.citra_emu.BuildConfig;
/**
* Contains methods that call through to {@link android.util.Log}, but
* with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
* levels in release builds.
*/
public final class Log {
private static final String TAG = "Citra Frontend";
private Log() {
}
public static void verbose(String message) {
if (BuildConfig.DEBUG) {
android.util.Log.v(TAG, message);
}
}
public static void debug(String message) {
if (BuildConfig.DEBUG) {
android.util.Log.d(TAG, message);
}
}
public static void info(String message) {
android.util.Log.i(TAG, message);
}
public static void warning(String message) {
android.util.Log.w(TAG, message);
}
public static void error(String message) {
android.util.Log.e(TAG, message);
}
}

View File

@ -0,0 +1,35 @@
package org.citra.citra_emu.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class PermissionsHandler {
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
// 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;
}
@TargetApi(Build.VERSION_CODES.M)
public static boolean checkWritePermission(final FragmentActivity activity) {
if (isFirstBoot(activity)) {
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_WRITE_PERMISSION);
return false;
}
return true;
}
public static boolean hasWriteAccess(Context context) {
return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
}

View File

@ -0,0 +1,44 @@
package org.citra.citra_emu.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import com.squareup.picasso.Transformation;
public class PicassoRoundedCornersTransformation implements Transformation {
@Override
public Bitmap transform(Bitmap icon) {
final Rect rect = new Rect(0, 0, icon.getWidth(), icon.getHeight());
final int size = Math.min(icon.getWidth(), icon.getHeight());
final int x = (icon.getWidth() - size) / 2;
final int y = (icon.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size);
if (squaredBitmap != icon) {
icon.recycle();
}
Bitmap output =
Bitmap.createBitmap(icon.getWidth(), icon.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
canvas.drawRoundRect(new RectF(rect), 10, 10, paint);
squaredBitmap.recycle();
return output;
}
@Override
public String key() {
return "circle";
}
}

View File

@ -0,0 +1,59 @@
package org.citra.citra_emu.utils;
import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import java.io.IOException;
import androidx.annotation.Nullable;
public class PicassoUtils {
private static boolean mPicassoInitialized = false;
public static void init() {
if (mPicassoInitialized) {
return;
}
Picasso picassoInstance = new Picasso.Builder(CitraApplication.getAppContext())
.addRequestHandler(new GameIconRequestHandler())
.build();
Picasso.setSingletonInstance(picassoInstance);
mPicassoInitialized = true;
}
public static void loadGameIcon(ImageView imageView, String gamePath) {
Picasso
.get()
.load(Uri.parse("iso:/" + gamePath))
.noFade()
.placeholder(R.drawable.no_icon)
.fit()
.centerInside()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_icon)
.transform(new PicassoRoundedCornersTransformation())
.into(imageView);
}
// Blocking call. Load image from file and crop/resize it to fit in width x height.
@Nullable
public static Bitmap LoadBitmapFromFile(String uri, int width, int height) {
try {
return Picasso.get()
.load(Uri.parse(uri))
.config(Bitmap.Config.ARGB_8888)
.centerCrop()
.resize(width, height)
.get();
} catch (IOException e) {
return null;
}
}
}

View File

@ -0,0 +1,45 @@
package org.citra.citra_emu.utils;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
public final class StartupHandler {
private static void handlePermissionsCheck(FragmentActivity parent) {
// Ask the user to grant write permission if it's not already granted
PermissionsHandler.checkWritePermission(parent);
String start_file = "";
Bundle extras = parent.getIntent().getExtras();
if (extras != null) {
start_file = extras.getString("AutoStartFile");
}
if (!TextUtils.isEmpty(start_file)) {
// Start the emulation activity, send the ISO passed in and finish the main activity
Intent emulation_intent = new Intent(parent, EmulationActivity.class);
emulation_intent.putExtra("SelectedGame", start_file);
parent.startActivity(emulation_intent);
parent.finish();
}
}
public static void HandleInit(FragmentActivity parent) {
if (PermissionsHandler.isFirstBoot(parent)) {
// Prompt user with standard first boot disclaimer
new AlertDialog.Builder(parent)
.setTitle(R.string.app_name)
.setIcon(R.mipmap.ic_launcher)
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
.setPositiveButton(android.R.string.ok, null)
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
.show();
}
}
}

View File

@ -0,0 +1,34 @@
package org.citra.citra_emu.utils;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.appcompat.app.AppCompatDelegate;
import org.citra.citra_emu.CitraApplication;
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) {
switch (designValue) {
case 0:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case 1:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case 2:
AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM :
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
break;
}
}
public static void applyTheme() {
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0));
}
}

View File

@ -0,0 +1,46 @@
package org.citra.citra_emu.viewholders;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
/**
* A simple class that stores references to views so that the GameAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public class GameViewHolder extends RecyclerView.ViewHolder {
private View itemView;
public ImageView imageIcon;
public TextView textGameTitle;
public TextView textCompany;
public TextView textFileName;
public String gameId;
// TODO Not need any of this stuff. Currently only the properties dialog needs it.
public String path;
public String title;
public String description;
public String regions;
public String company;
public GameViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
itemView.setTag(this);
imageIcon = itemView.findViewById(R.id.image_game_screen);
textGameTitle = itemView.findViewById(R.id.text_game_title);
textCompany = itemView.findViewById(R.id.text_company);
textFileName = itemView.findViewById(R.id.text_filename);
}
public View getItemView() {
return itemView;
}
}

View File

@ -1,13 +0,0 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra;
import android.app.Application;
public class CitraApplication extends Application {
static {
System.loadLibrary("citra-android");
}
}

View File

@ -1,41 +0,0 @@
package org.citra_emu.citra;
public class LOG {
private interface LOG_LEVEL {
int TRACE = 0, DEBUG = 1, INFO = 2, WARNING = 3, ERROR = 4, CRITICAL = 5;
}
public static void TRACE(String msg, Object... args) {
LOG(LOG_LEVEL.TRACE, msg, args);
}
public static void DEBUG(String msg, Object... args) {
LOG(LOG_LEVEL.DEBUG, msg, args);
}
public static void INFO(String msg, Object... args) {
LOG(LOG_LEVEL.INFO, msg, args);
}
public static void WARNING(String msg, Object... args) {
LOG(LOG_LEVEL.WARNING, msg, args);
}
public static void ERROR(String msg, Object... args) {
LOG(LOG_LEVEL.ERROR, msg, args);
}
public static void CRITICAL(String msg, Object... args) {
LOG(LOG_LEVEL.CRITICAL, msg, args);
}
private static void LOG(int level, String msg, Object... args) {
StackTraceElement trace = Thread.currentThread().getStackTrace()[4];
logEntry(level, trace.getFileName(), trace.getLineNumber(), trace.getMethodName(),
String.format(msg, args));
}
private static native void logEntry(int level, String file_name, int line_number,
String function, String message);
}

View File

@ -1,58 +0,0 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra.ui.main;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import org.citra_emu.citra.R;
import org.citra_emu.citra.utils.FileUtil;
import org.citra_emu.citra.utils.PermissionUtil;
public final class MainActivity extends AppCompatActivity {
// Java enums suck
private interface PermissionCodes { int INITIALIZE = 0; }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PermissionUtil.verifyPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE,
PermissionCodes.INITIALIZE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case PermissionCodes.INITIALIZE:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initUserPath(FileUtil.getUserPath().toString());
initLogging();
} else {
AlertDialog.Builder dialog =
new AlertDialog.Builder(this)
.setTitle("Permission Error")
.setMessage("Citra requires storage permissions to function.")
.setCancelable(false)
.setPositiveButton("OK", (dialogInterface, which) -> {
PermissionUtil.verifyPermission(
MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE,
PermissionCodes.INITIALIZE);
});
dialog.show();
}
}
}
private static native void initUserPath(String path);
private static native void initLogging();
}

View File

@ -1,19 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra_emu.citra.utils;
import android.os.Environment;
import java.io.File;
public class FileUtil {
public static File getUserPath() {
File storage = Environment.getExternalStorageDirectory();
File userPath = new File(storage, "citra");
if (!userPath.isDirectory())
userPath.mkdir();
return userPath;
}
}

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