diff --git a/CMakeLists.txt b/CMakeLists.txt index 41d55f37538..5b8d64cd23d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,11 +209,7 @@ if (ENABLE_FFMPEG) endif() endif() - if (ENABLE_FFMPEG_VIDEO_DUMPER) - find_package(FFmpeg REQUIRED COMPONENTS avcodec avformat avutil swscale swresample) - else() - find_package(FFmpeg REQUIRED COMPONENTS avcodec) - endif() + find_package(FFmpeg REQUIRED COMPONENTS avcodec) if ("${FFmpeg_avcodec_VERSION}" VERSION_LESS "57.48.101") message(FATAL_ERROR "Found version for libavcodec is too low. The required version is at least 57.48.101 (included in FFmpeg 3.1 and later).") endif() @@ -331,10 +327,12 @@ git_describe(GIT_DESC --always --long --dirty) git_branch_name(GIT_BRANCH) get_timestamp(BUILD_DATE) -enable_testing() +if(NOT ANDROID) + enable_testing() + add_subdirectory(dist/installer) +endif() add_subdirectory(externals) add_subdirectory(src) -add_subdirectory(dist/installer) # Set citra-qt project or citra project as default StartUp Project in Visual Studio depending on whether QT is enabled or not diff --git a/README.md b/README.md index dc5c6ce300b..5ff6eb69362 100644 --- a/README.md +++ b/README.md @@ -1,44 +1 @@ -**BEFORE FILING AN ISSUE, READ THE RELEVANT SECTION IN THE [CONTRIBUTING](https://github.com/citra-emu/citra/wiki/Contributing#reporting-issues) FILE!!!** - -Citra -============== -[![Travis CI Build Status](https://travis-ci.org/citra-emu/citra.svg?branch=master)](https://travis-ci.org/citra-emu/citra) -[![AppVeyor CI Build Status](https://ci.appveyor.com/api/projects/status/sdf1o4kh3g1e68m9?svg=true)](https://ci.appveyor.com/project/bunnei/citra) -[![Bitrise CI Build Status](https://app.bitrise.io/app/4ccd8e5720f0d13b/status.svg?token=H32TmbCwxb3OQ-M66KbAyw&branch=master)](https://app.bitrise.io/app/4ccd8e5720f0d13b) - -Citra is an experimental open-source Nintendo 3DS emulator/debugger written in C++. It is written with portability in mind, with builds actively maintained for Windows, Linux and macOS. - -Citra emulates a subset of 3DS hardware and therefore is useful for running/debugging homebrew applications, and it is also able to run many commercial games! Some of these do not run at a playable state, but we are working every day to advance the project forward. (Playable here means compatibility of at least "Okay" on our [game compatibility list](https://citra-emu.org/game).) - -Citra is licensed under the GPLv2 (or any later version). Refer to the license.txt file included. Please read the [FAQ](https://citra-emu.org/wiki/faq/) before getting started with the project. - -Check out our [website](https://citra-emu.org/)! - -Need help? Check out our [asking for help](https://citra-emu.org/help/reference/asking/) guide. - -For development discussion, please join us at #citra-dev on freenode. - -### Development - -Most of the development happens on GitHub. It's also where [our central repository](https://github.com/citra-emu/citra) is hosted. - -If you want to contribute please take a look at the [Contributor's Guide](https://github.com/citra-emu/citra/wiki/Contributing) and [Developer Information](https://github.com/citra-emu/citra/wiki/Developer-Information). You should as well contact any of the developers in the forum in order to know about the current state of the emulator because the [TODO list](https://docs.google.com/document/d/1SWIop0uBI9IW8VGg97TAtoT_CHNoP42FzYmvG1F4QDA) isn't maintained anymore. - -If you want to contribute to the user interface translation, please checkout [citra project on transifex](https://www.transifex.com/citra/citra). We centralize the translation work there, and periodically upstream translation. - -### Building - -* __Windows__: [Windows Build](https://github.com/citra-emu/citra/wiki/Building-For-Windows) -* __Linux__: [Linux Build](https://github.com/citra-emu/citra/wiki/Building-For-Linux) -* __macOS__: [macOS Build](https://github.com/citra-emu/citra/wiki/Building-for-macOS) - - -### Support -We happily accept monetary donations or donated games and hardware. Please see our [donations page](https://citra-emu.org/donate/) for more information on how you can contribute to Citra. Any donations received will go towards things like: -* 3DS consoles for developers to explore the hardware -* 3DS games for testing -* Any equipment required for homebrew -* Infrastructure setup -* Eventually 3D displays to get proper 3D output working - -We also more than gladly accept used 3DS consoles, preferably ones with firmware 4.5 or lower! If you would like to give yours away, don't hesitate to join our IRC channel #citra on [Freenode](http://webchat.freenode.net/?channels=citra) and talk to neobrain or bunnei. Mind you, IRC is slow-paced, so it might be a while until people reply. If you're in a hurry you can just leave contact details in the channel or via private message and we'll get back to you. +Appreciate citra team's awesome work. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 663a9493f2f..d68302ead1a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,7 +88,10 @@ add_subdirectory(video_core) add_subdirectory(audio_core) add_subdirectory(network) add_subdirectory(input_common) -add_subdirectory(tests) + +if(NOT ANDROID) + add_subdirectory(tests) +endif() if (ENABLE_SDL2) add_subdirectory(citra) @@ -98,7 +101,7 @@ if (ENABLE_QT) add_subdirectory(citra_qt) endif() if (ANDROID) - add_subdirectory(android/app/src/main/cpp) + add_subdirectory(android/jni) else() add_subdirectory(dedicated_room) endif() diff --git a/src/android/.gitignore b/src/android/.gitignore index 5edb4eeb072..d93539ee693 100644 --- a/src/android/.gitignore +++ b/src/android/.gitignore @@ -8,3 +8,5 @@ /build /captures .externalNativeBuild +app/release +app/.cxx diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index e5727151748..322c3971e15 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -1,8 +1,7 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 - buildToolsVersion '28.0.3' + compileSdkVersion 28 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -16,23 +15,19 @@ android { } defaultConfig { - applicationId "org.citra_emu" - minSdkVersion 21 - targetSdkVersion 26 - + applicationId "org.citra.emu" + minSdkVersion 24 + targetSdkVersion 28 versionCode(getBuildVersionCode()) - versionName "${getVersion()}" } signingConfigs { release { - if (project.hasProperty('keystore')) { - storeFile file(project.property('keystore')) - storePassword project.property('storepass') - keyAlias project.property('keyalias') - keyPassword project.property('keypass') - } + keyAlias 'dolphin-release-key' + keyPassword 'zhangwei' + storeFile file('D:/Android/android-sign-key/dolphin-release-key.jks') + storePassword 'zhangwei' } } @@ -41,6 +36,9 @@ android { // Signed by release key, allowing for upload to Play Store. release { signingConfig signingConfigs.release + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } // Signed by debug key disallowing distribution on Play Store. @@ -55,8 +53,8 @@ android { externalNativeBuild { cmake { - version getCmakeVersion() path "../../../CMakeLists.txt" + version "3.10.2" } } @@ -65,20 +63,19 @@ android { cmake { arguments "-DENABLE_QT=0", // Don't use QT "-DENABLE_SDL2=0", // Don't use SDL + "-DENABLE_WEB_SERVICE=0", // Don't use web services "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work - "-DENABLE_CUBEB=0", - "-DANDROID_STL=c++_shared" - + "-DENABLE_CUBEB=1", + "-DANDROID_STL=c++_static", + "-DCMAKE_CXX_FLAGS_RELEASE=-Ofast" abiFilters "arm64-v8a" - - targets "citra-android" } } } } ext { - androidSupportVersion = '26.1.0' + androidSupportVersion = '27.1.0' } dependencies { @@ -86,14 +83,6 @@ dependencies { implementation "com.android.support:cardview-v7:$androidSupportVersion" implementation "com.android.support:recyclerview-v7:$androidSupportVersion" implementation "com.android.support:design:$androidSupportVersion" - - // Android TV UI libraries. - implementation "com.android.support:leanback-v17:$androidSupportVersion" - - implementation 'com.android.support.constraint:constraint-layout:1.1.0' - - testImplementation "com.android.support.test:runner:1.0.2" - androidTestImplementation "com.android.support.test:runner:1.0.1" } def getVersion() { @@ -122,19 +111,3 @@ def getBuildVersionCode() { 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 -} diff --git a/src/android/app/proguard-rules.pro b/src/android/app/proguard-rules.pro index f1b424510da..e1ef77cc6b3 100644 --- a/src/android/app/proguard-rules.pro +++ b/src/android/app/proguard-rules.pro @@ -19,3 +19,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keep class org.citra.emu.NativeLibrary { *; } +-keep class android.support.v7.app.** { *; } diff --git a/src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java b/src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java deleted file mode 100644 index 7055de885ee..00000000000 --- a/src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.citra_emu.citra; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("org.citra_emu.citra_android", appContext.getPackageName()); - } -} diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c1e38446abd..114b65262ba 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -1,20 +1,19 @@ - + package="org.citra.emu"> - + + - + + + + @@ -34,6 +33,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/assets/shaders/FXAA.glsl b/src/android/app/src/main/assets/shaders/FXAA.glsl new file mode 100644 index 00000000000..daa7dc7c817 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/FXAA.glsl @@ -0,0 +1,67 @@ +// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +// Version 2, December 2004 + +// Copyright (C) 2013 mudlord + +// Everyone is permitted to copy and distribute verbatim or modified +// copies of this license document, and changing it is allowed as long +// as the name is changed. + +// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +// TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +// 0. You just DO WHAT THE FUCK YOU WANT TO. + +#define FXAA_REDUCE_MIN (1.0/ 128.0) +#define FXAA_REDUCE_MUL (1.0 / 8.0) +#define FXAA_SPAN_MAX 8.0 + +float4 applyFXAA(float2 fragCoord) +{ + float4 color; + float2 inverseVP = GetInvResolution(); + float3 rgbNW = SampleLocation((fragCoord + float2(-1.0, -1.0)) * inverseVP).xyz; + float3 rgbNE = SampleLocation((fragCoord + float2(1.0, -1.0)) * inverseVP).xyz; + float3 rgbSW = SampleLocation((fragCoord + float2(-1.0, 1.0)) * inverseVP).xyz; + float3 rgbSE = SampleLocation((fragCoord + float2(1.0, 1.0)) * inverseVP).xyz; + float3 rgbM = SampleLocation(fragCoord * inverseVP).xyz; + float3 luma = float3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + float2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * + (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(float2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(float2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) * inverseVP; + + float3 rgbA = 0.5 * ( + SampleLocation(fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz + + SampleLocation(fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz); + float3 rgbB = rgbA * 0.5 + 0.25 * ( + SampleLocation(fragCoord * inverseVP + dir * -0.5).xyz + + SampleLocation(fragCoord * inverseVP + dir * 0.5).xyz); + + float lumaB = dot(rgbB, luma); + if ((lumaB < lumaMin) || (lumaB > lumaMax)) + color = float4(rgbA, 1.0); + else + color = float4(rgbB, 1.0); + return color; +} + +void main() +{ + SetOutput(applyFXAA(GetCoordinates() * GetResolution())); +} diff --git a/src/android/app/src/main/assets/shaders/FXAA_natural.glsl b/src/android/app/src/main/assets/shaders/FXAA_natural.glsl new file mode 100644 index 00000000000..8a16acbb820 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/FXAA_natural.glsl @@ -0,0 +1,80 @@ +#define FXAA_REDUCE_MIN (1.0 / 128.0) +#define FXAA_REDUCE_MUL (1.0 / 8.0) +#define FXAA_SPAN_MAX 8.0 + +float3 applyFXAA(float2 fragCoord) +{ + float2 inverseVP = GetInvResolution(); + float3 rgbNW = SampleLocation((fragCoord + float2(-1.0, -1.0)) * inverseVP).xyz; + float3 rgbNE = SampleLocation((fragCoord + float2(1.0, -1.0)) * inverseVP).xyz; + float3 rgbSW = SampleLocation((fragCoord + float2(-1.0, 1.0)) * inverseVP).xyz; + float3 rgbSE = SampleLocation((fragCoord + float2(1.0, 1.0)) * inverseVP).xyz; + float3 rgbM = SampleLocation(fragCoord * inverseVP).xyz; + float3 luma = float3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + float2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * + (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(float2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(float2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) * inverseVP; + + float3 rgbA = 0.5 * ( + SampleLocation(fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz + + SampleLocation(fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz); + float3 rgbB = rgbA * 0.5 + 0.25 * ( + SampleLocation(fragCoord * inverseVP + dir * -0.5).xyz + + SampleLocation(fragCoord * inverseVP + dir * 0.5).xyz); + + float lumaB = dot(rgbB, luma); + if ((lumaB < lumaMin) || (lumaB > lumaMax)) + return rgbA; + else + return rgbB; +} + +#define MUL(a, b) (b * a) +#define GIN 2.2 +#define GOUT 2.2 +#define Y 1.1 +#define I 1.1 +#define Q 1.1 + +const mat3x3 RGBtoYIQ = mat3x3(0.299, 0.587, 0.114, + 0.595716, -0.274453, -0.321263, + 0.211456, -0.522591, 0.311135); + +const mat3x3 YIQtoRGB = mat3x3(1, 0.95629572, 0.62102442, + 1, -0.27212210, -0.64738060, + 1, -1.10698902, 1.70461500); + +const float3 YIQ_lo = float3(0, -0.595716, -0.522591); +const float3 YIQ_hi = float3(1, 0.595716, 0.522591); + +float4 applyNatural(float3 c) +{ + c = pow(c, float3(GIN, GIN, GIN)); + c = MUL(RGBtoYIQ, c); + c = float3(pow(c.x, Y), c.y * I, c.z * Q); + c = clamp(c, YIQ_lo, YIQ_hi); + c = MUL(YIQtoRGB, c); + c = pow(c, float3(1.0/GOUT, 1.0/GOUT, 1.0/GOUT)); + return float4(c, 1.0); +} + +void main() +{ + SetOutput(applyNatural(applyFXAA(GetCoordinates() * GetResolution()))); +} diff --git a/src/android/app/src/main/assets/shaders/SEDI.glsl b/src/android/app/src/main/assets/shaders/SEDI.glsl new file mode 100644 index 00000000000..bf6571f4c81 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/SEDI.glsl @@ -0,0 +1,114 @@ +/* + Simple Edge Directed Interpolation (SEDI) v1.0 + + Copyright (C) 2017 SimoneT - simone1tarditi@gmail.com + + de Blur - Copyright (C) 2016 guest(r) - guest.r@gmail.com + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +// de Blur control +#define filterparam 5.0 + +float CLength(float3 c1) +{ + float rmean = c1.r * 0.5; + c1 *= c1; + return sqrt((2.0 + rmean) * c1.r + 4.0 * c1.g + (3.0 - rmean) * c1.b); +} + +float Cdistance(float3 c1, float3 c2) +{ + float rmean = (c1.r + c2.r) * 0.5; + c1 = pow(c1 - c2, float3(2.0)); + return sqrt((2.0 + rmean) * c1.r + 4.0 * c1.g + (3.0 - rmean) * c1.b); +} + +float3 ColMin(float3 a, float3 b) +{ + float dist = step(0.01, sign(CLength(a) - CLength(b))); + return mix(a, b, dist); +} + +float3 ColMax(float3 a, float3 b) +{ + float dist = step(0.01, sign(CLength(a) - CLength(b))); + return mix(b, a, dist); +} + +float3 Blur(float2 TexCoord) +{ + float2 shift = GetInvResolution() * 0.5; + + float3 C06 = SampleLocation(TexCoord - shift.xy).rgb; + float3 C07 = SampleLocation(TexCoord + float2( shift.x,-shift.y)).rgb; + float3 C11 = SampleLocation(TexCoord + float2(-shift.x, shift.y)).rgb; + float3 C12 = SampleLocation(TexCoord + shift.xy).rgb; + + float dif1 = Cdistance(C06, C12) + 0.00001; + float dif2 = Cdistance(C07, C11) + 0.00001; + + dif1 = pow(dif1, filterparam); + dif2 = pow(dif2, filterparam); + + float dif3 = dif1 + dif2; + return (dif1 * (C07 + C11) * 0.5 + dif2 * (C06 + C12) * 0.5) / dif3; +} + +// de Blur code +float3 deBlur() +{ + float2 Size = GetInvResolution(); + float2 coord = GetCoordinates(); + float2 dx = float2( Size.x, 0.0); + float2 dy = float2( 0.0, Size.y); + float2 g1 = float2( Size.x, Size.y); + float2 g2 = float2(-Size.x, Size.y); + + float3 C0 = Blur(coord-g1).rgb; + float3 C1 = Blur(coord-dy).rgb; + float3 C2 = Blur(coord-g2).rgb; + float3 C3 = Blur(coord-dx).rgb; + float3 C4 = Blur(coord ).rgb; + float3 C5 = Blur(coord+dx).rgb; + float3 C6 = Blur(coord+g2).rgb; + float3 C7 = Blur(coord+dy).rgb; + float3 C8 = Blur(coord+g1).rgb; + + float3 mn1 = ColMin(ColMin(C0, C1), C2); + float3 mn2 = ColMin(ColMin(C3, C4), C5); + float3 mn3 = ColMin(ColMin(C6, C7), C8); + mn1 = ColMin(ColMin(mn1, mn2), mn3); + + float3 mx1 = ColMax(ColMax(C0, C1), C2); + float3 mx2 = ColMax(ColMax(C3, C4), C5); + float3 mx3 = ColMax(ColMax(C6, C7), C8); + mx1 = ColMax(ColMax(mx1, mx2), mx3); + + float dif1 = Cdistance(C4, mn1) + 0.00001; + float dif2 = Cdistance(C4, mx1) + 0.00001; + + dif1 = pow(dif1, filterparam); + dif2 = pow(dif2, filterparam); + + float dif3 = dif1 + dif2; + return (dif1 * mx1 + dif2 * mn1) / dif3; +} + +void main() +{ + SetOutput(float4(deBlur(), 1.0)); +} diff --git a/src/android/app/src/main/assets/shaders/bloom.glsl b/src/android/app/src/main/assets/shaders/bloom.glsl new file mode 100644 index 00000000000..07f04c07316 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/bloom.glsl @@ -0,0 +1,35 @@ +const float amount = 0.60; // suitable range = 0.00 - 1.00 +const float power = 0.5; // suitable range = 0.0 - 1.0 + +void main() +{ + float2 vCoord0 = GetCoordinates(); + float3 color = Sample().xyz; + float4 sum = vec4(0); + float3 bloom; + + for(int i= -3 ;i < 3; i++) + { + sum += SampleLocation(vCoord0 + vec2(-1, i) * 0.004) * amount; + sum += SampleLocation(vCoord0 + vec2( 0, i) * 0.004) * amount; + sum += SampleLocation(vCoord0 + vec2( 1, i) * 0.004) * amount; + } + + if (color.r < 0.3 && color.g < 0.3 && color.b < 0.3) + { + bloom = sum.xyz * sum.xyz * 0.012 + color; + } + else + { + if (color.r < 0.5 && color.g < 0.5 && color.b < 0.5) + { + bloom = sum.xyz * sum.xyz * 0.009 + color; + } + else + { + bloom = sum.xyz * sum.xyz * 0.0075 + color; + } + } + + SetOutput(float4(mix(color, bloom, power), 1.0)); +} diff --git a/src/android/app/src/main/assets/shaders/brighten.glsl b/src/android/app/src/main/assets/shaders/brighten.glsl new file mode 100644 index 00000000000..669e09c37a7 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/brighten.glsl @@ -0,0 +1,4 @@ +void main() +{ + SetOutput(Sample()* 2.0); +} diff --git a/src/android/app/src/main/assets/shaders/cartoon.glsl b/src/android/app/src/main/assets/shaders/cartoon.glsl new file mode 100644 index 00000000000..5feaa6ea1f3 --- /dev/null +++ b/src/android/app/src/main/assets/shaders/cartoon.glsl @@ -0,0 +1,47 @@ +const float scaleoffset = 0.8; //edge detection offset +const float bb = 0.5; // effects black border sensitivity; from 0.0 to 1.0 + +void main() +{ + float2 delta = GetInvResolution() * scaleoffset; + float2 dg1 = float2( delta.x, delta.y); + float2 dg2 = float2(-delta.x, delta.y); + float2 dx = float2( delta.x, 0.0); + float2 dy = float2( 0.0, delta.y); + + float4 v_texcoord1, v_texcoord2, v_texcoord3, v_texcoord4, v_texcoord5, v_texcoord6; + + v_texcoord1.xy = GetCoordinates() - dy; + v_texcoord2.xy = GetCoordinates() + dy; + v_texcoord3.xy = GetCoordinates() - dx; + v_texcoord4.xy = GetCoordinates() + dx; + v_texcoord5.xy = GetCoordinates() - dg1; + v_texcoord6.xy = GetCoordinates() + dg1; + v_texcoord1.zw = GetCoordinates() - dg2; + v_texcoord2.zw = GetCoordinates() + dg2; + + float3 c00 = SampleLocation(v_texcoord5.xy).xyz; + float3 c10 = SampleLocation(v_texcoord1.xy).xyz; + float3 c20 = SampleLocation(v_texcoord2.zw).xyz; + float3 c01 = SampleLocation(v_texcoord3.xy).xyz; + float3 c11 = SampleLocation(GetCoordinates()).xyz; + float3 c21 = SampleLocation(v_texcoord4.xy).xyz; + float3 c02 = SampleLocation(v_texcoord1.zw).xyz; + float3 c12 = SampleLocation(v_texcoord2.xy).xyz; + float3 c22 = SampleLocation(v_texcoord6.xy).xyz; + float3 dt = float3(1.0, 1.0, 1.0); + + float d1 = dot(abs(c00 - c22), dt); + float d2 = dot(abs(c20 - c02), dt); + float hl = dot(abs(c01 - c21), dt); + float vl = dot(abs(c10 - c12), dt); + float d = bb * (d1 + d2 + hl + vl) / (dot(c11, dt) + 0.15); + + float lc = 4.0 * length(c11); + float f = fract(lc); f*=f; + lc = 0.25 * (floor(lc) + f * f) + 0.05; + c11 = 4.0 * normalize(c11); + float3 frct = fract(c11); frct *= frct; + c11 = floor(c11) + 0.05 * dt + frct * frct; + SetOutput(float4(0.25 * lc * (1.1 - d * sqrt(d)) * c11, 1.0)); +} diff --git a/src/android/app/src/main/assets/shaders/film.glsl b/src/android/app/src/main/assets/shaders/film.glsl new file mode 100644 index 00000000000..e81b1d0c40c --- /dev/null +++ b/src/android/app/src/main/assets/shaders/film.glsl @@ -0,0 +1,109 @@ +const float Filmic_Strength = 0.60; +const float Filmic_Contrast = 1.03; +const float Fade = 0.0; +const float Linearization = 1.0; +const float Filmic_Bleach = 0.0; +const float Saturation = -0.25; +const float BaseCurve = 1.5; +const float BaseGamma = 1.0; +const float EffectGamma = 0.68; + +void main() +{ + float3 B = Sample().rgb; + float3 G = B; + float3 H = float3(0.01); + + B = clamp(B, 0.0, 1.); + B = pow(float3(B), float3(Linearization)); + B = mix(H, B, Filmic_Contrast); + + float3 LumCoeff = float3(0.2126, 0.7152, 0.0722); + float A = dot(B.rgb, LumCoeff); + float3 D = float3(A); + + B = pow(B, 1.0 / float3(BaseGamma)); + + float RedCurve = 1.0; + float GreenCurve = 1.0; + float BlueCurve = 1.0; + + float a = RedCurve; + float b = GreenCurve; + float c = BlueCurve; + float d = BaseCurve; + + float y = 1.0 / (1.0 + exp(a / 2.0)); + float z = 1.0 / (1.0 + exp(b / 2.0)); + float w = 1.0 / (1.0 + exp(c / 2.0)); + float v = 1.0 / (1.0 + exp(d / 2.0)); + + float3 C = B; + + D.r = (1.0 / (1.0 + exp(-a * (D.r - 0.5))) - y) / (1.0 - 2.0 * y); + D.g = (1.0 / (1.0 + exp(-b * (D.g - 0.5))) - z) / (1.0 - 2.0 * z); + D.b = (1.0 / (1.0 + exp(-c * (D.b - 0.5))) - w) / (1.0 - 2.0 * w); + + D = pow(D, 1.0 / float3(EffectGamma)); + + float3 Di = 1.0 - D; + + D = mix(D, Di, Filmic_Bleach); + + float EffectGammaR = 1.0; + float EffectGammaG = 1.0; + float EffectGammaB = 1.0; + + D.r = pow(abs(D.r), 1.0 / EffectGammaR); + D.g = pow(abs(D.g), 1.0 / EffectGammaG); + D.b = pow(abs(D.b), 1.0 / EffectGammaB); + + if (D.r < 0.5) + C.r = (2.0 * D.r - 1.0) * (B.r - B.r * B.r) + B.r; + else + C.r = (2.0 * D.r - 1.0) * (sqrt(B.r) - B.r) + B.r; + + if (D.g < 0.5) + C.g = (2.0 * D.g - 1.0) * (B.g - B.g * B.g) + B.g; + else + C.g = (2.0 * D.g - 1.0) * (sqrt(B.g) - B.g) + B.g; + + if (D.b < 0.5) + C.b = (2.0 * D.b - 1.0) * (B.b - B.b * B.b) + B.b; + else + C.b = (2.0 * D.b - 1.0) * (sqrt(B.b) - B.b) + B.b; + + float3 F = mix(B, C, Filmic_Strength); + F = (1.0 / (1.0 + exp(-d * (F - 0.5))) - v) / (1.0 - 2.0 * v); + + float r2R = 1.0 - Saturation; + float g2R = 0.0 + Saturation; + float b2R = 0.0 + Saturation; + + float r2G = 0.0 + Saturation; + float g2G = (1.0 - Fade) - Saturation; + float b2G = (0.0 + Fade) + Saturation; + + float r2B = 0.0 + Saturation; + float g2B = (0.0 + Fade) + Saturation; + float b2B = (1.0 - Fade) - Saturation; + + float3 iF = F; + + F.r = (iF.r * r2R + iF.g * g2R + iF.b * b2R); + F.g = (iF.r * r2G + iF.g * g2G + iF.b * b2G); + F.b = (iF.r * r2B + iF.g * g2B + iF.b * b2B); + + float N = dot(F.rgb, LumCoeff); + float3 Cn = F; + + if (N < 0.5) + Cn = (2.0 * N - 1.0) * (F - F * F) + F; + else + Cn = (2.0 * N - 1.0) * (sqrt(F) - F) + F; + + Cn = pow(max(Cn, float3(0)), 1.0 / float3(Linearization)); + + float3 Fn = mix(B, Cn, Filmic_Strength); + SetOutput(float4(Fn, 1.0)); +} diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt deleted file mode 100644 index f3a7e0131d3..00000000000 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -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 "../../../../../" "./") diff --git a/src/android/app/src/main/cpp/logging/log.cpp b/src/android/app/src/main/cpp/logging/log.cpp deleted file mode 100644 index 044f4eb4cff..00000000000 --- a/src/android/app/src/main/cpp/logging/log.cpp +++ /dev/null @@ -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), GetJString(env, file_name).data(), - static_cast(line_number), GetJString(env, function).data(), - GetJString(env, msg).data()); -} -} -} // namespace Log diff --git a/src/android/app/src/main/cpp/logging/logcat_backend.cpp b/src/android/app/src/main/cpp/logging/logcat_backend.cpp deleted file mode 100644 index 17b6ae1a01c..00000000000 --- a/src/android/app/src/main/cpp/logging/logcat_backend.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#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 \ No newline at end of file diff --git a/src/android/app/src/main/cpp/logging/logcat_backend.h b/src/android/app/src/main/cpp/logging/logcat_backend.h deleted file mode 100644 index f3bac4762a1..00000000000 --- a/src/android/app/src/main/cpp/logging/logcat_backend.h +++ /dev/null @@ -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 diff --git a/src/android/app/src/main/cpp/native_interface.cpp b/src/android/app/src/main/cpp/native_interface.cpp deleted file mode 100644 index fc4d73b77c2..00000000000 --- a/src/android/app/src/main/cpp/native_interface.cpp +++ /dev/null @@ -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 diff --git a/src/android/app/src/main/cpp/native_interface.h b/src/android/app/src/main/cpp/native_interface.h deleted file mode 100644 index a7b99cb510a..00000000000 --- a/src/android/app/src/main/cpp/native_interface.h +++ /dev/null @@ -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 -#include - -namespace CitraJNI { -extern "C" { -jint JNI_OnLoad(JavaVM* vm, void* reserved); -} - -std::string GetJString(JNIEnv* env, jstring jstr); -} // namespace CitraJNI diff --git a/src/android/app/src/main/cpp/ui/main/main_activity.cpp b/src/android/app/src/main/cpp/ui/main/main_activity.cpp deleted file mode 100644 index b99ba189027..00000000000 --- a/src/android/app/src/main/cpp/ui/main/main_activity.cpp +++ /dev/null @@ -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_dir + LOG_FILE)); - Log::AddBackend(std::make_unique()); -} -}; -}; // namespace MainActivity diff --git a/src/android/app/src/main/ic_citra-web.png b/src/android/app/src/main/ic_citra-web.png deleted file mode 100644 index 129946a3721..00000000000 Binary files a/src/android/app/src/main/ic_citra-web.png and /dev/null differ diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java new file mode 100644 index 00000000000..9aca6ddb830 --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerActivity.java @@ -0,0 +1,168 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ClipData; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; +import java.util.ArrayList; +import java.util.List; +import org.citra.emu.R; + +/** + * An abstract base activity that handles all the fluff you don't care about. + *

+ * Usage: To start a child activity you could either use an intent starting the + * activity directly, or you could use an implicit intent with GET_CONTENT, if + * it + * is also defined in your manifest. It is defined to be handled here in case + * you + * want the user to be able to use other file pickers on the system. + *

+ * That means using an intent with action GET_CONTENT + * If you want to be able to select multiple items, include EXTRA_ALLOW_MULTIPLE + * (default false). + *

+ * Some non-standard extra arguments are supported as well: + * EXTRA_ONLY_DIRS - (default false) allows only directories to be selected. + * EXTRA_START_PATH - (default null) which should specify the starting path. + * EXTRA_ALLOW_EXISTING_FILE - (default true) if existing files are selectable in 'new file'-mode + *

+ * The result of the user's action is returned in onActivityResult intent, + * access it using getUri. + * In case of multiple choices, these can be accessed with getClipData + * containing Uri objects. + * If running earlier than JellyBean you can access them with + * getStringArrayListExtra(EXTRA_PATHS) + * + * @param + */ +public abstract class AbstractFilePickerActivity + extends AppCompatActivity implements AbstractFilePickerFragment.OnFilePickedListener { + public static final String EXTRA_START_PATH = "nononsense.intent" + + ".START_PATH"; + public static final String EXTRA_MODE = "nononsense.intent.MODE"; + public static final String EXTRA_SINGLE_CLICK = "nononsense.intent" + + ".SINGLE_CLICK"; + // For compatibility + public static final String EXTRA_ALLOW_MULTIPLE = "android.intent.extra" + + ".ALLOW_MULTIPLE"; + public static final String EXTRA_ALLOW_EXISTING_FILE = "android.intent.extra" + + ".ALLOW_EXISTING_FILE"; + public static final String EXTRA_PATHS = "nononsense.intent.PATHS"; + public static final int MODE_FILE = AbstractFilePickerFragment.MODE_FILE; + public static final int MODE_FILE_AND_DIR = AbstractFilePickerFragment.MODE_FILE_AND_DIR; + public static final int MODE_NEW_FILE = AbstractFilePickerFragment.MODE_NEW_FILE; + public static final int MODE_DIR = AbstractFilePickerFragment.MODE_DIR; + protected static final String TAG = "filepicker_fragment"; + protected String startPath = null; + protected int mode = AbstractFilePickerFragment.MODE_FILE; + protected boolean allowMultiple = false; + private boolean allowExistingFile = true; + protected boolean singleClick = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.nnf_activity_filepicker); + + Intent intent = getIntent(); + if (intent != null) { + startPath = intent.getStringExtra(EXTRA_START_PATH); + mode = intent.getIntExtra(EXTRA_MODE, mode); + allowMultiple = intent.getBooleanExtra(EXTRA_ALLOW_MULTIPLE, allowMultiple); + allowExistingFile = + intent.getBooleanExtra(EXTRA_ALLOW_EXISTING_FILE, allowExistingFile); + singleClick = intent.getBooleanExtra(EXTRA_SINGLE_CLICK, singleClick); + } + + // Default to cancelled + setResult(Activity.RESULT_CANCELED); + } + + @Override + protected void onResume() { + super.onResume(); + + FragmentManager fm = getSupportFragmentManager(); + Fragment fragment = fm.findFragmentByTag(TAG); + + if (fragment == null) { + fragment = getFragment(startPath, mode, allowMultiple, allowExistingFile, singleClick); + } + + if (fragment != null) { + fm.beginTransaction().replace(R.id.fragment, fragment, TAG).commit(); + } + } + + protected abstract AbstractFilePickerFragment getFragment(@Nullable final String startPath, + final int mode, + final boolean allowMultiple, + final boolean allowExistingFile, + final boolean singleClick); + + @Override + public void onSaveInstanceState(Bundle b) { + b.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE"); + super.onSaveInstanceState(b); + } + + @Override + public void onFilePicked(@NonNull final Uri file) { + Intent i = new Intent(); + i.setData(file); + setResult(Activity.RESULT_OK, i); + finish(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onFilesPicked(@NonNull final List files) { + Intent i = new Intent(); + i.putExtra(EXTRA_ALLOW_MULTIPLE, true); + + // Set as String Extras for all versions + ArrayList paths = new ArrayList<>(); + for (Uri file : files) { + paths.add(file.toString()); + } + i.putStringArrayListExtra(EXTRA_PATHS, paths); + + // Set as Clip Data for Jelly bean and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + ClipData clip = null; + for (Uri file : files) { + if (clip == null) { + clip = new ClipData("Paths", new String[] {}, new ClipData.Item(file)); + } else { + clip.addItem(new ClipData.Item(file)); + } + } + i.setClipData(clip); + } + + setResult(Activity.RESULT_OK, i); + finish(); + } + + @Override + public void onCancelled() { + setResult(Activity.RESULT_CANCELED); + finish(); + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java new file mode 100644 index 00000000000..086ac160c0c --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/AbstractFilePickerFragment.java @@ -0,0 +1,870 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import static com.nononsenseapps.filepicker.Utils.appendPath; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.util.SortedList; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.citra.emu.R; + +/** + * A fragment representing a list of Files. + *

+ *

+ * Activities containing this fragment MUST implement the {@link + * OnFilePickedListener} + * interface. + */ +public abstract class AbstractFilePickerFragment + extends Fragment implements LoaderManager.LoaderCallbacks>, LogicHandler { + + // The different preset modes of operation. This impacts the behaviour + // and possible actions in the UI. + public static final int MODE_FILE = 0; + public static final int MODE_DIR = 1; + public static final int MODE_FILE_AND_DIR = 2; + public static final int MODE_NEW_FILE = 3; + // Where to display on open. + public static final String KEY_START_PATH = "KEY_START_PATH"; + // See MODE_XXX constants above for possible values + public static final String KEY_MODE = "KEY_MODE"; + // Allow multiple items to be selected. + public static final String KEY_ALLOW_MULTIPLE = "KEY_ALLOW_MULTIPLE"; + // Allow an existing file to be selected under MODE_NEW_FILE + public static final String KEY_ALLOW_EXISTING_FILE = "KEY_ALLOW_EXISTING_FILE"; + // If file can be selected by clicking only and checkboxes are not visible + public static final String KEY_SINGLE_CLICK = "KEY_SINGLE_CLICK"; + // Used for saving state. + protected static final String KEY_CURRENT_PATH = "KEY_CURRENT_PATH"; + protected final HashSet mCheckedItems; + protected final HashSet mCheckedVisibleViewHolders; + protected int mode = MODE_FILE; + protected T mCurrentPath = null; + protected boolean allowMultiple = false; + protected boolean allowExistingFile = true; + protected boolean singleClick = false; + protected OnFilePickedListener mListener; + protected FileItemAdapter mAdapter = null; + protected TextView mCurrentDirView; + protected EditText mEditTextFileName; + protected RecyclerView recyclerView; + protected LinearLayoutManager layoutManager; + protected SortedList mFiles = null; + protected Toast mToast = null; + // Keep track if we are currently loading a directory, in case it takes a long time + protected boolean isLoading = false; + protected View mNewFileButtonContainer = null; + protected View mRegularButtonContainer = null; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public AbstractFilePickerFragment() { + mCheckedItems = new HashSet<>(); + mCheckedVisibleViewHolders = new HashSet<>(); + + // Retain this fragment across configuration changes, to allow + // asynctasks and such to be used with ease. + setRetainInstance(true); + } + + protected FileItemAdapter getAdapter() { + return mAdapter; + } + + protected FileItemAdapter getDummyAdapter() { + return new FileItemAdapter<>(this); + } + + /** + * Set before making the fragment visible. This method will re-use the existing + * arguments bundle in the fragment if it exists so extra arguments will not + * be overwritten. This allows you to set any extra arguments in the fragment + * constructor if you wish. + *

+ * The key/value-pairs listed below will be overwritten however. + * + * @param startPath path to directory the picker will show upon start + * @param mode what is allowed to be selected (dirs, files, both) + * @param allowMultiple selecting a single item or several? + * @param allowExistingFile if selecting a "new" file, can existing files be chosen + * @param singleClick selecting an item does not require a press on OK + */ + public void setArgs(@Nullable final String startPath, final int mode, + final boolean allowMultiple, final boolean allowExistingFile, + final boolean singleClick) { + // Validate some assumptions so users don't get surprised (or get surprised early) + if (mode == MODE_NEW_FILE && allowMultiple) { + throw new IllegalArgumentException("MODE_NEW_FILE does not support 'allowMultiple'"); + } + // Single click only makes sense if we are not selecting multiple items + if (singleClick && allowMultiple) { + throw new IllegalArgumentException( + "'singleClick' can not be used with 'allowMultiple'"); + } + // There might have been arguments set elsewhere, if so do not overwrite them. + Bundle b = getArguments(); + if (b == null) { + b = new Bundle(); + } + + if (startPath != null) { + b.putString(KEY_START_PATH, startPath); + } + b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); + b.putBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); + b.putBoolean(KEY_SINGLE_CLICK, singleClick); + b.putInt(KEY_MODE, mode); + setArguments(b); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View view = inflateRootView(inflater, container); + + Toolbar toolbar = (Toolbar)view.findViewById(R.id.nnf_picker_toolbar); + if (toolbar != null) { + setupToolbar(toolbar); + } + + recyclerView = (RecyclerView)view.findViewById(android.R.id.list); + // improve performance if you know that changes in content + // do not change the size of the RecyclerView + recyclerView.setHasFixedSize(true); + // use a linear layout manager + layoutManager = new LinearLayoutManager(getActivity()); + recyclerView.setLayoutManager(layoutManager); + // Set Item Decoration if exists + configureItemDecoration(inflater, recyclerView); + // Set adapter + mAdapter = new FileItemAdapter<>(this); + recyclerView.setAdapter(mAdapter); + + view.findViewById(R.id.nnf_button_cancel).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onClickCancel(v); + } + }); + + view.findViewById(R.id.nnf_button_ok).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onClickOk(v); + } + }); + view.findViewById(R.id.nnf_button_ok_newfile) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickOk(v); + } + }); + + mNewFileButtonContainer = view.findViewById(R.id.nnf_newfile_button_container); + mRegularButtonContainer = view.findViewById(R.id.nnf_button_container); + + mEditTextFileName = (EditText)view.findViewById(R.id.nnf_text_filename); + mEditTextFileName.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + // deSelect anything selected since the user just modified the name + clearSelections(); + } + }); + + mCurrentDirView = (TextView)view.findViewById(R.id.nnf_current_dir); + // Restore state + if (mCurrentPath != null && mCurrentDirView != null) { + mCurrentDirView.setText(getFullPath(mCurrentPath)); + } + + return view; + } + + protected View inflateRootView(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(R.layout.nnf_fragment_filepicker, container, false); + } + + /** + * Checks if a divider drawable has been defined in the current theme. If it has, will apply + * an item decoration with the divider. If no divider has been specified, then does nothing. + */ + protected void configureItemDecoration(@NonNull LayoutInflater inflater, + @NonNull RecyclerView recyclerView) { + final TypedArray attributes = + getActivity().obtainStyledAttributes(new int[] {R.attr.nnf_list_item_divider}); + Drawable divider = attributes.getDrawable(0); + attributes.recycle(); + + if (divider != null) { + recyclerView.addItemDecoration(new DividerItemDecoration(divider)); + } + } + + /** + * Called when the cancel-button is pressed. + * + * @param view which was clicked. Not used in default implementation. + */ + public void onClickCancel(@NonNull View view) { + if (mListener != null) { + mListener.onCancelled(); + } + } + + /** + * Called when the ok-button is pressed. + * + * @param view which was clicked. Not used in default implementation. + */ + public void onClickOk(@NonNull View view) { + if (mListener == null) { + return; + } + + // Some invalid cases first + /*if (MODE_NEW_FILE == mode && !isValidFileName(getNewFileName())) { + mToast = Toast.makeText(getActivity(), R.string.nnf_need_valid_filename, + Toast.LENGTH_SHORT); + mToast.show(); + return; + }*/ + if ((allowMultiple || mode == MODE_FILE) && + (mCheckedItems.isEmpty() || getFirstCheckedItem() == null)) { + if (mToast == null) { + mToast = Toast.makeText(getActivity(), R.string.nnf_select_something_first, + Toast.LENGTH_SHORT); + } + mToast.show(); + return; + } + + // New file allows only a single file + if (mode == MODE_NEW_FILE) { + final String filename = getNewFileName(); + final Uri result; + if (filename.startsWith("/")) { + // Return absolute paths directly + result = toUri(getPath(filename)); + } else { + // Append to current directory + result = toUri(getPath(appendPath(getFullPath(mCurrentPath), filename))); + } + mListener.onFilePicked(result); + } else if (allowMultiple) { + mListener.onFilesPicked(toUri(mCheckedItems)); + } else if (mode == MODE_FILE) { + // noinspection ConstantConditions + mListener.onFilePicked(toUri(getFirstCheckedItem())); + } else if (mode == MODE_DIR) { + mListener.onFilePicked(toUri(mCurrentPath)); + } else { + // single FILE OR DIR + if (mCheckedItems.isEmpty()) { + mListener.onFilePicked(toUri(mCurrentPath)); + } else { + mListener.onFilePicked(toUri(getFirstCheckedItem())); + } + } + } + + /** + * + * @return filename as entered/picked by the user for the new file + */ + @NonNull + protected String getNewFileName() { + return mEditTextFileName.getText().toString(); + } + + /** + * Configure the toolbar anyway you like here. Default is to set it as the activity's + * main action bar. Override if you already provide an action bar. + * Not called if no toolbar was found. + * + * @param toolbar from layout with id "picker_toolbar" + */ + protected void setupToolbar(@NonNull Toolbar toolbar) { + ((AppCompatActivity)getActivity()).setSupportActionBar(toolbar); + } + + public @Nullable T getFirstCheckedItem() { + // noinspection LoopStatementThatDoesntLoop + for (T file : mCheckedItems) { + return file; + } + return null; + } + + protected @NonNull List toUri(@NonNull Iterable files) { + ArrayList uris = new ArrayList<>(); + for (T file : files) { + uris.add(toUri(file)); + } + return uris; + } + + public boolean isCheckable(@NonNull final T data) { + boolean checkable = false; + if (isDir(data)) { + checkable = false; + } else if (mode == MODE_FILE) { + checkable = true; + } + return checkable; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mListener = (OnFilePickedListener)context; + } catch (ClassCastException e) { + throw new ClassCastException(context.toString() + + " must implement OnFilePickedListener"); + } + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onViewStateRestored(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Only if we have no state + if (mCurrentPath == null) { + if (savedInstanceState != null) { + mode = savedInstanceState.getInt(KEY_MODE, mode); + allowMultiple = savedInstanceState.getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); + allowExistingFile = + savedInstanceState.getBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); + singleClick = savedInstanceState.getBoolean(KEY_SINGLE_CLICK, singleClick); + + String path = savedInstanceState.getString(KEY_CURRENT_PATH); + if (path != null) { + mCurrentPath = getPath(path.trim()); + } + } else if (getArguments() != null) { + mode = getArguments().getInt(KEY_MODE, mode); + allowMultiple = getArguments().getBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); + allowExistingFile = + getArguments().getBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); + singleClick = getArguments().getBoolean(KEY_SINGLE_CLICK, singleClick); + if (getArguments().containsKey(KEY_START_PATH)) { + String path = getArguments().getString(KEY_START_PATH); + if (path != null) { + T file = getPath(path.trim()); + if (isDir(file)) { + mCurrentPath = file; + } else { + mCurrentPath = getParent(file); + mEditTextFileName.setText(getName(file)); + } + } + } + } + } + + setModeView(); + + // If still null + if (mCurrentPath == null) { + mCurrentPath = getRoot(); + } + refresh(mCurrentPath); + } + + /** + * Hides/Shows appropriate views depending on mode + */ + protected void setModeView() { + boolean nf = mode == MODE_NEW_FILE; + mNewFileButtonContainer.setVisibility(nf ? View.VISIBLE : View.GONE); + mRegularButtonContainer.setVisibility(nf ? View.GONE : View.VISIBLE); + + if (!nf && singleClick) { + getActivity().findViewById(R.id.nnf_button_ok).setVisibility(View.GONE); + } + } + + @Override + public void onSaveInstanceState(Bundle b) { + b.putString(KEY_CURRENT_PATH, mCurrentPath.toString()); + b.putBoolean(KEY_ALLOW_MULTIPLE, allowMultiple); + b.putBoolean(KEY_ALLOW_EXISTING_FILE, allowExistingFile); + b.putBoolean(KEY_SINGLE_CLICK, singleClick); + b.putInt(KEY_MODE, mode); + super.onSaveInstanceState(b); + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + /** + * Refreshes the list. Call this when current path changes. This method also checks + * if permissions are granted and requests them if necessary. See hasPermission() + * and handlePermission(). By default, these methods do nothing. Override them if + * you need to request permissions at runtime. + * + * @param nextPath path to list files for + */ + protected void refresh(@NonNull T nextPath) { + if (hasPermission(nextPath)) { + mCurrentPath = nextPath; + isLoading = true; + getLoaderManager().restartLoader(0, null, AbstractFilePickerFragment.this); + } else { + handlePermission(nextPath); + } + } + + /** + * If permission has not been granted yet, this method should request it. + *

+ * Override only if you need to request a permission. + * + * @param path The path for which permission should be requested + */ + protected void handlePermission(@NonNull T path) { + // Nothing to do by default + } + + /** + * If your implementation needs to request a specific permission to function, check if it + * has been granted here. You should probably also override handlePermission() to request it. + * + * @param path the path for which permissions should be checked + * @return true if permission has been granted, false otherwise. + */ + protected boolean hasPermission(@NonNull T path) { + // Nothing to request by default + return true; + } + + /** + * Instantiate and return a new Loader for the given ID. + * + * @param id The ID whose loader is to be created. + * @param args Any arguments supplied by the caller. + * @return Return a new Loader instance that is ready to start loading. + */ + @Override + public Loader> onCreateLoader(final int id, final Bundle args) { + return getLoader(); + } + + /** + * Called when a previously created loader has finished its load. + * + * @param loader The Loader that has finished. + * @param data The data generated by the Loader. + */ + @Override + public void onLoadFinished(final Loader> loader, final SortedList data) { + isLoading = false; + mCheckedItems.clear(); + mCheckedVisibleViewHolders.clear(); + mFiles = data; + mAdapter.setList(data); + recyclerView.scrollToPosition(0); + if (mCurrentDirView != null) { + mCurrentDirView.setText(getFullPath(mCurrentPath)); + } + // Stop loading now to avoid a refresh clearing the user's selections + getLoaderManager().destroyLoader(0); + } + + /** + * Called when a previously created loader is being reset, and thus + * making its data unavailable. The application should at this point + * remove any references it has to the Loader's data. + * + * @param loader The Loader that is being reset. + */ + @Override + public void onLoaderReset(final Loader> loader) { + isLoading = false; + } + + /** + * @param position 0 - n, where the header has been subtracted + * @param data the actual file or directory + * @return an integer greater than 0 + */ + @Override + public int getItemViewType(int position, @NonNull T data) { + if (isCheckable(data)) { + return LogicHandler.VIEWTYPE_CHECKABLE; + } else { + return LogicHandler.VIEWTYPE_DIR; + } + } + + @Override + public void onBindHeaderViewHolder(@NonNull HeaderViewHolder viewHolder) { + viewHolder.text.setText(".."); + } + + /** + * @param parent Containing view + * @param viewType which the ViewHolder will contain + * @return a view holder for a file or directory + */ + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v; + switch (viewType) { + case LogicHandler.VIEWTYPE_HEADER: + v = LayoutInflater.from(getActivity()) + .inflate(R.layout.nnf_filepicker_listitem_dir, parent, false); + return new HeaderViewHolder(v); + case LogicHandler.VIEWTYPE_CHECKABLE: + v = LayoutInflater.from(getActivity()) + .inflate(R.layout.nnf_filepicker_listitem_checkable, parent, false); + return new CheckableViewHolder(v); + case LogicHandler.VIEWTYPE_DIR: + default: + v = LayoutInflater.from(getActivity()) + .inflate(R.layout.nnf_filepicker_listitem_dir, parent, false); + return new DirViewHolder(v); + } + } + + /** + * @param vh to bind data from either a file or directory + * @param position 0 - n, where the header has been subtracted + * @param data the file or directory which this item represents + */ + @Override + public void onBindViewHolder(@NonNull DirViewHolder vh, int position, @NonNull T data) { + vh.file = data; + vh.icon.setImageResource(isDir(data) ? R.drawable.nnf_ic_folder_black_48dp + : R.mipmap.ic_country); + vh.text.setText(getName(data)); + + if (isCheckable(data)) { + if (mCheckedItems.contains(data)) { + mCheckedVisibleViewHolders.add((CheckableViewHolder)vh); + ((CheckableViewHolder)vh).checkbox.setChecked(true); + } else { + // noinspection SuspiciousMethodCalls + mCheckedVisibleViewHolders.remove(vh); + ((CheckableViewHolder)vh).checkbox.setChecked(false); + } + } + } + + /** + * Animate de-selection of visible views and clear + * selected set. + */ + public void clearSelections() { + for (CheckableViewHolder vh : mCheckedVisibleViewHolders) { + vh.checkbox.setChecked(false); + } + mCheckedVisibleViewHolders.clear(); + mCheckedItems.clear(); + } + + /** + * Called when a header item ("..") is clicked. + * + * @param view that was clicked. Not used in default implementation. + * @param viewHolder for the clicked view + */ + public void onClickHeader(@NonNull View view, @NonNull HeaderViewHolder viewHolder) { + goUp(); + } + + /** + * Browses to the parent directory from the current directory. For example, if the current + * directory is /foo/bar/, then goUp() will change the current directory to /foo/. It is up to + * the caller to not call this in vain, e.g. if you are already at the root. + *

+ * Currently selected items are cleared by this operation. + */ + public void goUp() { + goToDir(getParent(mCurrentPath)); + } + + /** + * Called when a non-selectable item, typically a directory, is clicked. + * + * @param view that was clicked. Not used in default implementation. + * @param viewHolder for the clicked view + */ + public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { + if (isDir(viewHolder.file)) { + goToDir(viewHolder.file); + } + } + + /** + * Cab be used by the list to determine whether a file should be displayed or not. + * Default behavior is to always display folders. If files can be selected, + * then files are also displayed. In case a new file is supposed to be selected, + * the {@link #allowExistingFile} determines if existing files are visible + * + * @param file either a directory or file. + * @return True if item should be visible in the picker, false otherwise + */ + protected boolean isItemVisible(final T file) { + return true; + } + + /** + * Browses to the designated directory. It is up to the caller verify that the argument is + * in fact a directory. If another directory is in the process of being loaded, this method + * will not start another load. + *

+ * Currently selected items are cleared by this operation. + * + * @param file representing the target directory. + */ + public void goToDir(@NonNull T file) { + if (!isLoading) { + mCheckedItems.clear(); + mCheckedVisibleViewHolders.clear(); + refresh(file); + } + } + + /** + * Long clicking a non-selectable item does nothing by default. + * + * @param view which was long clicked. Not used in default implementation. + * @param viewHolder for the clicked view + * @return true if the callback consumed the long click, false otherwise. + */ + public boolean onLongClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { + return false; + } + + /** + * Called when a selectable item is clicked. This might be either a file or a directory. + * + * @param view that was clicked. Not used in default implementation. + * @param viewHolder for the clicked view + */ + public void onClickCheckable(@NonNull View view, @NonNull CheckableViewHolder viewHolder) { + if (isDir(viewHolder.file)) { + goToDir(viewHolder.file); + } else { + onLongClickCheckable(view, viewHolder); + if (singleClick) { + onClickOk(view); + } + } + } + + /** + * Long clicking a selectable item should toggle its selected state. Note that if only a + * single item can be selected, then other potentially selected views on screen must be + * de-selected. + * + * @param view which was long clicked. Not used in default implementation. + * @param viewHolder for the clicked view + * @return true if the callback consumed the long click, false otherwise. + */ + public boolean onLongClickCheckable(@NonNull View view, + @NonNull CheckableViewHolder viewHolder) { + if (MODE_NEW_FILE == mode) { + mEditTextFileName.setText(getName(viewHolder.file)); + } + onClickCheckBox(viewHolder); + return true; + } + + /** + * Called when a selectable item's checkbox is pressed. This should toggle its selected state. + * Note that if only a single item can be selected, then other potentially selected views on + * screen must be de-selected. The text box for new filename is also cleared. + * + * @param viewHolder for the item containing the checkbox. + */ + public void onClickCheckBox(@NonNull CheckableViewHolder viewHolder) { + if (mCheckedItems.contains(viewHolder.file)) { + viewHolder.checkbox.setChecked(false); + mCheckedItems.remove(viewHolder.file); + mCheckedVisibleViewHolders.remove(viewHolder); + } else { + if (!allowMultiple) { + clearSelections(); + } + viewHolder.checkbox.setChecked(true); + mCheckedItems.add(viewHolder.file); + mCheckedVisibleViewHolders.add(viewHolder); + } + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface OnFilePickedListener { + void onFilePicked(@NonNull Uri file); + + void onFilesPicked(@NonNull List files); + + void onCancelled(); + } + + public class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + final TextView text; + + public HeaderViewHolder(View v) { + super(v); + v.setOnClickListener(this); + text = (TextView)v.findViewById(android.R.id.text1); + } + + /** + * Called when a view has been clicked. + * + * @param v The view that was clicked. + */ + @Override + public void onClick(View v) { + onClickHeader(v, this); + } + } + + public class DirViewHolder + extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + + public ImageView icon; + public TextView text; + public T file; + + public DirViewHolder(View v) { + super(v); + v.setOnClickListener(this); + v.setOnLongClickListener(this); + icon = v.findViewById(R.id.item_icon); + text = (TextView)v.findViewById(android.R.id.text1); + } + + /** + * Called when a view has been clicked. + * + * @param v The view that was clicked. + */ + @Override + public void onClick(View v) { + onClickDir(v, this); + } + + /** + * Called when a view has been clicked and held. + * + * @param v The view that was clicked and held. + * @return true if the callback consumed the long click, false otherwise. + */ + @Override + public boolean onLongClick(View v) { + return onLongClickDir(v, this); + } + } + + public class CheckableViewHolder extends DirViewHolder { + + public CheckBox checkbox; + + public CheckableViewHolder(View v) { + super(v); + boolean nf = mode == MODE_NEW_FILE; + + checkbox = (CheckBox)v.findViewById(R.id.checkbox); + checkbox.setVisibility((nf || singleClick) ? View.GONE : View.VISIBLE); + checkbox.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickCheckBox(CheckableViewHolder.this); + } + }); + } + + /** + * Called when a view has been clicked. + * + * @param v The view that was clicked. + */ + @Override + public void onClick(View v) { + onClickCheckable(v, this); + } + + /** + * Called when a view has been clicked and held. + * + * @param v The view that was clicked and held. + * @return true if the callback consumed the long click, false otherwise. + */ + @Override + public boolean onLongClick(View v) { + return onLongClickCheckable(v, this); + } + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/DividerItemDecoration.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/DividerItemDecoration.java new file mode 100644 index 00000000000..4d855e55b57 --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/DividerItemDecoration.java @@ -0,0 +1,49 @@ +package com.nononsenseapps.filepicker; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * Basic ItemDecoration which loads a drawable as a divider. + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + private Drawable mDivider; + + public DividerItemDecoration(Drawable divider) { + mDivider = divider; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + + if (parent.getChildAdapterPosition(view) == 0) { + return; + } + + outRect.top = mDivider.getIntrinsicHeight(); + } + + @Override + public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + int dividerLeft = parent.getPaddingLeft(); + int dividerRight = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams(); + + int dividerTop = child.getBottom() + params.bottomMargin; + int dividerBottom = dividerTop + mDivider.getIntrinsicHeight(); + + mDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom); + mDivider.draw(canvas); + } + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/FileItemAdapter.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FileItemAdapter.java new file mode 100644 index 00000000000..6817db76f75 --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FileItemAdapter.java @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +/** + * A simple adapter which also inserts a header item ".." to handle going up to the parent folder. + * @param the type which is used, for example a normal java File object. + */ +public class FileItemAdapter extends RecyclerView.Adapter { + + protected final LogicHandler mLogic; + protected SortedList mList = null; + + public FileItemAdapter(@NonNull LogicHandler logic) { + this.mLogic = logic; + } + + public void setList(@Nullable SortedList list) { + mList = list; + notifyDataSetChanged(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return mLogic.onCreateViewHolder(parent, viewType); + } + + @Override + @SuppressWarnings("unchecked") + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int headerPosition) { + if (headerPosition == 0) { + mLogic.onBindHeaderViewHolder( + (AbstractFilePickerFragment.HeaderViewHolder)viewHolder); + } else { + int pos = headerPosition - 1; + mLogic.onBindViewHolder((AbstractFilePickerFragment.DirViewHolder)viewHolder, pos, + mList.get(pos)); + } + } + + @Override + public int getItemViewType(int headerPosition) { + if (0 == headerPosition) { + return LogicHandler.VIEWTYPE_HEADER; + } else { + int pos = headerPosition - 1; + return mLogic.getItemViewType(pos, mList.get(pos)); + } + } + + @Override + public int getItemCount() { + if (mList == null) { + return 0; + } + + // header + count + return 1 + mList.size(); + } + + /** + * Get the item at the designated position in the adapter. + * + * @param position of item in adapter + * @return null if position is zero (that means it's the ".." header), the item otherwise. + */ + protected @Nullable T getItem(int position) { + if (position == 0) { + return null; + } + return mList.get(position - 1); + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java new file mode 100644 index 00000000000..bcfffd1da7c --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerActivity.java @@ -0,0 +1,31 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import android.annotation.SuppressLint; +import android.os.Environment; +import android.support.annotation.Nullable; +import java.io.File; + +@SuppressLint("Registered") +public class FilePickerActivity extends AbstractFilePickerActivity { + + @Override + protected AbstractFilePickerFragment getFragment(@Nullable final String startPath, + final int mode, + final boolean allowMultiple, + final boolean allowExistingFile, + final boolean singleClick) { + AbstractFilePickerFragment fragment = new FilePickerFragment(); + // 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, allowExistingFile, singleClick); + return fragment; + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerFragment.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerFragment.java new file mode 100644 index 00000000000..fa4e88a91db --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/FilePickerFragment.java @@ -0,0 +1,333 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.FileObserver; +import android.support.annotation.NonNull; +import android.support.v4.content.AsyncTaskLoader; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.FileProvider; +import android.support.v4.content.Loader; +import android.support.v7.util.SortedList; +import android.support.v7.widget.util.SortedListAdapterCallback; +import android.widget.Toast; +import java.io.File; +import org.citra.emu.R; + +/** + * An implementation of the picker which allows you to select a file from the internal/external + * storage (SD-card) on a device. + */ +public class FilePickerFragment extends AbstractFilePickerFragment { + + protected static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; + protected boolean showHiddenItems = false; + private File mRequestedPath = null; + + public FilePickerFragment() {} + + /** + * This method is used to dictate whether hidden files and folders should be shown or not + * + * @param showHiddenItems whether hidden items should be shown or not + */ + public void showHiddenItems(boolean showHiddenItems) { + this.showHiddenItems = showHiddenItems; + } + + /** + * Returns if hidden items are shown or not + * + * @return true if hidden items are shown, otherwise false + */ + + public boolean areHiddenItemsShown() { + return showHiddenItems; + } + + /** + * @return true if app has been granted permission to write to the SD-card. + */ + @Override + protected boolean hasPermission(@NonNull File path) { + return PackageManager.PERMISSION_GRANTED == + ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + + /** + * Request permission to write to the SD-card. + */ + @Override + protected void handlePermission(@NonNull File path) { + // Should we show an explanation? + // if (shouldShowRequestPermissionRationale( + // Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + // Explain to the user why we need permission + // } + + mRequestedPath = path; + requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, + PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); + } + + /** + * This the method that gets notified when permission is granted/denied. By default, + * a granted request will result in a refresh of the list. + * + * @param requestCode the code you requested + * @param permissions array of permissions you requested. empty if process was cancelled. + * @param grantResults results for requests. empty if process was cancelled. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + // If arrays are empty, then process was cancelled + if (permissions.length == 0) { + // Treat this as a cancel press + if (mListener != null) { + mListener.onCancelled(); + } + } else { // if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE) { + if (PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Do refresh + if (mRequestedPath != null) { + refresh(mRequestedPath); + } + } else { + Toast + .makeText(getContext(), R.string.nnf_permission_external_write_denied, + Toast.LENGTH_SHORT) + .show(); + // Treat this as a cancel press + if (mListener != null) { + mListener.onCancelled(); + } + } + } + } + + /** + * Return true if the path is a directory and not a file. + * + * @param path either a file or directory + * @return true if path is a directory, false if file + */ + @Override + public boolean isDir(@NonNull final File path) { + return path.isDirectory(); + } + + /** + * @param path either a file or directory + * @return filename of path + */ + @NonNull + @Override + public String getName(@NonNull File path) { + return path.getName(); + } + + /** + * Return the path to the parent directory. Should return the root if + * from is root. + * + * @param from either a file or directory + * @return the parent directory + */ + @NonNull + @Override + public File getParent(@NonNull final File from) { + if (from.getPath().equals(getRoot().getPath())) { + // Already at root, we can't go higher + return from; + } else if (from.getParentFile() != null) { + return from.getParentFile(); + } else { + return from; + } + } + + /** + * Convert the path to the type used. + * + * @param path either a file or directory + * @return File representation of the string path + */ + @NonNull + @Override + public File getPath(@NonNull final String path) { + return new File(path); + } + + /** + * @param path either a file or directory + * @return the full path to the file + */ + @NonNull + @Override + public String getFullPath(@NonNull final File path) { + return path.getPath(); + } + + /** + * Get the root path. + * + * @return the highest allowed path, which is "/" by default + */ + @NonNull + @Override + public File getRoot() { + return new File("/"); + } + + /** + * Convert the path to a URI for the return intent + * + * @param file either a file or directory + * @return a Uri + */ + @NonNull + @Override + public Uri toUri(@NonNull final File file) { + return FileProvider.getUriForFile( + getContext(), getContext().getApplicationContext().getPackageName() + ".provider", + file); + } + + /** + * Get a loader that lists the Files in the current path, + * and monitors changes. + */ + @NonNull + @Override + public Loader> getLoader() { + return new AsyncTaskLoader>(getActivity()) { + FileObserver fileObserver; + + @Override + public SortedList loadInBackground() { + File[] listFiles = mCurrentPath.listFiles(); + final int initCap = listFiles == null ? 0 : listFiles.length; + + SortedList files = new SortedList<>( + File.class, new SortedListAdapterCallback(getDummyAdapter()) { + @Override + public int compare(File lhs, File rhs) { + return compareFiles(lhs, rhs); + } + + @Override + public boolean areContentsTheSame(File file, File file2) { + return file.getAbsolutePath().equals(file2.getAbsolutePath()) && + (file.isFile() == file2.isFile()); + } + + @Override + public boolean areItemsTheSame(File file, File file2) { + return areContentsTheSame(file, file2); + } + }, initCap); + + files.beginBatchedUpdates(); + if (listFiles != null) { + for (java.io.File f : listFiles) { + if (isItemVisible(f)) { + files.add(f); + } + } + } + files.endBatchedUpdates(); + + return files; + } + + /** + * Handles a request to start the Loader. + */ + @Override + protected void onStartLoading() { + super.onStartLoading(); + + // handle if directory does not exist. Fall back to root. + if (mCurrentPath == null || !mCurrentPath.isDirectory()) { + mCurrentPath = getRoot(); + } + + // Start watching for changes + fileObserver = new FileObserver( + mCurrentPath.getPath(), FileObserver.CREATE | FileObserver.DELETE | + FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { + @Override + public void onEvent(int event, String path) { + // Reload + onContentChanged(); + } + }; + fileObserver.startWatching(); + + forceLoad(); + } + + /** + * Handles a request to completely reset the Loader. + */ + @Override + protected void onReset() { + super.onReset(); + + // Stop watching + if (fileObserver != null) { + fileObserver.stopWatching(); + fileObserver = null; + } + } + }; + } + + /** + * Used by the list to determine whether a file should be displayed or not. + * Default behavior is to always display folders. If files can be selected, + * then files are also displayed. Set the showHiddenFiles property to show + * hidden file. Default behaviour is to hide hidden files. Override this method to enable other + * filtering behaviour, like only displaying files with specific extensions (.zip, .txt, etc). + * + * @param file to maybe add. Can be either a directory or file. + * @return True if item should be added to the list, false otherwise + */ + protected boolean isItemVisible(final File file) { + if (!showHiddenItems && file.isHidden()) { + return false; + } + return super.isItemVisible(file); + } + + /** + * Compare two files to determine their relative sort order. This follows the usual + * comparison interface. Override to determine your own custom sort order. + *

+ * Default behaviour is to place directories before files, but sort them alphabetically + * otherwise. + * + * @param lhs File on the "left-hand side" + * @param rhs File on the "right-hand side" + * @return -1 if if lhs should be placed before rhs, 0 if they are equal, + * and 1 if rhs should be placed before lhs + */ + protected int compareFiles(@NonNull File lhs, @NonNull File rhs) { + if (lhs.isDirectory() && !rhs.isDirectory()) { + return -1; + } else if (rhs.isDirectory() && !lhs.isDirectory()) { + return 1; + } else { + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + } +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/LogicHandler.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/LogicHandler.java new file mode 100644 index 00000000000..90f1db5cb27 --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/LogicHandler.java @@ -0,0 +1,108 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package com.nononsenseapps.filepicker; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.content.Loader; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +/** + * An interface for the methods required to handle backend-specific stuff. + */ +public interface LogicHandler { + + int VIEWTYPE_HEADER = 0; + int VIEWTYPE_DIR = 1; + int VIEWTYPE_CHECKABLE = 2; + + /** + * Return true if the path is a directory and not a file. + * + * @param path + */ + boolean isDir(@NonNull final T path); + + /** + * @param path + * @return filename of path + */ + @NonNull String getName(@NonNull final T path); + + /** + * Convert the path to a URI for the return intent + * + * @param path + * @return a Uri + */ + @NonNull Uri toUri(@NonNull final T path); + + /** + * Return the path to the parent directory. Should return the root if + * from is root. + * + * @param from + */ + @NonNull T getParent(@NonNull final T from); + + /** + * @param path + * @return the full path to the file + */ + @NonNull String getFullPath(@NonNull final T path); + + /** + * Convert the path to the type used. + * + * @param path + */ + @NonNull T getPath(@NonNull final String path); + + /** + * Get the root path (lowest allowed). + */ + @NonNull T getRoot(); + + /** + * Get a loader that lists the files in the current path, + * and monitors changes. + */ + @NonNull Loader> getLoader(); + + /** + * Bind the header ".." which goes to parent folder. + * + * @param viewHolder + */ + void onBindHeaderViewHolder(@NonNull AbstractFilePickerFragment.HeaderViewHolder viewHolder); + + /** + * Header is subtracted from the position + * + * @param parent + * @param viewType + * @return a view holder for a file or directory + */ + @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + /** + * @param viewHolder to bind data from either a file or directory + * @param position 0 - n, where the header has been subtracted + * @param data + */ + void onBindViewHolder(@NonNull AbstractFilePickerFragment.DirViewHolder viewHolder, + int position, @NonNull T data); + + /** + * @param position 0 - n, where the header has been subtracted + * @param data + * @return an integer greater than 0 + */ + int getItemViewType(int position, @NonNull T data); +} diff --git a/src/android/app/src/main/java/com/nononsenseapps/filepicker/Utils.java b/src/android/app/src/main/java/com/nononsenseapps/filepicker/Utils.java new file mode 100644 index 00000000000..2c6dfe5c5e8 --- /dev/null +++ b/src/android/app/src/main/java/com/nononsenseapps/filepicker/Utils.java @@ -0,0 +1,115 @@ +package com.nononsenseapps.filepicker; + +import static com.nononsenseapps.filepicker.AbstractFilePickerActivity.EXTRA_ALLOW_MULTIPLE; +import static com.nononsenseapps.filepicker.AbstractFilePickerActivity.EXTRA_PATHS; + +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Some utility methods + */ +public class Utils { + + private static final String SEP = "/"; + + /** + * Name is validated to be non-null, non-empty and not containing any + * slashes. + * + * @param name The name of the folder the user wishes to create. + */ + public static boolean isValidFileName(@Nullable String name) { + return !TextUtils.isEmpty(name) && !name.contains("/") && !name.equals(".") && + !name.equals(".."); + } + + /** + * Append the second pathString to the first. The result will not end with a /. + * In case two absolute paths are given, e.g. /A/B/, and /C/D/, then the result + * will be /A/B/C/D + * + * Multiple slashes will be shortened to a single slash, so /A///B is equivalent to /A/B + */ + @NonNull + public static String appendPath(@NonNull String first, @NonNull String second) { + String result = first + SEP + second; + + while (result.contains("//")) { + result = result.replaceAll("//", "/"); + } + + if (result.length() > 1 && result.endsWith(SEP)) { + return result.substring(0, result.length() - 1); + } else { + return result; + } + } + + /** + * Convert a uri generated by a fileprovider, like content://AUTHORITY/ROOT/actual/path + * to a file pointing to file:///actual/path + * + * Note that it only works for paths generated with `ROOT` as the path element. This is done if + * nnf_provider_paths.xml is used to define the file provider in the manifest. + * + * @param uri generated from a file provider + * @return Corresponding {@link File} object + */ + @NonNull + public static File getFileForUri(@NonNull Uri uri) { + String path = uri.getEncodedPath(); + final int splitIndex = path.indexOf('/', 1); + final String tag = Uri.decode(path.substring(1, splitIndex)); + path = Uri.decode(path.substring(splitIndex + 1)); + + if (!"root".equalsIgnoreCase(tag)) { + throw new IllegalArgumentException( + String.format("Can't decode paths to '%s', only for 'root' paths.", tag)); + } + + final File root = new File("/"); + + File file = new File(root, path); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve canonical path for " + file); + } + + if (!file.getPath().startsWith(root.getPath())) { + throw new SecurityException("Resolved path jumped beyond configured root"); + } + + return file; + } + + /** + * Parses the returned files from a filepicker activity into a nice list + * + * @param data returned by the {@link AbstractFilePickerActivity} + * @return a {@link List} of files (uris) which the user selected in the picker. + */ + @NonNull + public static List getSelectedFilesFromResult(@NonNull Intent data) { + List result = new ArrayList<>(); + if (data.getBooleanExtra(EXTRA_ALLOW_MULTIPLE, false)) { + List paths = data.getStringArrayListExtra(EXTRA_PATHS); + if (paths != null) { + for (String path : paths) { + result.add(Uri.parse(path)); + } + } + } else { + result.add(data.getData()); + } + return result; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/CitraApplication.java b/src/android/app/src/main/java/org/citra/emu/CitraApplication.java new file mode 100644 index 00000000000..45c1c98f421 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/CitraApplication.java @@ -0,0 +1,23 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.emu; + +import android.app.Application; +import org.citra.emu.utils.DirectoryInitialization; +import org.citra.emu.utils.PermissionsHandler; + +public class CitraApplication extends Application { + static { + System.loadLibrary("main"); + } + + @Override + public void onCreate() { + super.onCreate(); + + if (PermissionsHandler.hasWriteAccess(getApplicationContext())) + DirectoryInitialization.start(getApplicationContext()); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/emu/NativeLibrary.java new file mode 100644 index 00000000000..00e3f20fc6a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/NativeLibrary.java @@ -0,0 +1,207 @@ +package org.citra.emu; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.Surface; +import java.io.File; +import java.io.FileOutputStream; +import org.citra.emu.ui.EmulationActivity; +import org.citra.emu.ui.MainActivity; + +public final class NativeLibrary { + + public static Context getMainContext() { + return MainActivity.get(); + } + + public static Context getEmulationContext() { + return EmulationActivity.get(); + } + + public static void notifyGameShudown() { + Activity activity = EmulationActivity.get(); + if (activity != null) { + activity.finish(); + } + } + + public static void showMessageDialog(int type, String msg) { + Context context = getMainContext(); + if (context == null) { + context = getEmulationContext(); + if (context == null) { + Log.e("citra", "showMessageDialog: " + msg); + return; + } + } + final Activity activity = (Activity)context; + activity.runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.error); + builder.setMessage(msg); + builder.show(); + }); + } + + public static void showInputBoxDialog(int maxLength, String hint, String button0, + String button1, String button2) { + EmulationActivity activity = EmulationActivity.get(); + if (activity != null) { + activity.runOnUiThread( + () -> activity.showInputBoxDialog(maxLength, hint, button0, button1, button2)); + } + } + + public static void showMiiSelectorDialog(boolean cancel, String title, String[] miis) { + EmulationActivity activity = EmulationActivity.get(); + if (activity != null) { + activity.runOnUiThread(() -> activity.showMiiSelectorDialog(cancel, title, miis)); + } + } + + public static void saveImageToFile(String path, int width, int height, int[] pixels) { + if (pixels.length > 0 && width > 0 && height > 0) { + File file = new File(path); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + try { + FileOutputStream out = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + } catch (Exception e) { + Log.i("citra", "saveImageToFile error: " + e.getMessage()); + } + } + } + + public static void updateProgress(String name, int written, int total) { + MainActivity activity = MainActivity.get(); + if (activity != null) { + activity.runOnUiThread(() -> activity.updateProgress(name, written, total)); + } + } + + public static boolean isValidFile(String filename) { + String name = filename.toLowerCase(); + return (name.endsWith(".cia") || name.endsWith(".cci") || name.endsWith(".3ds") || + name.endsWith(".cxi")); + } + + public static native String GetAppId(String path); + + public static native String GetAppTitle(String path); + + public static native int[] GetAppIcon(String path); + + public static native int GetAppRegion(String path); + + public static native boolean IsAppExecutable(String path); + + public static native void SaveScreenShot(); + + public static native void InstallCIA(String[] path); + + public static native void SetUserPath(String path); + + // input overlay + public static native void InputEvent(int button, float value); + + // touch screen + public static native void TouchEvent(int action, int x, int y); + + // gamepad + public static native boolean KeyEvent(int button, int action); + public static native void MoveEvent(int axis, float value); + + // edit box + public static native void KeyboardEvent(int type, String text); + + public static native boolean IsRunning(); + + public static native void SurfaceChanged(Surface surf); + + public static native void SurfaceDestroyed(); + + public static native void Run(String path); + + public static native void ResumeEmulation(); + + public static native void PauseEmulation(); + + public static native void StopEmulation(); + + public static native int[] getRunningSettings(); + + public static native void setRunningSettings(int[] settings); + + /** + * Button type for use in onTouchEvent + */ + public static final class ButtonType { + public static final int N3DS_BUTTON_A = 0; + public static final int N3DS_BUTTON_B = 1; + public static final int N3DS_BUTTON_X = 2; + public static final int N3DS_BUTTON_Y = 3; + + public static final int N3DS_DPAD_UP = 4; + public static final int N3DS_DPAD_DOWN = 5; + public static final int N3DS_DPAD_LEFT = 6; + public static final int N3DS_DPAD_RIGHT = 7; + + public static final int N3DS_BUTTON_L = 8; + public static final int N3DS_BUTTON_R = 9; + + public static final int N3DS_BUTTON_START = 10; + public static final int N3DS_BUTTON_SELECT = 11; + public static final int N3DS_BUTTON_DEBUG = 12; + public static final int N3DS_BUTTON_GPIO14 = 13; + + public static final int N3DS_BUTTON_ZL = 14; + public static final int N3DS_BUTTON_ZR = 15; + + public static final int N3DS_BUTTON_HOME = 16; + + public static final int N3DS_CPAD_X = 17; + public static final int N3DS_CPAD_Y = 18; + public static final int N3DS_STICK_X = 19; + public static final int N3DS_STICK_Y = 20; + + public static final int N3DS_TOUCH_X = 21; + public static final int N3DS_TOUCH_Y = 22; + public static final int N3DS_TOUCH_Z = 23; + } + + /** + * Button states + */ + public static final class ButtonState { + public static final int RELEASED = 0; + public static final int PRESSED = 1; + } + + public static final class TouchEvent { + public static final int TOUCH_PRESSED = 1; + public static final int TOUCH_MOVED = 2; + public static final int TOUCH_RELEASED = 4; + public static final int BEGIN_TILT = 8; + public static final int TILT = 16; + public static final int END_TILT = 32; + } + + /** + * Game regions + */ + public static final class GameRegion { + public static final int Invalid = -1; + public static final int Japan = 0; + public static final int NorthAmerica = 1; + public static final int Europe = 2; + public static final int Australia = 3; + public static final int China = 4; + public static final int Korea = 5; + public static final int Taiwan = 6; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/model/GameFile.java b/src/android/app/src/main/java/org/citra/emu/model/GameFile.java new file mode 100644 index 00000000000..8dbd4b0016c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/model/GameFile.java @@ -0,0 +1,90 @@ +package org.citra.emu.model; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; + +import java.nio.IntBuffer; +import org.citra.emu.NativeLibrary; +import org.citra.emu.R; + +public final class GameFile { + private String mId; + private String mPath; + private String mName; + private String mInfo; + private Bitmap mIcon; + private int mRegion = NativeLibrary.GameRegion.Invalid; + + public GameFile(String path) { + mPath = path; + } + + public String getId() { + if (mId == null) { + mId = NativeLibrary.GetAppId(mPath); + } + return mId; + } + + public String getName() { + if (mName == null) { + mName = NativeLibrary.GetAppTitle(mPath); + } + return mName; + } + + public String getInfo() { + if (mInfo == null) { + mInfo = mPath.substring(mPath.lastIndexOf('/') + 1); + } + return mInfo; + } + + public String getPath() { + return mPath; + } + + public Bitmap getIcon(Context context) { + if (mIcon == null) { + int[] pixels = NativeLibrary.GetAppIcon(mPath); + if (pixels == null || pixels.length == 0) { + mIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.no_banner); + } else { + mIcon = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); + mIcon.copyPixelsFromBuffer(IntBuffer.wrap(pixels)); + } + + // rounded corners + float radius = 5.0f; + Rect rect = new Rect(0, 0, mIcon.getWidth(), mIcon.getHeight()); + Bitmap output = Bitmap.createBitmap(mIcon.getWidth(), mIcon.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + BitmapShader shader = new BitmapShader(mIcon, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(shader); + // rect contains the bounds of the shape + // radius is the radius in pixels of the rounded corners + // paint contains the shader that will texture the shape + canvas.drawRoundRect(new RectF(rect), radius, radius, paint); + mIcon = output; + } + return mIcon; + } + + public int getRegion() { + if (mRegion == NativeLibrary.GameRegion.Invalid) { + mRegion = NativeLibrary.GetAppRegion(mPath); + } + return mRegion; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlay.java new file mode 100644 index 00000000000..afa220a93ba --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlay.java @@ -0,0 +1,532 @@ +package org.citra.emu.overlay; + +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.BitmapDrawable; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; +import java.util.ArrayList; +import org.citra.emu.NativeLibrary.ButtonType; +import org.citra.emu.R; + +public final class InputOverlay extends SurfaceView implements View.OnTouchListener { + public static final String PREF_CONTROLLER_INIT = "InitOverlay"; + public static final String PREF_JOYSTICK_RELATIVE = "JoystickRelative"; + public static final String PREF_CONTROLLER_SCALE = "ControllerScale"; + public static final String PREF_CONTROLLER_ALPHA = "ControllerAlpha"; + + public static int sControllerScale = 50; + public static int sControllerAlpha = 100; + public static boolean sJoystickRelative = true; + public static boolean sEmulateMotionByTouch = false; + + private final ArrayList mButtons = new ArrayList<>(); + private final ArrayList mDpads = new ArrayList<>(); + private final ArrayList mJoysticks = new ArrayList<>(); + private boolean mIsLandscape = false; + private boolean mInEditMode = false; + private InputOverlayButton mButtonBeingConfigured; + private InputOverlayDpad mDpadBeingConfigured; + private InputOverlayJoystick mJoystickBeingConfigured; + private InputOverlayPointer mOverlayPointer = null; + + private SharedPreferences mPreferences; + + public InputOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + setOnTouchListener(this); + setWillNotDraw(false); + requestFocus(); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (!mPreferences.getBoolean(PREF_CONTROLLER_INIT, false)) { + defaultOverlay(); + } + sJoystickRelative = mPreferences.getBoolean(InputOverlay.PREF_JOYSTICK_RELATIVE, true); + sControllerScale = mPreferences.getInt(InputOverlay.PREF_CONTROLLER_SCALE, 50); + sControllerAlpha = mPreferences.getInt(InputOverlay.PREF_CONTROLLER_ALPHA, 100); + + // Load the controls. + refreshControls(); + } + + private void defaultOverlay() { + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + Resources res = getResources(); + int[][] buttons = new int[][] { + {ButtonType.N3DS_BUTTON_A, R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y}, + {ButtonType.N3DS_BUTTON_B, R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y}, + {ButtonType.N3DS_BUTTON_X, R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y}, + {ButtonType.N3DS_BUTTON_Y, R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y}, + {ButtonType.N3DS_BUTTON_START, R.integer.BUTTON_START_X, R.integer.BUTTON_START_Y}, + {ButtonType.N3DS_BUTTON_SELECT, R.integer.BUTTON_SELECT_X, R.integer.BUTTON_SELECT_Y}, + {ButtonType.N3DS_BUTTON_ZL, R.integer.TRIGGER_L_X, R.integer.TRIGGER_L_Y}, + {ButtonType.N3DS_BUTTON_ZR, R.integer.TRIGGER_R_X, R.integer.TRIGGER_R_Y}, + {ButtonType.N3DS_BUTTON_L, R.integer.TRIGGER_L_X, R.integer.TRIGGER_L_Y}, + {ButtonType.N3DS_BUTTON_R, R.integer.TRIGGER_R_X, R.integer.TRIGGER_R_Y}, + {ButtonType.N3DS_DPAD_UP, R.integer.PAD_MAIN_X, R.integer.PAD_MAIN_Y}, + {ButtonType.N3DS_CPAD_X, R.integer.STICK_MAIN_X, R.integer.STICK_MAIN_Y}, + }; + + for (int i = 0; i < buttons.length; ++i) { + int id = buttons[i][0]; + int x = buttons[i][1]; + int y = buttons[i][2]; + float posX = res.getInteger(x) / 100.0f; + float posY = res.getInteger(y) / 100.0f; + sPrefsEditor.putFloat(id + "_X", posX); + sPrefsEditor.putFloat(id + "_Y", posY); + sPrefsEditor.putFloat(id + "_XX", posX); + sPrefsEditor.putFloat(id + "_YY", posY); + } + + sPrefsEditor.putBoolean(PREF_CONTROLLER_INIT, true); + sPrefsEditor.apply(); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + for (InputOverlayButton button : mButtons) { + button.onDraw(canvas); + } + + for (InputOverlayDpad dpad : mDpads) { + dpad.onDraw(canvas); + } + + for (InputOverlayJoystick joystick : mJoysticks) { + joystick.onDraw(canvas); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isInEditMode()) { + return onTouchWhileEditing(event); + } + + boolean isProcessed = false; + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + int pointerIndex = event.getActionIndex(); + int pointerId = event.getPointerId(pointerIndex); + float pointerX = event.getX(pointerIndex); + float pointerY = event.getY(pointerIndex); + + for (InputOverlayJoystick joystick : mJoysticks) { + if (joystick.getBounds().contains((int)pointerX, (int)pointerY)) { + joystick.onPointerDown(pointerId, pointerX, pointerY); + isProcessed = true; + break; + } + } + + for (InputOverlayButton button : mButtons) { + if (button.getBounds().contains((int)pointerX, (int)pointerY)) { + button.onPointerDown(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + + for (InputOverlayDpad dpad : mDpads) { + if (dpad.getBounds().contains((int)pointerX, (int)pointerY)) { + dpad.onPointerDown(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + + if (!isProcessed && mOverlayPointer != null && mOverlayPointer.getPointerId() == -1) + mOverlayPointer.onPointerDown(pointerId, pointerX, pointerY); + break; + } + case MotionEvent.ACTION_MOVE: { + int pointerCount = event.getPointerCount(); + for (int i = 0; i < pointerCount; ++i) { + boolean isCaptured = false; + int pointerId = event.getPointerId(i); + float pointerX = event.getX(i); + float pointerY = event.getY(i); + + for (InputOverlayJoystick joystick : mJoysticks) { + if (joystick.getPointerId() == pointerId) { + joystick.onPointerMove(pointerId, pointerX, pointerY); + isCaptured = true; + isProcessed = true; + break; + } + } + if (isCaptured) + continue; + + for (InputOverlayButton button : mButtons) { + if (button.getBounds().contains((int)pointerX, (int)pointerY)) { + if (button.getPointerId() == -1) { + button.onPointerDown(pointerId, pointerX, pointerY); + isProcessed = true; + } + } else if (button.getPointerId() == pointerId) { + button.onPointerUp(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + + for (InputOverlayDpad dpad : mDpads) { + if (dpad.getPointerId() == pointerId) { + dpad.onPointerMove(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + + if (mOverlayPointer != null && mOverlayPointer.getPointerId() == pointerId) { + mOverlayPointer.onPointerMove(pointerId, pointerX, pointerY); + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + int pointerIndex = event.getActionIndex(); + int pointerId = event.getPointerId(pointerIndex); + float pointerX = event.getX(pointerIndex); + float pointerY = event.getY(pointerIndex); + + if (mOverlayPointer != null && mOverlayPointer.getPointerId() == pointerId) { + mOverlayPointer.onPointerUp(pointerId, pointerX, pointerY); + } + + for (InputOverlayJoystick joystick : mJoysticks) { + if (joystick.getPointerId() == pointerId) { + joystick.onPointerUp(pointerId, pointerX, pointerY); + isProcessed = true; + break; + } + } + + for (InputOverlayButton button : mButtons) { + if (button.getPointerId() == pointerId) { + button.onPointerUp(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + + for (InputOverlayDpad dpad : mDpads) { + if (dpad.getPointerId() == pointerId) { + dpad.onPointerUp(pointerId, pointerX, pointerY); + isProcessed = true; + } + } + break; + } + + case MotionEvent.ACTION_CANCEL: { + isProcessed = true; + if (mOverlayPointer != null) { + mOverlayPointer.onPointerUp(0, 0, 0); + } + + for (InputOverlayJoystick joystick : mJoysticks) { + joystick.onPointerUp(0, 0, 0); + } + + for (InputOverlayButton button : mButtons) { + button.onPointerUp(0, 0, 0); + } + + for (InputOverlayDpad dpad : mDpads) { + dpad.onPointerUp(0, 0, 0); + } + break; + } + } + + if (isProcessed) + invalidate(); + + return true; + } + + public boolean onTouchWhileEditing(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int pointerX = (int)event.getX(pointerIndex); + int pointerY = (int)event.getY(pointerIndex); + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (mButtonBeingConfigured != null || mDpadBeingConfigured != null || + mJoystickBeingConfigured != null) + return false; + for (InputOverlayButton button : mButtons) { + if (mButtonBeingConfigured == null && + button.getBounds().contains(pointerX, pointerY)) { + mButtonBeingConfigured = button; + mButtonBeingConfigured.onConfigureBegin(pointerX, pointerY); + return true; + } + } + + for (InputOverlayDpad dpad : mDpads) { + if (mDpadBeingConfigured == null && dpad.getBounds().contains(pointerX, pointerY)) { + mDpadBeingConfigured = dpad; + mDpadBeingConfigured.onConfigureBegin(pointerX, pointerY); + return true; + } + } + + for (InputOverlayJoystick joystick : mJoysticks) { + if (mJoystickBeingConfigured == null && + joystick.getBounds().contains(pointerX, pointerY)) { + mJoystickBeingConfigured = joystick; + mJoystickBeingConfigured.onConfigureBegin(pointerX, pointerY); + return true; + } + } + break; + case MotionEvent.ACTION_MOVE: + if (mButtonBeingConfigured != null) { + mButtonBeingConfigured.onConfigureMove(pointerX, pointerY); + invalidate(); + return true; + } + if (mDpadBeingConfigured != null) { + mDpadBeingConfigured.onConfigureMove(pointerX, pointerY); + invalidate(); + return true; + } + if (mJoystickBeingConfigured != null) { + mJoystickBeingConfigured.onConfigureMove(pointerX, pointerY); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mButtonBeingConfigured != null) { + saveControlPosition(mButtonBeingConfigured.getButtonId(), + mButtonBeingConfigured.getBounds()); + mButtonBeingConfigured = null; + return true; + } + if (mDpadBeingConfigured != null) { + saveControlPosition(mDpadBeingConfigured.getButtonId(), + mDpadBeingConfigured.getBounds()); + mDpadBeingConfigured = null; + return true; + } + if (mJoystickBeingConfigured != null) { + saveControlPosition(mJoystickBeingConfigured.getButtonId(), + mJoystickBeingConfigured.getBounds()); + mJoystickBeingConfigured = null; + return true; + } + break; + } + + return false; + } + + private void saveControlPosition(int buttonId, Rect bounds) { + final DisplayMetrics dm = getResources().getDisplayMetrics(); + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + float x = + (bounds.left + (bounds.right - bounds.left) / 2.0f) / dm.widthPixels * 2.0f - 1.0f; + float y = + (bounds.top + (bounds.bottom - bounds.top) / 2.0f) / dm.heightPixels * 2.0f - 1.0f; + sPrefsEditor.putFloat(buttonId + (mIsLandscape ? "_XX" : "_X"), x); + sPrefsEditor.putFloat(buttonId + (mIsLandscape ? "_YY" : "_Y"), y); + sPrefsEditor.apply(); + } + + public void refreshControls() { + // Remove all the overlay buttons + mIsLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + mButtons.clear(); + mDpads.clear(); + mJoysticks.clear(); + + int i = 0; + int[][] buttons = new int[][] { + {ButtonType.N3DS_BUTTON_A, R.drawable.classic_a, R.drawable.classic_a_pressed}, + {ButtonType.N3DS_BUTTON_B, R.drawable.classic_b, R.drawable.classic_b_pressed}, + {ButtonType.N3DS_BUTTON_X, R.drawable.classic_x, R.drawable.classic_x_pressed}, + {ButtonType.N3DS_BUTTON_Y, R.drawable.classic_y, R.drawable.classic_y_pressed}, + {ButtonType.N3DS_BUTTON_START, R.drawable.gcpad_start, R.drawable.gcpad_start_pressed}, + {ButtonType.N3DS_BUTTON_SELECT, R.drawable.wiimote_plus, + R.drawable.wiimote_plus_pressed}, + {ButtonType.N3DS_BUTTON_L, R.drawable.classic_l, R.drawable.classic_l_pressed}, + {ButtonType.N3DS_BUTTON_R, R.drawable.classic_r, R.drawable.classic_r_pressed}, + }; + + for (; i < buttons.length; ++i) { + int id = buttons[i][0]; + int normal = buttons[i][1]; + int pressed = buttons[i][2]; + mButtons.add(initializeButton(normal, pressed, id)); + } + + // mDpads.add(initializeDpad(ButtonType.N3DS_CPAD_X)); + mDpads.add(initializeDpad(ButtonType.N3DS_DPAD_UP)); + mJoysticks.add(initializeJoystick(ButtonType.N3DS_CPAD_X)); + mOverlayPointer = new InputOverlayPointer(); + } + + private InputOverlayButton initializeButton(int defaultResId, int pressedResId, int buttonId) { + final Resources res = getResources(); + final DisplayMetrics dm = res.getDisplayMetrics(); + float scale = 0.14f * (sControllerScale + 50) / 100; + + switch (buttonId) { + case ButtonType.N3DS_BUTTON_L: + case ButtonType.N3DS_BUTTON_R: + scale *= 1.7f; + break; + case ButtonType.N3DS_BUTTON_ZL: + case ButtonType.N3DS_BUTTON_ZR: + scale *= 1.2f; + break; + + case ButtonType.N3DS_BUTTON_START: + case ButtonType.N3DS_BUTTON_SELECT: + scale *= 0.8f; + break; + } + + Bitmap defaultBitmap = resizeBitmap(BitmapFactory.decodeResource(res, defaultResId), scale); + Bitmap pressedBitmap = resizeBitmap(BitmapFactory.decodeResource(res, pressedResId), scale); + InputOverlayButton overlay = + new InputOverlayButton(new BitmapDrawable(res, defaultBitmap), + new BitmapDrawable(res, pressedBitmap), buttonId); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + float x = mPreferences.getFloat(buttonId + (mIsLandscape ? "_XX" : "_X"), 0f); + float y = mPreferences.getFloat(buttonId + (mIsLandscape ? "_YY" : "_Y"), 0.5f); + + int width = defaultBitmap.getWidth(); + int height = defaultBitmap.getHeight(); + int drawableX = (int)((dm.widthPixels / 2.0f) * (1.0f + x) - width / 2.0f); + int drawableY = (int)((dm.heightPixels / 2.0f) * (1.0f + y) - height / 2.0f); + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the + // InputOverlayDrawableButton will be. + overlay.setBounds(new Rect(drawableX, drawableY, drawableX + width, drawableY + height)); + + // Need to set the image's position + overlay.setPosition(drawableX, drawableY); + overlay.setAlpha((sControllerAlpha * 255) / 100); + + return overlay; + } + + private InputOverlayDpad initializeDpad(int dpadId) { + final Resources res = getResources(); + final DisplayMetrics dm = res.getDisplayMetrics(); + final int defaultResId = R.drawable.gcwii_dpad; + final int pressedOneDirectionResId = R.drawable.gcwii_dpad_pressed_one_direction; + final int pressedTwoDirectionsResId = R.drawable.gcwii_dpad_pressed_two_directions; + float scale = 0.3f * (sControllerScale + 50) / 100; + + Bitmap defaultBitmap = resizeBitmap(BitmapFactory.decodeResource(res, defaultResId), scale); + Bitmap onePressedBitmap = + resizeBitmap(BitmapFactory.decodeResource(res, pressedOneDirectionResId), scale); + Bitmap twoPressedBitmap = + resizeBitmap(BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), scale); + InputOverlayDpad overlay = new InputOverlayDpad( + new BitmapDrawable(res, defaultBitmap), new BitmapDrawable(res, onePressedBitmap), + new BitmapDrawable(res, twoPressedBitmap), dpadId); + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + float x = mPreferences.getFloat(dpadId + (mIsLandscape ? "_XX" : "_X"), 0f); + float y = mPreferences.getFloat(dpadId + (mIsLandscape ? "_YY" : "_Y"), 0.5f); + + int width = defaultBitmap.getWidth(); + int height = defaultBitmap.getHeight(); + int drawableX = (int)((dm.widthPixels / 2.0f) * (1.0f + x) - width / 2.0f); + int drawableY = (int)((dm.heightPixels / 2.0f) * (1.0f + y) - height / 2.0f); + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the + // InputOverlayDrawableDpad will be. + overlay.setBounds(new Rect(drawableX, drawableY, drawableX + width, drawableY + height)); + + // Need to set the image's position + overlay.setPosition(drawableX, drawableY); + overlay.setAlpha((sControllerAlpha * 255) / 100); + + return overlay; + } + + private InputOverlayJoystick initializeJoystick(int joystick) { + final Resources res = getResources(); + final DisplayMetrics dm = res.getDisplayMetrics(); + final int resOuter = R.drawable.gcwii_joystick_range; + final int defaultResInner = R.drawable.gcwii_joystick; + final int pressedResInner = R.drawable.gcwii_joystick_pressed; + float scale = 0.275f * (sControllerScale + 50) / 100; + + // Initialize the InputOverlayDrawableJoystick. + final Bitmap bitmapOuter = resizeBitmap(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. + float x = mPreferences.getFloat(joystick + (mIsLandscape ? "_XX" : "_X"), -0.3f); + float y = mPreferences.getFloat(joystick + (mIsLandscape ? "_YY" : "_Y"), 0.3f); + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the + // InputOverlayDrawableJoystick will be. + float innerScale = 1.833f; + int outerSize = bitmapOuter.getWidth(); + int drawableX = (int)((dm.widthPixels / 2.0f) * (1.0f + x) - outerSize / 2.0f); + int drawableY = (int)((dm.heightPixels / 2.0f) * (1.0f + y) - outerSize / 2.0f); + Rect outerRect = + new Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize); + Rect innerRect = + new Rect(0, 0, (int)(outerSize / innerScale), (int)(outerSize / innerScale)); + + // Send the drawableId to the joystick so it can be referenced when saving control position. + InputOverlayJoystick overlay = new InputOverlayJoystick( + new BitmapDrawable(res, bitmapOuter), new BitmapDrawable(res, bitmapOuter), + new BitmapDrawable(res, bitmapInnerDefault), + new BitmapDrawable(res, bitmapInnerPressed), outerRect, innerRect, joystick); + + // Need to set the image's position + overlay.setPosition(drawableX, drawableY); + overlay.setAlpha((sControllerAlpha * 255) / 100); + + return overlay; + } + + public Bitmap resizeBitmap(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 = getResources().getDisplayMetrics(); + int dimension = (int)(Math.min(dm.widthPixels, dm.heightPixels) * scale); + return Bitmap.createScaledBitmap(bitmap, dimension, dimension, true); + } + + public boolean isInEditMode() { + return mInEditMode; + } + + public void setInEditMode(boolean mode) { + mInEditMode = mode; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayButton.java b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayButton.java new file mode 100644 index 00000000000..c9ec1cc5c3c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayButton.java @@ -0,0 +1,84 @@ +package org.citra.emu.overlay; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import org.citra.emu.NativeLibrary; + +public final class InputOverlayButton { + private BitmapDrawable mDefaultBitmap; + private BitmapDrawable mPressedBitmap; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mPointerId; + private int mButtonId; + + public InputOverlayButton(BitmapDrawable defaultBitmap, BitmapDrawable pressedBitmap, + int buttonId) { + mPointerId = -1; + mButtonId = buttonId; + mDefaultBitmap = defaultBitmap; + mPressedBitmap = pressedBitmap; + } + + public void onConfigureBegin(int x, int y) { + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onConfigureMove(int x, int y) { + Rect bounds = getBounds(); + mControlPositionX += x - mPreviousTouchX; + mControlPositionY += y - mPreviousTouchY; + setBounds(new Rect(mControlPositionX, mControlPositionY, mControlPositionX + bounds.width(), + mControlPositionY + bounds.height())); + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onDraw(Canvas canvas) { + getCurrentBitmapDrawable().draw(canvas); + } + + public void onPointerDown(int id, float x, float y) { + mPointerId = id; + NativeLibrary.InputEvent(mButtonId, NativeLibrary.ButtonState.PRESSED); + } + + public void onPointerMove(int id, float x, float y) {} + + public void onPointerUp(int id, float x, float y) { + mPointerId = -1; + NativeLibrary.InputEvent(mButtonId, NativeLibrary.ButtonState.RELEASED); + } + + private BitmapDrawable getCurrentBitmapDrawable() { + return mPointerId != -1 ? mPressedBitmap : mDefaultBitmap; + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void setAlpha(int value) { + mDefaultBitmap.setAlpha(value); + } + + public int getButtonId() { + return mButtonId; + } + + public int getPointerId() { + return mPointerId; + } + + public Rect getBounds() { + return mDefaultBitmap.getBounds(); + } + + public void setBounds(Rect bounds) { + mDefaultBitmap.setBounds(bounds); + mPressedBitmap.setBounds(bounds); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayDpad.java b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayDpad.java new file mode 100644 index 00000000000..23e9b075774 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayDpad.java @@ -0,0 +1,214 @@ +package org.citra.emu.overlay; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import org.citra.emu.NativeLibrary; + +public final class InputOverlayDpad { + private boolean[] mPressStates = new boolean[4]; + private int[] mButtonIds = new int[4]; + private boolean mIsStick; + private BitmapDrawable mDefaultBitmap; + private BitmapDrawable mOnePressedBitmap; + private BitmapDrawable mTwoPressedBitmap; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mPointerId; + + public InputOverlayDpad(BitmapDrawable defaultBitmap, BitmapDrawable onePressedBitmap, + BitmapDrawable twoPressedBitmap, int buttonId) { + mPointerId = -1; + mDefaultBitmap = defaultBitmap; + mOnePressedBitmap = onePressedBitmap; + mTwoPressedBitmap = twoPressedBitmap; + + mIsStick = NativeLibrary.ButtonType.N3DS_DPAD_UP != buttonId; + + mButtonIds[0] = buttonId + 0; + mButtonIds[1] = buttonId + 1; + mButtonIds[2] = buttonId + 2; + mButtonIds[3] = buttonId + 3; + + mPressStates[0] = false; + mPressStates[1] = false; + mPressStates[2] = false; + mPressStates[3] = false; + } + + public void onConfigureBegin(int x, int y) { + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onConfigureMove(int x, int y) { + Rect bounds = getBounds(); + mControlPositionX += x - mPreviousTouchX; + mControlPositionY += y - mPreviousTouchY; + setBounds(new Rect(mControlPositionX, mControlPositionY, mControlPositionX + bounds.width(), + mControlPositionY + bounds.height())); + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onDraw(Canvas canvas) { + Rect bounds = getBounds(); + int px = mControlPositionX + (bounds.width() / 2); + int py = mControlPositionY + (bounds.height() / 2); + + boolean up = mPressStates[0]; + boolean down = mPressStates[1]; + boolean left = mPressStates[2]; + boolean right = mPressStates[3]; + + if (up) { + if (left) + mTwoPressedBitmap.draw(canvas); + else if (right) { + canvas.save(); + canvas.rotate(90, px, py); + mTwoPressedBitmap.draw(canvas); + canvas.restore(); + } else + mOnePressedBitmap.draw(canvas); + } else if (down) { + if (left) { + canvas.save(); + canvas.rotate(270, px, py); + mTwoPressedBitmap.draw(canvas); + canvas.restore(); + } else if (right) { + canvas.save(); + canvas.rotate(180, px, py); + mTwoPressedBitmap.draw(canvas); + canvas.restore(); + } else { + canvas.save(); + canvas.rotate(180, px, py); + mOnePressedBitmap.draw(canvas); + canvas.restore(); + } + } else if (left) { + canvas.save(); + canvas.rotate(270, px, py); + mOnePressedBitmap.draw(canvas); + canvas.restore(); + } else if (right) { + canvas.save(); + canvas.rotate(90, px, py); + mOnePressedBitmap.draw(canvas); + canvas.restore(); + } else { + mDefaultBitmap.draw(canvas); + } + } + + public void onPointerDown(int id, float x, float y) { + mPointerId = id; + if (mIsStick) + setDpadState2((int)x, (int)y); + else + setDpadState4((int)x, (int)y); + } + + public void onPointerMove(int id, float x, float y) { + if (mIsStick) + setDpadState2((int)x, (int)y); + else + setDpadState4((int)x, (int)y); + } + + public void onPointerUp(int id, float x, float y) { + mPointerId = -1; + if (mIsStick) + setDpadState2((int)x, (int)y); + else + setDpadState4((int)x, (int)y); + } + + private void setDpadState2(int pointerX, int pointerY) { + // x, y + float[] axises = {0f, 0f}; + // Up, Down, Left, Right + mPressStates[0] = false; + mPressStates[1] = false; + mPressStates[2] = false; + mPressStates[3] = false; + + if (mPointerId != -1) { + Rect bounds = getBounds(); + if (bounds.top + (bounds.height() / 3) > pointerY) { + axises[1] = 1.0f; + mPressStates[0] = true; + } else if (bounds.bottom - (bounds.height() / 3) < pointerY) { + axises[1] = -1.0f; + mPressStates[1] = true; + } + + if (bounds.left + (bounds.width() / 3) > pointerX) { + axises[0] = -1.0f; + mPressStates[2] = true; + } else if (bounds.right - (bounds.width() / 3) < pointerX) { + axises[0] = 1.0f; + mPressStates[3] = true; + } + } + + NativeLibrary.InputEvent(mButtonIds[0], axises[0]); + NativeLibrary.InputEvent(mButtonIds[1], axises[1]); + } + + private void setDpadState4(int pointerX, int pointerY) { + // Up, Down, Left, Right + boolean[] pressed = {false, false, false, false}; + + if (mPointerId != -1) { + Rect bounds = getBounds(); + + if (bounds.top + (bounds.height() / 3) > pointerY) + pressed[0] = true; + if (bounds.bottom - (bounds.height() / 3) < pointerY) + pressed[1] = true; + if (bounds.left + (bounds.width() / 3) > pointerX) + pressed[2] = true; + if (bounds.right - (bounds.width() / 3) < pointerX) + pressed[3] = true; + } + + for (int i = 0; i < pressed.length; ++i) { + if (pressed[i] != mPressStates[i]) { + NativeLibrary.InputEvent(mButtonIds[i], pressed[i] + ? NativeLibrary.ButtonState.PRESSED + : NativeLibrary.ButtonState.RELEASED); + mPressStates[i] = pressed[i]; + } + } + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void setAlpha(int value) { + mDefaultBitmap.setAlpha(value); + } + + public int getButtonId() { + return mButtonIds[0]; + } + + public int getPointerId() { + return mPointerId; + } + + public Rect getBounds() { + return mDefaultBitmap.getBounds(); + } + + public void setBounds(Rect bounds) { + mDefaultBitmap.setBounds(bounds); + mOnePressedBitmap.setBounds(bounds); + mTwoPressedBitmap.setBounds(bounds); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayJoystick.java b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayJoystick.java new file mode 100644 index 00000000000..856caad4d24 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayJoystick.java @@ -0,0 +1,173 @@ +package org.citra.emu.overlay; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import org.citra.emu.NativeLibrary; + +public final class InputOverlayJoystick { + private final int[] mAxisIDs = {0, 0}; + private final float[] mAxises = {0f, 0f}; + + private BitmapDrawable mOuterBitmap; + private BitmapDrawable mDefaultInnerBitmap; + private BitmapDrawable mPressedInnerBitmap; + private BitmapDrawable mBoundsBoxBitmap; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mAlpha; + private int mPointerId; + private Rect mVirtBounds; + private Rect mOrigBounds; + + public InputOverlayJoystick(BitmapDrawable bitmapBounds, BitmapDrawable bitmapOuter, + BitmapDrawable InnerDefault, BitmapDrawable InnerPressed, + Rect rectOuter, Rect rectInner, int buttonId) { + mPointerId = -1; + mOuterBitmap = bitmapOuter; + mDefaultInnerBitmap = InnerDefault; + mPressedInnerBitmap = InnerPressed; + mBoundsBoxBitmap = bitmapBounds; + + mAxisIDs[0] = buttonId + 0; + mAxisIDs[1] = buttonId + 1; + + setBounds(rectOuter); + mDefaultInnerBitmap.setBounds(rectInner); + mPressedInnerBitmap.setBounds(rectInner); + mVirtBounds = mOuterBitmap.copyBounds(); + mOrigBounds = mOuterBitmap.copyBounds(); + mBoundsBoxBitmap.setAlpha(0); + mBoundsBoxBitmap.setBounds(mVirtBounds); + updateInnerBounds(); + } + + public void onConfigureBegin(int x, int y) { + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onConfigureMove(int x, int y) { + int deltaX = x - mPreviousTouchX; + int deltaY = y - mPreviousTouchY; + mControlPositionX += deltaX; + mControlPositionY += deltaY; + setBounds(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() + mControlPositionY)); + mVirtBounds.set(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() + mControlPositionY); + updateInnerBounds(); + mOrigBounds.set(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() + mControlPositionY); + mPreviousTouchX = x; + mPreviousTouchY = y; + } + + public void onDraw(Canvas canvas) { + mOuterBitmap.draw(canvas); + getCurrentBitmapDrawable().draw(canvas); + mBoundsBoxBitmap.draw(canvas); + } + + public void onPointerDown(int id, float x, float y) { + mOuterBitmap.setAlpha(0); + mBoundsBoxBitmap.setAlpha(mAlpha); + if (InputOverlay.sJoystickRelative) { + // reCenter + mVirtBounds.offset((int)x - mVirtBounds.centerX(), (int)y - mVirtBounds.centerY()); + } + mBoundsBoxBitmap.setBounds(mVirtBounds); + mPointerId = id; + + setJoystickState(x, y); + } + + public void onPointerMove(int id, float x, float y) { + setJoystickState(x, y); + } + + public void onPointerUp(int id, float x, float y) { + mOuterBitmap.setAlpha(mAlpha); + mBoundsBoxBitmap.setAlpha(0); + mVirtBounds = new Rect(mOrigBounds); + setBounds(mOrigBounds); + mPointerId = -1; + + setJoystickState(x, y); + } + + private void setJoystickState(float touchX, float touchY) { + if (mPointerId != -1) { + float maxY = mVirtBounds.bottom; + float maxX = mVirtBounds.right; + touchX -= mVirtBounds.centerX(); + maxX -= mVirtBounds.centerX(); + touchY -= mVirtBounds.centerY(); + maxY -= mVirtBounds.centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY / maxY; + mAxises[0] = AxisX; + mAxises[1] = -AxisY; + } else { + mAxises[0] = mAxises[1] = 0.0f; + } + + updateInnerBounds(); + + NativeLibrary.InputEvent(mAxisIDs[0], mAxises[0]); + NativeLibrary.InputEvent(mAxisIDs[1], mAxises[1]); + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void setAlpha(int value) { + mAlpha = value; + mDefaultInnerBitmap.setAlpha(value); + mOuterBitmap.setAlpha(value); + } + + public int getButtonId() { + return mAxisIDs[0]; + } + + public int getPointerId() { + return mPointerId; + } + + public Rect getBounds() { + return mOuterBitmap.getBounds(); + } + + public void setBounds(Rect bounds) { + mOuterBitmap.setBounds(bounds); + } + + private BitmapDrawable getCurrentBitmapDrawable() { + return mPointerId != -1 ? mPressedInnerBitmap : mDefaultInnerBitmap; + } + + private void updateInnerBounds() { + int X = mVirtBounds.centerX() + (int)((mAxises[0]) * (mVirtBounds.width() / 2)); + int Y = mVirtBounds.centerY() + (int)((-mAxises[1]) * (mVirtBounds.height() / 2)); + + if (X > mVirtBounds.centerX() + (mVirtBounds.width() / 2)) + X = mVirtBounds.centerX() + (mVirtBounds.width() / 2); + if (X < mVirtBounds.centerX() - (mVirtBounds.width() / 2)) + X = mVirtBounds.centerX() - (mVirtBounds.width() / 2); + if (Y > mVirtBounds.centerY() + (mVirtBounds.height() / 2)) + Y = mVirtBounds.centerY() + (mVirtBounds.height() / 2); + if (Y < mVirtBounds.centerY() - (mVirtBounds.height() / 2)) + Y = mVirtBounds.centerY() - (mVirtBounds.height() / 2); + + int width = mPressedInnerBitmap.getBounds().width() / 2; + int height = mPressedInnerBitmap.getBounds().height() / 2; + mDefaultInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); + mPressedInnerBitmap.setBounds(mDefaultInnerBitmap.getBounds()); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayPointer.java b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayPointer.java new file mode 100644 index 00000000000..904ff4a52d4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/overlay/InputOverlayPointer.java @@ -0,0 +1,41 @@ +package org.citra.emu.overlay; + +import org.citra.emu.NativeLibrary; + +public final class InputOverlayPointer { + private int mPointerId; + + public InputOverlayPointer() { + mPointerId = -1; + } + + public void onPointerDown(int id, float x, float y) { + mPointerId = id; + int action = NativeLibrary.TouchEvent.TOUCH_PRESSED; + if (InputOverlay.sEmulateMotionByTouch) { + action |= NativeLibrary.TouchEvent.BEGIN_TILT; + } + NativeLibrary.TouchEvent(action, (int)x, (int)y); + } + + public void onPointerMove(int id, float x, float y) { + int action = NativeLibrary.TouchEvent.TOUCH_MOVED; + if (InputOverlay.sEmulateMotionByTouch) { + action |= NativeLibrary.TouchEvent.TILT; + } + NativeLibrary.TouchEvent(action, (int)x, (int)y); + } + + public void onPointerUp(int id, float x, float y) { + mPointerId = -1; + int action = NativeLibrary.TouchEvent.TOUCH_RELEASED; + if (InputOverlay.sEmulateMotionByTouch) { + action |= NativeLibrary.TouchEvent.END_TILT; + } + NativeLibrary.TouchEvent(action, (int)x, (int)y); + } + + public int getPointerId() { + return mPointerId; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/MenuTag.java b/src/android/app/src/main/java/org/citra/emu/settings/MenuTag.java new file mode 100644 index 00000000000..a8ebbfe9a0f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/MenuTag.java @@ -0,0 +1,62 @@ +package org.citra.emu.settings; + +import android.support.annotation.Nullable; + +public enum MenuTag { + CONFIG("Config"), + INPUT("Input"); + + private String mTag; + private int mSubType; + + MenuTag(String tag) { + mTag = tag; + mSubType = -1; + } + + MenuTag(String tag, int subtype) { + mTag = tag; + mSubType = subtype; + } + + @Nullable + public static MenuTag getMenuTag(String menuTagStr) { + if (menuTagStr == null || menuTagStr.isEmpty()) { + return null; + } + String tag = menuTagStr; + int subtype = -1; + int sep = menuTagStr.indexOf('|'); + if (sep != -1) { + tag = menuTagStr.substring(0, sep); + subtype = Integer.parseInt(menuTagStr.substring(sep + 1)); + } + return getMenuTag(tag, subtype); + } + + private static MenuTag getMenuTag(String tag, int subtype) { + for (MenuTag menuTag : MenuTag.values()) { + if (menuTag.mTag.equals(tag) && menuTag.mSubType == subtype) + return menuTag; + } + + throw new IllegalArgumentException("You are asking for a menu that is not available or " + + "passing a wrong subtype"); + } + + @Override + public String toString() { + if (mSubType != -1) { + return mTag + "|" + mSubType; + } + return mTag; + } + + public String getTag() { + return mTag; + } + + public int getSubType() { + return mSubType; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/emu/settings/MotionAlertDialog.java new file mode 100644 index 00000000000..47f01c55765 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/MotionAlertDialog.java @@ -0,0 +1,135 @@ +package org.citra.emu.settings; + +import android.app.AlertDialog; +import android.content.Context; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import java.util.ArrayList; +import java.util.List; +import org.citra.emu.settings.view.InputBindingSetting; +import org.citra.emu.utils.ControllerMappingHelper; + +/** + * {@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 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.d("zhangwei", "[MotionAlertDialog] Received key event: " + event.getAction()); + switch (event.getAction()) { + case KeyEvent.ACTION_UP: + if (!ControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode)) { + 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 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(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 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 = 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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/Settings.java b/src/android/app/src/main/java/org/citra/emu/settings/Settings.java new file mode 100644 index 00000000000..7f94d489b2f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/Settings.java @@ -0,0 +1,52 @@ +package org.citra.emu.settings; + +import java.util.HashMap; +import org.citra.emu.settings.model.SettingSection; + +public final class Settings { + + public static final String SECTION_INI_CORE = "Core"; + public static final String SECTION_INI_RENDERER = "Renderer"; + public static final String SECTION_INI_AUDIO = "Audio"; + public static final String SECTION_INI_CONTROLS = "Controls"; + private HashMap mSections = new SettingsSectionMap(); + + public SettingSection getSection(String sectionName) { + return mSections.get(sectionName); + } + + public boolean isEmpty() { + return mSections.isEmpty(); + } + + public void loadSettings(String gameId) { + mSections = new SettingsSectionMap(); + mSections.putAll(SettingsFile.loadSettings(gameId)); + } + + public void saveSettings() { + SettingsFile.saveFile(mSections); + } + + /** + * A HashMap 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 { + @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); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/SettingsActivity.java b/src/android/app/src/main/java/org/citra/emu/settings/SettingsActivity.java new file mode 100644 index 00000000000..52968744eaf --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/SettingsActivity.java @@ -0,0 +1,138 @@ +package org.citra.emu.settings; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AppCompatActivity; +import org.citra.emu.R; +import org.citra.emu.settings.model.Setting; + +public final class SettingsActivity extends AppCompatActivity { + + 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 static final String KEY_SHOULD_SAVE = "should_save"; + private static final String KEY_MENU_TAG = "menu_tag"; + private static final String KEY_GAME_ID = "game_id"; + + private Settings mSettings = new Settings(); + private int mStackCount; + private boolean mShouldSave; + private MenuTag mMenuTag; + private String mGameId; + + public static void launch(Context context, MenuTag 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); + + if (savedInstanceState == null) { + Intent intent = getIntent(); + mMenuTag = (MenuTag)intent.getSerializableExtra(ARG_MENU_TAG); + mGameId = intent.getStringExtra(ARG_GAME_ID); + } else { + String menuTagStr = savedInstanceState.getString(KEY_MENU_TAG); + mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); + mMenuTag = MenuTag.getMenuTag(menuTagStr); + mGameId = savedInstanceState.getString(KEY_GAME_ID); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + // Critical: If super method is not called, rotations will be busted. + super.onSaveInstanceState(outState); + + outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); + outState.putString(KEY_MENU_TAG, mMenuTag.toString()); + outState.putString(KEY_GAME_ID, mGameId); + } + + @Override + protected void onStart() { + super.onStart(); + + if (mSettings.isEmpty()) { + mSettings.loadSettings(mGameId); + } + showSettingsFragment(mMenuTag, null, false, mGameId); + } + + @Override + protected void onStop() { + super.onStop(); + + if (mSettings != null && isFinishing() && mShouldSave) { + mSettings.saveSettings(); + } + } + + public void showSettingsFragment(MenuTag menuTag, Bundle extras, 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); + mStackCount++; + } + transaction.replace(R.id.frame_content, + SettingsFragment.newInstance(menuTag, gameID, extras), FRAGMENT_TAG); + transaction.commit(); + + // show settings + SettingsFragment fragment = getSettingsFragment(); + if (fragment != null) { + fragment.showSettingsList(mSettings); + } + } + + private boolean areSystemAnimationsEnabled() { + float duration = android.provider.Settings.Global.getFloat( + getContentResolver(), android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1); + + float transition = android.provider.Settings.Global.getFloat( + getContentResolver(), android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1); + + return duration != 0 && transition != 0; + } + + private SettingsFragment getSettingsFragment() { + return (SettingsFragment)getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); + } + + public Settings getSettings() { + return mSettings; + } + + public void setSettings(Settings settings) { + mSettings = settings; + } + + public void putSetting(Setting setting) { + mSettings.getSection(setting.getSection()).putSetting(setting); + } + + public void loadSubMenu(MenuTag menuKey) { + showSettingsFragment(menuKey, null, true, mGameId); + } + + public void setSettingChanged() { + mShouldSave = true; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/SettingsAdapter.java b/src/android/app/src/main/java/org/citra/emu/settings/SettingsAdapter.java new file mode 100644 index 00000000000..10221c74b62 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/SettingsAdapter.java @@ -0,0 +1,331 @@ +package org.citra.emu.settings; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import java.util.ArrayList; +import org.citra.emu.R; +import org.citra.emu.settings.model.BooleanSetting; +import org.citra.emu.settings.model.IntSetting; +import org.citra.emu.settings.model.Setting; +import org.citra.emu.settings.model.StringSetting; +import org.citra.emu.settings.view.CheckBoxSetting; +import org.citra.emu.settings.view.InputBindingSetting; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SingleChoiceSetting; +import org.citra.emu.settings.view.SliderSetting; +import org.citra.emu.settings.view.StringSingleChoiceSetting; +import org.citra.emu.settings.view.SubmenuSetting; +import org.citra.emu.settings.viewholder.CheckBoxSettingViewHolder; +import org.citra.emu.settings.viewholder.HeaderViewHolder; +import org.citra.emu.settings.viewholder.InputBindingSettingViewHolder; +import org.citra.emu.settings.viewholder.SeekbarViewHolder; +import org.citra.emu.settings.viewholder.SettingViewHolder; +import org.citra.emu.settings.viewholder.SingleChoiceViewHolder; +import org.citra.emu.settings.viewholder.SliderViewHolder; +import org.citra.emu.settings.viewholder.SubmenuViewHolder; + +public final class SettingsAdapter extends RecyclerView.Adapter + implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener { + private SettingsActivity mActivity; + private ArrayList mSettings; + + private SettingsItem mClickedItem; + private int mClickedPosition; + private int mSeekbarProgress; + + private AlertDialog mDialog; + private TextView mTextSliderValue; + + public SettingsAdapter(SettingsActivity activity) { + mActivity = activity; + 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_STRING_SINGLE_CHOICE: + case SettingsItem.TYPE_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); + + case SettingsItem.TYPE_SEEKBAR: + view = inflater.inflate(R.layout.list_item_setting_seekbar, parent, false); + return new SeekbarViewHolder(view, this); + + default: + Log.e("zhangwei", "[SettingsAdapter] Invalid view type: " + viewType); + return null; + } + } + + @Override + public void onBindViewHolder(SettingViewHolder holder, int position) { + holder.bind(mSettings.get(position)); + } + + @Override + public int getItemCount() { + if (mSettings != null) { + return mSettings.size(); + } else { + return 0; + } + } + + @Override + public int getItemViewType(int position) { + return mSettings.get(position).getType(); + } + + public String getSettingSection(int position) { + return mSettings.get(position).getSection(); + } + + public void setSettings(ArrayList settings) { + mSettings = settings; + notifyDataSetChanged(); + } + + public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { + BooleanSetting setting = item.setChecked(checked); + if (setting != null) { + mActivity.putSetting(setting); + } + mActivity.setSettingChanged(); + } + + public void onSingleChoiceClick(SingleChoiceSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + + int value = getSelectionForSingleChoiceValue(item); + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), value, this); + mDialog = builder.show(); + } + + public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); + mDialog = builder.show(); + } + + public void onSeekbarClick(SliderSetting item, int position, int progress) { + Setting setting = item.setSelectedValue(progress); + if (setting != null) { + mActivity.putSetting(setting); + } + mActivity.setSettingChanged(); + } + + public void onSliderClick(SliderSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + mSeekbarProgress = item.getSelectedValue(); + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); + + LayoutInflater inflater = LayoutInflater.from(mActivity); + View view = inflater.inflate(R.layout.dialog_seekbar, null); + + builder.setTitle(item.getNameId()); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, this); + 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 seekbar = view.findViewById(R.id.seekbar); + seekbar.setMax(item.getMax()); + seekbar.setProgress(mSeekbarProgress); + seekbar.setKeyProgressIncrement(5); + seekbar.setOnSeekBarChangeListener(this); + } + + public void onSubmenuClick(SubmenuSetting item) { + mActivity.loadSubMenu(item.getMenuKey()); + } + + private Spanned getFormatString(int resId, String arg) { + String unspanned = String.format(mActivity.getString(resId), arg); + Spanned spanned; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + spanned = Html.fromHtml(unspanned, Html.FROM_HTML_MODE_LEGACY); + } else { + spanned = Html.fromHtml(unspanned); + } + return spanned; + } + + public void onInputBindingClick(final InputBindingSetting item, final int position) { + mClickedItem = item; + mClickedPosition = position; + + final MotionAlertDialog dialog = new MotionAlertDialog(mActivity, item); + dialog.setTitle(R.string.input_binding); + dialog.setMessage(getFormatString(R.string.input_binding_description, + mActivity.getString(item.getNameId()))); + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mActivity.getString(android.R.string.cancel), + this); + dialog.setButton(AlertDialog.BUTTON_NEUTRAL, + mActivity.getString(R.string.clear_input_binding), + (dialogInterface, i) -> { + SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mActivity); + item.clearValue(); + }); + dialog.setOnDismissListener(dialog1 -> { + StringSetting setting = + new StringSetting(item.getKey(), item.getSection(), item.getValue()); + notifyItemChanged(position); + mActivity.putSetting(setting); + mActivity.setSettingChanged(); + }); + 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) + mActivity.setSettingChanged(); + + // 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) { + mActivity.putSetting(setting); + } else { + // + } + + closeDialog(); + } else if (mClickedItem instanceof StringSingleChoiceSetting) { + StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting)mClickedItem; + String value = scSetting.getValueAt(which); + if (!scSetting.getSelectedValue().equals(value)) + mActivity.setSettingChanged(); + + StringSetting setting = scSetting.setSelectedValue(value); + if (setting != null) { + mActivity.putSetting(setting); + } + + closeDialog(); + } else if (mClickedItem instanceof SliderSetting) { + SliderSetting sliderSetting = (SliderSetting)mClickedItem; + if (sliderSetting.getSelectedValue() != mSeekbarProgress) + mActivity.setSettingChanged(); + + Setting setting = sliderSetting.setSelectedValue(mSeekbarProgress); + if (setting != null) { + mActivity.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 = seekBar.getMax() > 99 ? (progress / 5) * 5 : 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 = mActivity.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 = mActivity.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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/SettingsFile.java b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFile.java new file mode 100644 index 00000000000..e397f591f55 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFile.java @@ -0,0 +1,213 @@ +package org.citra.emu.settings; + +import android.text.TextUtils; +import android.util.Log; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeSet; +import org.citra.emu.settings.model.BooleanSetting; +import org.citra.emu.settings.model.FloatSetting; +import org.citra.emu.settings.model.IntSetting; +import org.citra.emu.settings.model.Setting; +import org.citra.emu.settings.model.SettingSection; +import org.citra.emu.settings.model.StringSetting; +import org.citra.emu.utils.DirectoryInitialization; + +public final class SettingsFile { + // Core + public static final String KEY_USE_CPU_JIT = "use_cpu_jit"; + public static final String KEY_IS_NEW_3DS = "is_new_3ds"; + public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; + public static final String KEY_SYSTEM_REGION = "region_value"; + // Renderer + public static final String KEY_USE_GLES = "use_gles"; + public static final String KEY_SHOW_FPS = "show_fps"; + public static final String KEY_USE_HW_RENDERER = "use_hw_renderer"; + public static final String KEY_USE_HW_SHADER = "use_hw_shader"; + public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; + public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; + public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; + public static final String KEY_USE_FRAME_LIMIT = "use_frame_limit"; + public static final String KEY_FRAME_LIMIT = "frame_limit"; + public static final String KEY_LAYOUT_OPTION = "layout_option"; + public static final String KEY_POST_PROCESSING_SHADER = "pp_shader_name"; + // Audio + public static final String KEY_ENABLE_DSP_LLE = "enable_dsp_lle"; + public static final String KEY_AUDIO_STRETCHING = "enable_audio_stretching"; + public static final String KEY_AUDIO_VOLUME = "volume"; + public static final String KEY_AUDIO_ENGINE = "output_engine"; + public static final String KEY_AUDIO_DEVICE = "output_device"; + // controls + 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_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_START = "button_start"; + public static final String KEY_BUTTON_SELECT = "button_select"; + public static final String KEY_BUTTON_DEBUG = "button_debug"; + public static final String KEY_BUTTON_GPIO14 = "button_gpio14"; + public static final String KEY_BUTTON_ZL = "button_zl"; + public static final String KEY_BUTTON_ZR = "button_zr"; + public static final String KEY_BUTTON_HOME = "button_home"; + public static final String KEY_CIRCLE_PAD_UP = "circle_pad_up"; + public static final String KEY_CIRCLE_PAD_DOWN = "circle_pad_down"; + public static final String KEY_CIRCLE_PAD_LEFT = "circle_pad_left"; + public static final String KEY_CIRCLE_PAD_RIGHT = "circle_pad_right"; + + /** + * 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. + */ + public static HashMap loadSettings(String gameId) { + HashMap sections = new Settings.SettingsSectionMap(); + File ini = new File(DirectoryInitialization.getConfigFile()); + 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 = new SettingSection(line.substring(1, line.length() - 1)); + sections.put(current.getName(), current); + } else if ((current != null)) { + Setting setting = settingFromLine(current, line); + if (setting != null) { + current.putSetting(setting); + } + } + } + } catch (FileNotFoundException e) { + Log.e("zhangwei", + "[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage()); + } catch (IOException e) { + Log.e("zhangwei", + "[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage()); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.e("zhangwei", "[SettingsFile] Error closing: " + ini.getAbsolutePath() + + e.getMessage()); + } + } + } + + return sections; + } + + /** + * 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.w("zhangwei", "Skipping invalid config line \"" + line + "\""); + return null; + } + + String key = splitLine[0].trim(); + String value = splitLine[1].trim(); + + try { + int valueAsInt = Integer.valueOf(value); + + return new IntSetting(key, current.getName(), valueAsInt); + } catch (NumberFormatException ex) { + } + + try { + float valueAsFloat = Float.valueOf(value); + return new FloatSetting(key, current.getName(), valueAsFloat); + } catch (NumberFormatException ex) { + } + + switch (value) { + case "True": + return new BooleanSetting(key, current.getName(), true); + case "False": + return new BooleanSetting(key, current.getName(), false); + default: + return new StringSetting(key, current.getName(), value); + } + } + + /** + * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error + * telling why it failed. + * + * @param sections The HashMap containing the Settings we want to serialize. + */ + public static void saveFile(HashMap sections) { + File ini = new File(DirectoryInitialization.getConfigFile()); + PrintWriter writer = null; + try { + writer = new PrintWriter(ini, "UTF-8"); + + Set keySet = sections.keySet(); + Set sortedKeySet = new TreeSet<>(keySet); + + for (String key : sortedKeySet) { + SettingSection section = sections.get(key); + writeSection(writer, section); + } + } catch (FileNotFoundException e) { + Log.e("zhangwei", "[SettingsFile] File not found: " + e.getMessage()); + } catch (UnsupportedEncodingException e) { + Log.e("zhangwei", + "[SettingsFile] Bad encoding; please file a bug report: " + e.getMessage()); + } finally { + if (writer != null) { + writer.close(); + } + } + } + + /** + * Writes the contents of a Section HashMap to disk. + * + * @param writer A PrintWriter pointed at a file on disk. + * @param section A section containing settings to be written to the file. + */ + private static void writeSection(PrintWriter writer, SettingSection section) { + // Write this section's values. + HashMap settings = section.getSettings(); + if (settings.size() == 0) + return; + + // Write the section header. + String header = "[" + section.getName() + "]"; + writer.println(header); + + Set sortedKeySet = new TreeSet<>(settings.keySet()); + for (String key : sortedKeySet) { + Setting setting = settings.get(key); + String valueAsString = setting.getValueAsString(); + if (!TextUtils.isEmpty(valueAsString)) { + writer.println(setting.getKey() + " = " + valueAsString); + } + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/SettingsFragment.java b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFragment.java new file mode 100644 index 00000000000..4da1c7d0348 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFragment.java @@ -0,0 +1,271 @@ +package org.citra.emu.settings; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.nononsenseapps.filepicker.DividerItemDecoration; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.citra.emu.R; +import org.citra.emu.settings.model.Setting; +import org.citra.emu.settings.model.SettingSection; +import org.citra.emu.settings.view.CheckBoxSetting; +import org.citra.emu.settings.view.HeaderSetting; +import org.citra.emu.settings.view.InputBindingSetting; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SingleChoiceSetting; +import org.citra.emu.settings.view.StringSingleChoiceSetting; +import org.citra.emu.utils.DirectoryInitialization; + +public final class SettingsFragment extends Fragment { + private static final String ARGUMENT_MENU_TAG = "menu_tag"; + private static final String ARGUMENT_GAME_ID = "game_id"; + + private ArrayList mSettingsList; + private SettingsActivity mActivity; + private SettingsAdapter mAdapter; + private MenuTag mMenuTag; + private String mGameID; + private Settings mSettings; + + public static Fragment newInstance(MenuTag menuTag, String gameId, Bundle extras) { + SettingsFragment fragment = new SettingsFragment(); + + Bundle arguments = new Bundle(); + if (extras != null) { + arguments.putAll(extras); + } + + arguments.putSerializable(ARGUMENT_MENU_TAG, menuTag); + arguments.putString(ARGUMENT_GAME_ID, gameId); + + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mActivity = (SettingsActivity)context; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRetainInstance(true); + Bundle args = getArguments(); + mMenuTag = (MenuTag)args.getSerializable(ARGUMENT_MENU_TAG); + mGameID = getArguments().getString(ARGUMENT_GAME_ID); + mAdapter = new SettingsAdapter(mActivity); + } + + @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) { + Drawable lineDivider = mActivity.getDrawable(R.drawable.line_divider); + RecyclerView recyclerView = view.findViewById(R.id.list_settings); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(lineDivider)); + recyclerView.setAdapter(mAdapter); + showSettingsList(mActivity.getSettings()); + } + + @Override + public void onDetach() { + super.onDetach(); + mActivity = null; + } + + public void showSettingsList(Settings settings) { + mSettings = settings; + if (mMenuTag == MenuTag.INPUT) { + mSettingsList = loadBindingsList(); + } else { + mSettingsList = loadSettingsList(); + } + if (mSettingsList != null) { + mAdapter.setSettings(mSettingsList); + } + } + + private ArrayList loadSettingsList() { + ArrayList sl = new ArrayList<>(); + + // core + sl.add(new HeaderSetting(null, null, R.string.setting_header_core, 0)); + SettingSection coreSection = mSettings.getSection(Settings.SECTION_INI_CORE); + Setting isNew3DS = coreSection.getSetting(SettingsFile.KEY_IS_NEW_3DS); + Setting useVirtualSD = coreSection.getSetting(SettingsFile.KEY_USE_VIRTUAL_SD); + Setting systemRegion = coreSection.getSetting(SettingsFile.KEY_SYSTEM_REGION); + + sl.add(new CheckBoxSetting(SettingsFile.KEY_IS_NEW_3DS, Settings.SECTION_INI_CORE, + R.string.setting_is_new_3ds, 0, false, isNew3DS)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VIRTUAL_SD, Settings.SECTION_INI_CORE, + R.string.setting_use_virtual_sd, 0, true, useVirtualSD)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_SYSTEM_REGION, Settings.SECTION_INI_CORE, + R.string.setting_region_value, 0, + R.array.systemRegionEntries, R.array.systemRegionValues, -1, + systemRegion)); + + // renderer + sl.add(new HeaderSetting(null, null, R.string.setting_header_renderer, 0)); + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_INI_RENDERER); + Setting layoutOption = rendererSection.getSetting(SettingsFile.KEY_LAYOUT_OPTION); + Setting showFPS = rendererSection.getSetting(SettingsFile.KEY_SHOW_FPS); + Setting resolution = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); + Setting hwShader = rendererSection.getSetting(SettingsFile.KEY_USE_HW_SHADER); + Setting accurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); + Setting shader = rendererSection.getSetting(SettingsFile.KEY_POST_PROCESSING_SHADER); + + sl.add(new SingleChoiceSetting( + SettingsFile.KEY_LAYOUT_OPTION, Settings.SECTION_INI_RENDERER, R.string.layout_option, + 0, R.array.layoutOptionEntries, R.array.layoutOptionValues, 0, layoutOption)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_RESOLUTION_FACTOR, + Settings.SECTION_INI_RENDERER, R.string.internal_resolution, + 0, R.array.internalResolutionEntries, + R.array.internalResolutionValues, 1, resolution)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_SHOW_FPS, Settings.SECTION_INI_RENDERER, + R.string.show_fps, 0, true, showFPS)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_HW_SHADER, Settings.SECTION_INI_RENDERER, + R.string.setting_hw_shader, 0, true, hwShader)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, + Settings.SECTION_INI_RENDERER, + R.string.setting_shaders_accurate_mul, 0, false, accurateMul)); + // post process shaders + String[] shaderListValues = getShaderValues(); + String[] shaderListEntries = getShaderEntries(shaderListValues); + sl.add(new StringSingleChoiceSetting( + SettingsFile.KEY_POST_PROCESSING_SHADER, Settings.SECTION_INI_RENDERER, + R.string.post_processing_shader, 0, shaderListEntries, shaderListValues, "", shader)); + + // audio + sl.add(new HeaderSetting(null, null, R.string.setting_header_audio, 0)); + SettingSection audioSection = mSettings.getSection(Settings.SECTION_INI_AUDIO); + Setting audioOutput = audioSection.getSetting(SettingsFile.KEY_AUDIO_ENGINE); + Setting audioStretching = audioSection.getSetting(SettingsFile.KEY_AUDIO_STRETCHING); + + String[] outputEntries = getResources().getStringArray(R.array.audioOuputEntries); + String[] outputValues = getResources().getStringArray(R.array.audioOuputValues); + sl.add(new StringSingleChoiceSetting( + SettingsFile.KEY_AUDIO_ENGINE, Settings.SECTION_INI_AUDIO, + R.string.setting_audio_output, 0, outputEntries, outputValues, "auto", audioOutput)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_AUDIO_STRETCHING, Settings.SECTION_INI_AUDIO, + R.string.setting_audio_stretching, 0, false, audioStretching)); + + return sl; + } + + private ArrayList loadBindingsList() { + ArrayList sl = new ArrayList<>(); + + // controls + SettingSection bindingsSection = mSettings.getSection(Settings.SECTION_INI_CONTROLS); + Setting buttonA = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_A); + Setting buttonB = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_B); + Setting buttonX = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_X); + Setting buttonY = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_Y); + + Setting buttonUp = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_UP); + Setting buttonDown = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); + Setting buttonLeft = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); + Setting buttonRight = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); + + Setting buttonL = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_L); + Setting buttonR = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_R); + Setting buttonStart = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_START); + Setting buttonSelect = bindingsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); + + sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_INI_CONTROLS, + R.string.button_a, buttonA)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_INI_CONTROLS, + R.string.button_b, buttonB)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_INI_CONTROLS, + R.string.button_x, buttonX)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_INI_CONTROLS, + R.string.button_y, buttonY)); + + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_INI_CONTROLS, + R.string.button_l, buttonL)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_INI_CONTROLS, + R.string.button_r, buttonR)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_INI_CONTROLS, + R.string.button_start, buttonStart)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, + Settings.SECTION_INI_CONTROLS, R.string.button_select, + buttonSelect)); + + sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_INI_CONTROLS, + R.string.button_up, buttonUp)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_INI_CONTROLS, + R.string.button_down, buttonDown)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_INI_CONTROLS, + R.string.button_left, buttonLeft)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_INI_CONTROLS, + R.string.button_right, buttonRight)); + + return sl; + } + + private String capitalize(String text) { + if (text.contains("_")) { + text = text.replace("_", " "); + } + + if (text.length() > 1 && text.contains(" ")) { + String[] ss = text.split(" "); + text = capitalize(ss[0]); + for (int i = 1; i < ss.length; ++i) { + text += " " + capitalize(ss[i]); + } + return text; + } + + return text.substring(0, 1).toUpperCase() + text.substring(1); + } + + private String[] getShaderEntries(String[] values) { + String[] entries = new String[values.length]; + entries[0] = mActivity.getString(R.string.off); + for (int i = 1; i < values.length; ++i) { + entries[i] = capitalize(values[i]); + } + return entries; + } + + private String[] getShaderValues() { + List values = new ArrayList<>(); + values.add(""); + + String shadersPath = DirectoryInitialization.getShadersDirectory(); + File file = new File(shadersPath); + File[] shaderFiles = file.listFiles(); + if (shaderFiles != null) { + for (int i = 0; i < shaderFiles.length; ++i) { + String name = shaderFiles[i].getName(); + int extensionIndex = name.indexOf(".glsl"); + if (extensionIndex > 0) { + values.add(name.substring(0, extensionIndex)); + } + } + } + + return values.toArray(new String[0]); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/SettingsFrameLayout.java b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFrameLayout.java new file mode 100644 index 00000000000..cb19e941349 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/SettingsFrameLayout.java @@ -0,0 +1,48 @@ +package org.citra.emu.settings; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * FrameLayout subclass with few Properties added to simplify 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); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/model/BooleanSetting.java new file mode 100644 index 00000000000..176d92d09e6 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/BooleanSetting.java @@ -0,0 +1,23 @@ +package org.citra.emu.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"; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/model/FloatSetting.java new file mode 100644 index 00000000000..84bd3ba9831 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/FloatSetting.java @@ -0,0 +1,23 @@ +package org.citra.emu.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); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/IntSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/model/IntSetting.java new file mode 100644 index 00000000000..71b5936d624 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/IntSetting.java @@ -0,0 +1,23 @@ +package org.citra.emu.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); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/Setting.java b/src/android/app/src/main/java/org/citra/emu/settings/model/Setting.java new file mode 100644 index 00000000000..25293043c82 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/Setting.java @@ -0,0 +1,44 @@ +package org.citra.emu.settings.model; + +/** + * Abstraction for a setting item as read from / written to Dolphin'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(); +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/SettingSection.java b/src/android/app/src/main/java/org/citra/emu/settings/model/SettingSection.java new file mode 100644 index 00000000000..f9461a27f31 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/SettingSection.java @@ -0,0 +1,55 @@ +package org.citra.emu.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 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 getSettings() { + return mSettings; + } + + public void mergeSection(SettingSection settingSection) { + for (Setting setting : settingSection.mSettings.values()) { + putSetting(setting); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/model/StringSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/model/StringSetting.java new file mode 100644 index 00000000000..b44a0db05e3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/model/StringSetting.java @@ -0,0 +1,23 @@ +package org.citra.emu.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 == null ? "" : mValue; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/CheckBoxSetting.java new file mode 100644 index 00000000000..ffb1a332b73 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/CheckBoxSetting.java @@ -0,0 +1,47 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.model.BooleanSetting; +import org.citra.emu.settings.model.Setting; + +public final class CheckBoxSetting extends SettingsItem { + private boolean mDefaultValue; + + public CheckBoxSetting(String key, String section, int titleId, int descriptionId, + boolean defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + } + + public boolean isChecked() { + boolean value = mDefaultValue; + if (getSetting() != null) { + BooleanSetting setting = (BooleanSetting)getSetting(); + value = isInvertedSetting() != setting.getValue(); + } + return value; + } + + private boolean isInvertedSetting() { + return false; + } + + public BooleanSetting setChecked(boolean checked) { + if (isInvertedSetting()) + checked = !checked; + + if (getSetting() == null) { + BooleanSetting setting = new BooleanSetting(getKey(), getSection(), checked); + setSetting(setting); + return setting; + } else { + BooleanSetting setting = (BooleanSetting)getSetting(); + setting.setValue(checked); + return null; + } + } + + @Override + public int getType() { + return TYPE_CHECKBOX; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/HeaderSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/HeaderSetting.java new file mode 100644 index 00000000000..e198986becd --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/HeaderSetting.java @@ -0,0 +1,14 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/InputBindingSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/InputBindingSetting.java new file mode 100644 index 00000000000..7e737733ba1 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/InputBindingSetting.java @@ -0,0 +1,78 @@ +package org.citra.emu.settings.view; + +import android.view.InputDevice; +import android.view.KeyEvent; +import org.citra.emu.settings.model.Setting; +import org.citra.emu.settings.model.StringSetting; + +public class InputBindingSetting extends SettingsItem { + 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(); + } + + /** + * Write a value to the backing string. If that string was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param bind The input that will be bound + */ + public void setValue(String bind) { + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), bind); + setSetting(setting); + } else { + StringSetting setting = (StringSetting)getSetting(); + setting.setValue(bind); + } + } + + /** + * Saves the provided key input setting both to the INI file (so native code can use it) and as + * an Android preference (so it persists correctly and is human-readable.) + * + * @param keyEvent KeyEvent of this key press. + */ + public void onKeyInput(KeyEvent keyEvent) { + String bindStr = "code:" + keyEvent.getKeyCode(); + setValue(bindStr); + } + + /** + * Saves the provided motion input setting both to the INI file (so native code can use it) and + * as an Android preference (so it persists correctly and is human-readable.) + * + * @param device InputDevice from which the input event originated. + * @param motionRange MotionRange of the movement + * @param axisDir Either '-' or '+' + */ + public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, + char axisDir) { + String bindStr = "code:" + motionRange.getAxis() + ",dir:" + axisDir; + setValue(bindStr); + } + + public void clearValue() { + setValue(""); + } + + @Override + public int getType() { + return TYPE_INPUT_BINDING; + } + + public String getSettingText() { + if (getSetting() != null) { + return getSetting().getValueAsString(); + } + return null; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/SettingsItem.java b/src/android/app/src/main/java/org/citra/emu/settings/view/SettingsItem.java new file mode 100644 index 00000000000..f0eed40a541 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/SettingsItem.java @@ -0,0 +1,100 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.model.Setting; + +/** + * 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_SEEKBAR = 7; + + private String mKey; + private String mSection; + + private Setting mSetting; + + private int mNameId; + private int mDescriptionId; + + /** + * 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; + } + + /** + * @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; + } + + /** + * 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(); +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/SingleChoiceSetting.java new file mode 100644 index 00000000000..2fdb62b2797 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/SingleChoiceSetting.java @@ -0,0 +1,60 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.model.IntSetting; +import org.citra.emu.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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/SliderSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/SliderSetting.java new file mode 100644 index 00000000000..a451dbc743a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/SliderSetting.java @@ -0,0 +1,84 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.model.FloatSetting; +import org.citra.emu.settings.model.IntSetting; +import org.citra.emu.settings.model.Setting; + +public final class SliderSetting extends SettingsItem { + private int mMax; + private int mDefaultValue; + private String mUnits; + + public SliderSetting(String key, String section, int titleId, int descriptionId, int max, + String units, int defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mMax = max; + mUnits = units; + mDefaultValue = defaultValue; + } + + public int getMax() { + return mMax; + } + + 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; + if (isPercentSetting()) { + return Math.round(floatSetting.getValue() * 100); + } else { + return Math.round(floatSetting.getValue()); + } + } else { + // [SliderSetting] Error casting setting type. + return -1; + } + } + + public boolean isPercentSetting() { + return "%".equals(mUnits); + } + + public Setting setSelectedValue(int selection) { + if (getSetting() == null) { + if (isPercentSetting()) { + FloatSetting setting = new FloatSetting(getKey(), getSection(), selection / 100.0f); + setSetting(setting); + return setting; + } else { + IntSetting setting = new IntSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } + } else if (getSetting() instanceof FloatSetting) { + FloatSetting setting = (FloatSetting)getSetting(); + if (isPercentSetting()) + setting.setValue(selection / 100.0f); + else + setting.setValue(selection); + return null; + } else { + IntSetting setting = (IntSetting)getSetting(); + setting.setValue(selection); + return null; + } + } + + public String getUnits() { + return mUnits; + } + + @Override + public int getType() { + return TYPE_SLIDER; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/StringSingleChoiceSetting.java new file mode 100644 index 00000000000..84fb468f183 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/StringSingleChoiceSetting.java @@ -0,0 +1,83 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.model.Setting; +import org.citra.emu.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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/view/SubmenuSetting.java b/src/android/app/src/main/java/org/citra/emu/settings/view/SubmenuSetting.java new file mode 100644 index 00000000000..6cacd3179e7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/view/SubmenuSetting.java @@ -0,0 +1,23 @@ +package org.citra.emu.settings.view; + +import org.citra.emu.settings.MenuTag; +import org.citra.emu.settings.model.Setting; + +public final class SubmenuSetting extends SettingsItem { + private MenuTag mMenuKey; + + public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, + MenuTag menuKey) { + super(key, null, setting, titleId, descriptionId); + mMenuKey = menuKey; + } + + public MenuTag getMenuKey() { + return mMenuKey; + } + + @Override + public int getType() { + return TYPE_SUBMENU; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/CheckBoxSettingViewHolder.java new file mode 100644 index 00000000000..1c2a2e1285d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/CheckBoxSettingViewHolder.java @@ -0,0 +1,45 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.CheckBoxSetting; +import org.citra.emu.settings.view.SettingsItem; + +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()); + } + mCheckbox.setChecked(mItem.isChecked()); + } + + @Override + public void onClick(View clicked) { + mCheckbox.toggle(); + getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/HeaderViewHolder.java new file mode 100644 index 00000000000..cbfd137a5af --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/HeaderViewHolder.java @@ -0,0 +1,31 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; + +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 + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/InputBindingSettingViewHolder.java new file mode 100644 index 00000000000..ef58fcc7076 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/InputBindingSettingViewHolder.java @@ -0,0 +1,37 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.InputBindingSetting; +import org.citra.emu.settings.view.SettingsItem; + +public final class InputBindingSettingViewHolder extends SettingViewHolder { + private InputBindingSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public InputBindingSettingViewHolder(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 = (InputBindingSetting)item; + mTextSettingName.setText(mItem.getNameId()); + mTextSettingDescription.setText(mItem.getSettingText()); + } + + @Override + public void onClick(View clicked) { + getAdapter().onInputBindingClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SeekbarViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SeekbarViewHolder.java new file mode 100644 index 00000000000..11a93f26a1d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SeekbarViewHolder.java @@ -0,0 +1,57 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SliderSetting; + +public final class SeekbarViewHolder extends SettingViewHolder { + private SliderSetting mItem; + + private TextView mName; + private TextView mValue; + private SeekBar mSeekBar; + + public SeekbarViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mName = root.findViewById(R.id.text_setting_name); + mValue = root.findViewById(R.id.text_setting_value); + mSeekBar = root.findViewById(R.id.seekbar); + } + + @Override + public void bind(SettingsItem item) { + mItem = (SliderSetting)item; + mName.setText(item.getNameId()); + mSeekBar.setMax(mItem.getMax()); + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean b) { + if (mItem.getMax() > 99) + progress = (progress / 5) * 5; + mValue.setText(progress + mItem.getUnits()); + if (progress != mItem.getSelectedValue()) { + mItem.setSelectedValue(progress); + getAdapter().onSeekbarClick(mItem, getAdapterPosition(), progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }); + mSeekBar.setProgress(mItem.getSelectedValue()); + } + + @Override + public void onClick(View clicked) {} +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SettingViewHolder.java new file mode 100644 index 00000000000..3a00ddb8760 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SettingViewHolder.java @@ -0,0 +1,48 @@ +package org.citra.emu.settings.viewholder; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; + +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); +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SingleChoiceViewHolder.java new file mode 100644 index 00000000000..d635f9346aa --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SingleChoiceViewHolder.java @@ -0,0 +1,64 @@ +package org.citra.emu.settings.viewholder; + +import android.content.res.Resources; +import android.view.View; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SingleChoiceSetting; +import org.citra.emu.settings.view.StringSingleChoiceSetting; + +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()); + + 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 StringSingleChoiceSetting) { + StringSingleChoiceSetting setting = (StringSingleChoiceSetting)item; + String[] choices = setting.getChoicesId(); + int valueIndex = setting.getSelectValueIndex(); + if (valueIndex != -1) + mTextSettingDescription.setText(choices[valueIndex]); + } + } + + @Override + public void onClick(View clicked) { + int position = getAdapterPosition(); + if (mItem instanceof SingleChoiceSetting) { + getAdapter().onSingleChoiceClick((SingleChoiceSetting)mItem, position); + } else if (mItem instanceof StringSingleChoiceSetting) { + getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting)mItem, position); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SliderViewHolder.java new file mode 100644 index 00000000000..1fbc81f20d7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SliderViewHolder.java @@ -0,0 +1,41 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SliderSetting; + +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()); + } else { + mTextSettingDescription.setText(mItem.getSelectedValue() + mItem.getUnits()); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSliderClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SubmenuViewHolder.java new file mode 100644 index 00000000000..ddeef996c58 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/settings/viewholder/SubmenuViewHolder.java @@ -0,0 +1,41 @@ +package org.citra.emu.settings.viewholder; + +import android.view.View; +import android.widget.TextView; +import org.citra.emu.R; +import org.citra.emu.settings.SettingsAdapter; +import org.citra.emu.settings.view.SettingsItem; +import org.citra.emu.settings.view.SubmenuSetting; + +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()); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSubmenuClick(mItem); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/EditorActivity.java b/src/android/app/src/main/java/org/citra/emu/ui/EditorActivity.java new file mode 100644 index 00000000000..efb3e581c8f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/EditorActivity.java @@ -0,0 +1,123 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.emu.ui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import org.citra.emu.R; +import org.citra.emu.utils.DirectoryInitialization; + +public final class EditorActivity extends AppCompatActivity { + + private static final String ARG_PROGRAM_ID = "program_id"; + private static final String ARG_PROGRAM_TITLE = "program_title"; + + private EditText mEditor; + private RecyclerView mListView; + private ProgressBar mProgressBar; + + public static void launch(Context context, String programId, String title) { + Intent settings = new Intent(context, EditorActivity.class); + settings.putExtra(ARG_PROGRAM_ID, programId); + settings.putExtra(ARG_PROGRAM_TITLE, title); + context.startActivity(settings); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_editor); + + final String programId = getIntent().getStringExtra(ARG_PROGRAM_ID); + final String title = getIntent().getStringExtra(ARG_PROGRAM_TITLE); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + setTitle(title); + + mEditor = findViewById(R.id.code_content); + mListView = findViewById(R.id.code_list); + mProgressBar = findViewById(R.id.progress_bar); + mProgressBar.setVisibility(View.INVISIBLE); + + Button buttonConfirm = findViewById(R.id.button_confirm); + buttonConfirm.setOnClickListener(view -> { + saveCheatCode(programId); + mEditor.clearFocus(); + finish(); + }); + + Button buttonCancel = findViewById(R.id.button_cancel); + buttonCancel.setOnClickListener(view -> { + mEditor.clearFocus(); + finish(); + }); + + loadCheatCode(programId); + toggleListView(false); + } + + private void toggleListView(boolean isShowList) { + if (isShowList) { + InputMethodManager imm = + (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0); + mListView.setVisibility(View.VISIBLE); + mEditor.setVisibility(View.INVISIBLE); + } else { + mListView.setVisibility(View.INVISIBLE); + mEditor.setVisibility(View.VISIBLE); + } + } + + private void loadCheatCode(String programId) { + File cheatFile = DirectoryInitialization.getCheatFile(programId); + StringBuilder sb = new StringBuilder(); + if (cheatFile != null && cheatFile.exists()) { + try { + BufferedReader reader = new BufferedReader(new FileReader(cheatFile)); + String line = reader.readLine(); + while (line != null) { + sb.append(line); + line = reader.readLine(); + } + reader.close(); + } catch (IOException e) { + // + } + } + mEditor.setText(sb.toString()); + } + + private void saveCheatCode(String programId) { + File cheatFile = DirectoryInitialization.getCheatFile(programId); + String content = mEditor.getText().toString(); + if (content.isEmpty()) { + cheatFile.delete(); + } else { + try { + FileWriter writer = new FileWriter(cheatFile); + writer.write(content); + writer.close(); + } catch (IOException e) { + // + } + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/EmulationActivity.java b/src/android/app/src/main/java/org/citra/emu/ui/EmulationActivity.java new file mode 100644 index 00000000000..4b1b0e7439c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/EmulationActivity.java @@ -0,0 +1,223 @@ +package org.citra.emu.ui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.app.AppCompatActivity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import java.lang.ref.WeakReference; +import java.util.List; +import org.citra.emu.NativeLibrary; +import org.citra.emu.R; +import org.citra.emu.utils.ControllerMappingHelper; + +public final class EmulationActivity extends AppCompatActivity { + private static final String EXTRA_GAMEPATH = "SelectedGames"; + private static WeakReference sInstance = new WeakReference<>(null); + private String mPath; + private View mDecorView; + private boolean mMenuVisible; + private boolean mStopEmulation; + private EmulationFragment mEmulationFragment; + + public static void launch(Context context, String path) { + Intent intent = new Intent(context, EmulationActivity.class); + intent.putExtra(EXTRA_GAMEPATH, path); + context.startActivity(intent); + } + + public static EmulationActivity get() { + return sInstance.get(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_emulation); + sInstance = new WeakReference<>(this); + + if (savedInstanceState == null) { + Intent intent = getIntent(); + mPath = intent.getStringExtra(EXTRA_GAMEPATH); + } else { + mPath = savedInstanceState.getString(EXTRA_GAMEPATH); + } + + // 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 */); + } + }); + mStopEmulation = false; + + // Find or create the EmulationFragment + mEmulationFragment = (EmulationFragment)getSupportFragmentManager().findFragmentById( + R.id.fragment_emulation); + if (mEmulationFragment == null) { + mEmulationFragment = EmulationFragment.newInstance(mPath); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_emulation, mEmulationFragment) + .commit(); + } + + if (mPath != null) { + setTitle(NativeLibrary.GetAppTitle(mPath)); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putString(EXTRA_GAMEPATH, mPath); + super.onSaveInstanceState(outState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_emulation, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_emulation_screenshot: + NativeLibrary.SaveScreenShot(); + return true; + + case R.id.menu_running_setting: + RunningSettingDialog.newInstance().show(getSupportFragmentManager(), + "RunningSettingDialog"); + return true; + + case R.id.menu_emulation_edit_layout: + mEmulationFragment.startConfiguringControls(); + return true; + } + + return false; + } + + private void enableFullscreenImmersive() { + if (mStopEmulation) { + return; + } + mMenuVisible = false; + // 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); + } + + private void disableFullscreenImmersive() { + mMenuVisible = true; + mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + @Override + public void onBackPressed() { + if (mMenuVisible) { + mStopEmulation = true; + mEmulationFragment.stopEmulation(); + finish(); + } else { + disableFullscreenImmersive(); + } + } + + // Gets button presses + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mMenuVisible) { + return super.dispatchKeyEvent(event); + } + + InputDevice input = event.getDevice(); + int button = event.getKeyCode(); + int action; + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + // Handling the case where the back button is pressed. + if (button == 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; + } + + if (input != null) + return NativeLibrary.KeyEvent(button, action); + else + return false; + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mMenuVisible) { + return false; + } + + 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 motions = input.getMotionRanges(); + + for (InputDevice.MotionRange range : motions) { + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = ControllerMappingHelper.scaleAxis(input, axis, origValue); + // If the input is still in the "flat" area, that means it's really zero. + // This is used to compensate for imprecision in joysticks. + if (Math.abs(value) > range.getFlat()) { + NativeLibrary.MoveEvent(axis, value); + } else { + NativeLibrary.MoveEvent(axis, 0.0f); + } + } + + return true; + } + + public void showInputBoxDialog(int maxLength, String hint, String button0, String button1, + String button2) { + KeyboardDialog.newInstance(maxLength, hint, button0, button1, button2) + .show(getSupportFragmentManager(), "KeyboardDialog"); + } + + public void showMiiSelectorDialog(boolean cancel, String title, String[] miis) { + MiiSelectorDialog.newInstance(cancel, title, miis) + .show(getSupportFragmentManager(), "MiiSelectorDialog"); + } + + public void refreshControls() { + mEmulationFragment.refreshControls(); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/EmulationFragment.java b/src/android/app/src/main/java/org/citra/emu/ui/EmulationFragment.java new file mode 100644 index 00000000000..638e0e7c568 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/EmulationFragment.java @@ -0,0 +1,156 @@ +package org.citra.emu.ui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +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 org.citra.emu.NativeLibrary; +import org.citra.emu.R; +import org.citra.emu.overlay.InputOverlay; + +public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback { + private static final String KEY_GAMEPATH = "gamepath"; + private String mPath; + private Surface mSurface; + private EmulationState mState; + private boolean mRunWhenSurfaceIsValid; + private InputOverlay mInputOverlay; + private Button mBtnDone; + + 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 onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // So this fragment doesn't restart on configuration changes; i.e. rotation. + setRetainInstance(true); + + mPath = getArguments().getString(KEY_GAMEPATH); + mState = EmulationState.STOPPED; + } + + @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); + mBtnDone = contents.findViewById(R.id.done_control_config); + mBtnDone.setOnClickListener(v -> stopConfiguringControls()); + + return contents; + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // All work is done in surfaceChanged + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + mSurface = holder.getSurface(); + if (mRunWhenSurfaceIsValid) { + runWithValidSurface(); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (mSurface == null) { + // [EmulationFragment] clearSurface called, but surface already null. + } else { + mSurface = null; + if (mState == EmulationState.RUNNING) { + NativeLibrary.SurfaceDestroyed(); + mState = EmulationState.PAUSED; + } else if (mState == EmulationState.PAUSED) { + // [EmulationFragment] Surface cleared while emulation paused. + } else { + // [EmulationFragment] Surface cleared while emulation stopped. + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (NativeLibrary.IsRunning()) { + mState = EmulationState.PAUSED; + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (mSurface != null) { + runWithValidSurface(); + } else { + mRunWhenSurfaceIsValid = true; + } + } + + @Override + public void onPause() { + if (mState == EmulationState.RUNNING) { + mState = EmulationState.PAUSED; + // Release the surface before pausing, since emulation has to be running for that. + NativeLibrary.SurfaceDestroyed(); + NativeLibrary.PauseEmulation(); + } + super.onPause(); + } + + public void startConfiguringControls() { + mBtnDone.setVisibility(View.VISIBLE); + mInputOverlay.setInEditMode(true); + } + + public void stopConfiguringControls() { + mBtnDone.setVisibility(View.GONE); + mInputOverlay.setInEditMode(false); + } + + public void stopEmulation() { + if (mState != EmulationState.STOPPED) { + mState = EmulationState.STOPPED; + NativeLibrary.StopEmulation(); + } else { + // [EmulationFragment] Stop called while already stopped. + } + } + + public void refreshControls() { + mInputOverlay.refreshControls(); + mInputOverlay.invalidate(); + } + + private void runWithValidSurface() { + mRunWhenSurfaceIsValid = false; + if (mState == EmulationState.STOPPED) { + new Thread(() -> { + NativeLibrary.SurfaceChanged(mSurface); + NativeLibrary.Run(mPath); + }, "NativeEmulation").start(); + } else if (mState == EmulationState.PAUSED) { + NativeLibrary.SurfaceChanged(mSurface); + NativeLibrary.ResumeEmulation(); + } else { + // [EmulationFragment] Bug, run called while already running. + } + mState = EmulationState.RUNNING; + } + + private enum EmulationState { STOPPED, RUNNING, PAUSED } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/GameFilePickerActivity.java b/src/android/app/src/main/java/org/citra/emu/ui/GameFilePickerActivity.java new file mode 100644 index 00000000000..920277c7c5f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/GameFilePickerActivity.java @@ -0,0 +1,48 @@ +package org.citra.emu.ui; + +import android.net.Uri; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.FileProvider; +import com.nononsenseapps.filepicker.AbstractFilePickerFragment; +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.nononsenseapps.filepicker.FilePickerFragment; +import java.io.File; +import org.citra.emu.NativeLibrary; + +public final class GameFilePickerActivity extends FilePickerActivity { + @Override + protected AbstractFilePickerFragment getFragment(@Nullable final String startPath, + final int mode, + final boolean allowMultiple, + final boolean allowExistingFile, + final boolean singleClick) { + AbstractFilePickerFragment fragment = new GameFilePickerFragment(); + // 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, allowExistingFile, singleClick); + return fragment; + } + + public static class GameFilePickerFragment extends FilePickerFragment { + @NonNull + @Override + public Uri toUri(@NonNull final File file) { + return FileProvider.getUriForFile( + getContext(), + getContext().getApplicationContext().getPackageName() + ".filesprovider", file); + } + + @Override + protected boolean isItemVisible(final File file) { + if (file.isHidden()) + return false; + if (file.isDirectory()) + return true; + return NativeLibrary.isValidFile(file.getName()); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/KeyboardDialog.java b/src/android/app/src/main/java/org/citra/emu/ui/KeyboardDialog.java new file mode 100644 index 00000000000..7146df18871 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/KeyboardDialog.java @@ -0,0 +1,118 @@ +package org.citra.emu.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.text.InputFilter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import org.citra.emu.NativeLibrary; +import org.citra.emu.R; + +public class KeyboardDialog extends DialogFragment { + private static final String ARG_MAX_LENGTH = "max_length"; + private static final String ARG_INPUT_HINT = "hint"; + private static final String ARG_INPUT_BUTTON0 = "button0"; + private static final String ARG_INPUT_BUTTON1 = "button1"; + private static final String ARG_INPUT_BUTTON2 = "button2"; + + private static final int EVENT_FINISH = -1; + private static final int EVENT_BUTTON0 = 0; + private static final int EVENT_BUTTON1 = 1; + private static final int EVENT_BUTTON2 = 2; + + public static KeyboardDialog newInstance(int maxLength, String hint, String button0, + String button1, String button2) { + KeyboardDialog fragment = new KeyboardDialog(); + Bundle arguments = new Bundle(); + arguments.putInt(ARG_MAX_LENGTH, maxLength); + arguments.putString(ARG_INPUT_HINT, hint); + arguments.putString(ARG_INPUT_BUTTON0, button0); + arguments.putString(ARG_INPUT_BUTTON1, button1); + arguments.putString(ARG_INPUT_BUTTON2, button2); + fragment.setArguments(arguments); + return fragment; + } + + private boolean mIsProcessed = false; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + int maxLength = args.getInt(ARG_MAX_LENGTH, -1); + String hint = args.getString(ARG_INPUT_HINT, ""); + String text0 = args.getString(ARG_INPUT_BUTTON0, ""); + String text1 = args.getString(ARG_INPUT_BUTTON1, ""); + String text2 = args.getString(ARG_INPUT_BUTTON2, ""); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + ViewGroup contents = + (ViewGroup)getActivity().getLayoutInflater().inflate(R.layout.dialog_input_box, null); + + final EditText inputBox = contents.findViewById(R.id.text_input); + if (maxLength > 0) { + inputBox.setFilters(new InputFilter[] {new InputFilter.LengthFilter(maxLength)}); + } + if (!hint.isEmpty()) { + inputBox.setText(hint); + } + + TextView textInfo = contents.findViewById(R.id.max_length_info); + if (maxLength > 0) { + textInfo.setText(getString(R.string.input_text_max_length, maxLength)); + } else { + textInfo.setVisibility(View.GONE); + } + + Button button0 = contents.findViewById(R.id.button0); + if (!text0.isEmpty()) { + button0.setText(text0); + button0.setOnClickListener(view -> { + mIsProcessed = true; + NativeLibrary.KeyboardEvent(EVENT_BUTTON0, inputBox.getText().toString()); + dismiss(); + }); + } else { + button0.setVisibility(View.GONE); + } + + Button button1 = contents.findViewById(R.id.button1); + if (!text1.isEmpty()) { + button1.setText(text1); + button1.setOnClickListener(view -> { + mIsProcessed = true; + NativeLibrary.KeyboardEvent(EVENT_BUTTON1, inputBox.getText().toString()); + dismiss(); + }); + } else { + button1.setVisibility(View.GONE); + } + + Button button2 = contents.findViewById(R.id.button2); + if (!text2.isEmpty()) { + button2.setText(text2); + button2.setOnClickListener(view -> { + mIsProcessed = true; + NativeLibrary.KeyboardEvent(EVENT_BUTTON2, inputBox.getText().toString()); + dismiss(); + }); + } else { + button2.setVisibility(View.GONE); + } + + builder.setOnDismissListener(dialog -> { + if (!mIsProcessed) + NativeLibrary.KeyboardEvent(EVENT_FINISH, inputBox.getText().toString()); + }); + + builder.setView(contents); + return builder.create(); + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/MainActivity.java b/src/android/app/src/main/java/org/citra/emu/ui/MainActivity.java new file mode 100644 index 00000000000..775553d555b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/MainActivity.java @@ -0,0 +1,400 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.emu.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import com.nononsenseapps.filepicker.DividerItemDecoration; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import org.citra.emu.NativeLibrary; +import org.citra.emu.R; +import org.citra.emu.model.GameFile; +import org.citra.emu.settings.MenuTag; +import org.citra.emu.settings.SettingsActivity; +import org.citra.emu.utils.DirectoryInitialization; +import org.citra.emu.utils.FileBrowserHelper; +import org.citra.emu.utils.PermissionsHandler; + +public final class MainActivity extends AppCompatActivity { + public static final int REQUEST_ADD_DIRECTORY = 1; + public static final int REQUEST_OPEN_FILE = 2; + private static WeakReference sInstance = new WeakReference<>(null); + private List mGames; + private String mDirToAdd; + private String[] mFilesToAdd; + private GameAdapter mAdapter; + private ProgressBar mProgressBar; + private BroadcastReceiver mBroadcastReceiver; + + public static MainActivity get() { + return sInstance.get(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + sInstance = new WeakReference<>(this); + + Toolbar toolbar = findViewById(R.id.toolbar_main); + setSupportActionBar(toolbar); + + mProgressBar = findViewById(R.id.progress_bar); + mGames = new ArrayList<>(); + mAdapter = new GameAdapter(); + RecyclerView GameList = findViewById(R.id.grid_games); + GameList.setAdapter(mAdapter); + Drawable lineDivider = getDrawable(R.drawable.line_divider); + GameList.addItemDecoration(new DividerItemDecoration(lineDivider)); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); + GameList.setLayoutManager(layoutManager); + + if (PermissionsHandler.checkWritePermission(this)) { + loadGameList(); + showGames(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_main, menu); + menu.findItem(R.id.menu_input_binding).setVisible(false); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_add_directory: + FileBrowserHelper.openDirectoryPicker(this); + return true; + + case R.id.menu_settings_core: + SettingsActivity.launch(this, MenuTag.CONFIG, ""); + return true; + + case R.id.menu_input_binding: + SettingsActivity.launch(this, MenuTag.INPUT, ""); + return true; + + case R.id.menu_install_cia: + FileBrowserHelper.openFilePicker(this, REQUEST_OPEN_FILE); + return true; + + case R.id.menu_refresh: + refreshLibrary(); + return true; + } + + return false; + } + + @Override + protected void onResume() { + super.onResume(); + if (mDirToAdd != null) { + addGamesInDirectory(mDirToAdd); + mDirToAdd = null; + saveGameList(); + showGames(); + } + + if (mFilesToAdd != null) { + List filelist = new ArrayList<>(); + for (String f : mFilesToAdd) { + if (f.toLowerCase().endsWith(".cia")) { + filelist.add(f); + } + } + mFilesToAdd = null; + final String[] files = filelist.toArray(new String[0]); + new Thread(() -> NativeLibrary.InstallCIA(files)).start(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mBroadcastReceiver != null) { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + switch (requestCode) { + case REQUEST_ADD_DIRECTORY: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + mDirToAdd = FileBrowserHelper.getSelectedDirectory(result); + } + break; + case REQUEST_OPEN_FILE: + if (resultCode == MainActivity.RESULT_OK) { + mFilesToAdd = FileBrowserHelper.getSelectedFiles(result); + } + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + switch (requestCode) { + case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + IntentFilter filter = new IntentFilter(); + filter.addAction(DirectoryInitialization.BROADCAST_ACTION); + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + loadGameList(); + showGames(); + } + }; + LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, + filter); + DirectoryInitialization.start(this); + } else { + Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT).show(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + break; + } + } + + public void refreshLibrary() { + List dirs = new ArrayList<>(); + for (int i = mGames.size(); i > 0; --i) { + GameFile game = mGames.get(i - 1); + String path = game.getPath(); + if (new File(path).exists()) { + int lastSlash = path.lastIndexOf('/'); + if (lastSlash == -1) + path = "/"; + else + path = path.substring(0, lastSlash); + if (!dirs.contains(path)) { + dirs.add(path); + } + } else { + mGames.remove(i - 1); + } + } + for (String dir : dirs) { + addGamesInDirectory(dir); + } + saveGameList(); + showGames(); + } + + public void addGamesInDirectory(String directory) { + File[] files = new File(directory).listFiles((File dir, String name) -> { + if (NativeLibrary.isValidFile(name)) { + String path = dir.getPath() + File.separator + name; + for (GameFile game : mGames) { + if (path.equals(game.getPath())) { + return false; + } + } + return true; + } + return false; + }); + + if (files == null) { + return; + } + + for (File f : files) { + String path = f.getPath(); + if (NativeLibrary.IsAppExecutable(path)) + mGames.add(new GameFile(path)); + } + } + + public void saveGameList() { + StringBuilder sb = new StringBuilder(); + for (GameFile game : mGames) { + sb.append(game.getPath()); + sb.append(";"); + } + + File cache = getGameListCache(); + FileWriter writer; + try { + writer = new FileWriter(cache); + writer.write(sb.toString()); + writer.close(); + } catch (IOException e) { + // + } + } + + public void loadGameList() { + String content = ""; + File cache = getGameListCache(); + try { + FileReader reader = new FileReader(cache); + char[] buffer = new char[(int)cache.length()]; + int size = reader.read(buffer); + content = new String(buffer); + reader.close(); + } catch (IOException e) { + // + } + + mGames.clear(); + for (String path : content.split(";")) { + if (NativeLibrary.isValidFile(path) && new File(path).exists() && + NativeLibrary.IsAppExecutable(path)) { + mGames.add(new GameFile(path)); + } + } + } + + public File getGameListCache() { + return new File(DirectoryInitialization.getUserDirectory() + File.separator + + "gamelist.cache"); + } + + public void updateProgress(String name, int written, int total) { + if (written < total) { + mProgressBar.setVisibility(View.VISIBLE); + } else { + mProgressBar.setVisibility(View.INVISIBLE); + if (total == 0) { + if (written == 0) { + Toast.makeText(this, "Install Success!", Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(this, "Error: " + name, Toast.LENGTH_LONG).show(); + } + } + } + } + + public void showGames() { + mGames.sort((GameFile x, GameFile y) -> x.getName().compareTo(y.getName())); + mAdapter.setGameList(mGames); + } + + static class GameViewHolder extends RecyclerView.ViewHolder { + private ImageView mImageIcon; + private TextView mTextTitle; + private TextView mTextRegion; + private TextView mTextCompany; + private GameFile mModel; + + public GameViewHolder(View itemView) { + super(itemView); + itemView.setTag(this); + mImageIcon = itemView.findViewById(R.id.image_game_screen); + mTextTitle = itemView.findViewById(R.id.text_game_title); + mTextRegion = itemView.findViewById(R.id.text_region); + mTextCompany = itemView.findViewById(R.id.text_company); + } + + public void bind(GameFile model) { + int[] regions = { + R.string.region_invalid, R.string.region_japan, R.string.region_north_america, + R.string.region_europe, R.string.region_australia, R.string.region_china, + R.string.region_korea, R.string.region_taiwan, + }; + mModel = model; + mTextTitle.setText(model.getName()); + mTextCompany.setText(model.getInfo()); + mTextRegion.setText(regions[model.getRegion() + 1]); + mImageIcon.setImageBitmap(model.getIcon(mImageIcon.getContext())); + } + + public String getPath() { + return mModel.getPath(); + } + + public String getProgramId() { + return mModel.getId(); + } + + public String getTitle() { + return mModel.getName(); + } + } + + static class GameAdapter extends RecyclerView.Adapter + implements View.OnClickListener, View.OnLongClickListener { + private List mGameList = new ArrayList<>(); + + @Override + public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View gameCard = + LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + gameCard.setOnClickListener(this); + gameCard.setOnLongClickListener(this); + return new GameViewHolder(gameCard); + } + + @Override + public void onBindViewHolder(GameViewHolder holder, int position) { + holder.bind(mGameList.get(position)); + } + + @Override + public int getItemViewType(int position) { + return R.layout.card_game; + } + + @Override + public int getItemCount() { + return mGameList.size(); + } + + @Override + public void onClick(View view) { + GameViewHolder holder = (GameViewHolder)view.getTag(); + EmulationActivity.launch(view.getContext(), holder.getPath()); + } + + @Override + public boolean onLongClick(View view) { + GameViewHolder holder = (GameViewHolder)view.getTag(); + EditorActivity.launch(view.getContext(), holder.getProgramId(), holder.getTitle()); + return true; + } + + public void setGameList(List games) { + mGameList = games; + notifyDataSetChanged(); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/MiiSelectorDialog.java b/src/android/app/src/main/java/org/citra/emu/ui/MiiSelectorDialog.java new file mode 100644 index 00000000000..2134f574c6a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/MiiSelectorDialog.java @@ -0,0 +1,136 @@ +package org.citra.emu.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; +import com.nononsenseapps.filepicker.DividerItemDecoration; +import java.util.ArrayList; +import org.citra.emu.R; + +public class MiiSelectorDialog extends DialogFragment { + private static final String ARG_CANCEL = "cancel"; + private static final String ARG_TITLE = "title"; + private static final String ARG_MIIS = "miis"; + + public static MiiSelectorDialog newInstance(boolean cancel, String title, String[] miis) { + MiiSelectorDialog fragment = new MiiSelectorDialog(); + Bundle arguments = new Bundle(); + arguments.putBoolean(ARG_CANCEL, cancel); + arguments.putString(ARG_TITLE, title); + arguments.putStringArray(ARG_MIIS, miis); + fragment.setArguments(arguments); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ViewGroup contents = (ViewGroup)getActivity().getLayoutInflater().inflate( + R.layout.dialog_mii_selector, null); + + Drawable lineDivider = getContext().getDrawable(R.drawable.line_divider); + RecyclerView recyclerView = contents.findViewById(R.id.list_settings); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); + recyclerView.setLayoutManager(layoutManager); + MiisAdapter adapter = new MiisAdapter(); + recyclerView.setAdapter(adapter); + recyclerView.addItemDecoration(new DividerItemDecoration(lineDivider)); + builder.setView(contents); + return builder.create(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + } + + public class SettingsItem {} + + public abstract class SettingViewHolder + extends RecyclerView.ViewHolder implements View.OnClickListener { + public SettingViewHolder(View itemView) { + super(itemView); + itemView.setOnClickListener(this); + findViews(itemView); + } + + protected abstract void findViews(View root); + + public abstract void bind(SettingsItem item); + + public abstract void onClick(View clicked); + } + + public final class CheckBoxSettingViewHolder + extends SettingViewHolder implements CompoundButton.OnCheckedChangeListener { + SettingsItem mItem; + private TextView mTextSettingName; + private CheckBox mCheckbox; + + public CheckBoxSettingViewHolder(View itemView) { + super(itemView); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mCheckbox = root.findViewById(R.id.checkbox); + mCheckbox.setOnCheckedChangeListener(this); + } + + @Override + public void bind(SettingsItem item) { + mItem = item; + // mTextSettingName.setText(item.getName()); + // mCheckbox.setChecked(mItem.getValue() > 0); + } + + @Override + public void onClick(View clicked) { + mCheckbox.toggle(); + // mItem.setValue(mCheckbox.isChecked() ? 1 : 0); + } + + @Override + public void onCheckedChanged(CompoundButton view, boolean isChecked) { + // mItem.setValue(isChecked ? 1 : 0); + } + } + + public class MiisAdapter extends RecyclerView.Adapter { + private ArrayList mSettings; + + public MiisAdapter() {} + + @NonNull + @Override + public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View itemView = inflater.inflate(R.layout.list_item_running_checkbox, parent, false); + return new CheckBoxSettingViewHolder(itemView); + } + + @Override + public int getItemCount() { + return mSettings.size(); + } + + @Override + public void onBindViewHolder(@NonNull SettingViewHolder holder, int position) { + holder.bind(mSettings.get(position)); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/ui/RunningSettingDialog.java b/src/android/app/src/main/java/org/citra/emu/ui/RunningSettingDialog.java new file mode 100644 index 00000000000..2b4580cb984 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/ui/RunningSettingDialog.java @@ -0,0 +1,338 @@ +package org.citra.emu.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.SeekBar; +import android.widget.TextView; +import com.nononsenseapps.filepicker.DividerItemDecoration; +import java.util.ArrayList; +import org.citra.emu.NativeLibrary; +import org.citra.emu.R; +import org.citra.emu.overlay.InputOverlay; + +public class RunningSettingDialog extends DialogFragment { + private SettingsAdapter mAdapter; + + public static RunningSettingDialog newInstance() { + return new RunningSettingDialog(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ViewGroup contents = (ViewGroup)getActivity().getLayoutInflater().inflate( + R.layout.dialog_running_settings, null); + + Drawable lineDivider = getContext().getDrawable(R.drawable.line_divider); + RecyclerView recyclerView = contents.findViewById(R.id.list_settings); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); + recyclerView.setLayoutManager(layoutManager); + mAdapter = new SettingsAdapter(); + recyclerView.setAdapter(mAdapter); + recyclerView.addItemDecoration(new DividerItemDecoration(lineDivider)); + builder.setView(contents); + return builder.create(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + mAdapter.saveSettings(); + } + + public class SettingsItem { + // setting type + public static final int SETTING_CORE_TICKS_HACK = 0; + public static final int SETTING_LAYOUT_SINGLE_SCREEN = 1; + + // pref + public static final int SETTING_JOYSTICK_RELATIVE = 100; + public static final int SETTING_CONTROLLER_SCALE = 101; + public static final int SETTING_CONTROLLER_ALPHA = 102; + public static final int SETTING_EMULATE_MOTION_BY_TOUCH = 103; + + // view type + public static final int TYPE_CHECKBOX = 0; + public static final int TYPE_SEEK_BAR = 2; + + private int mSetting; + private String mName; + private int mType; + private int mValue; + + public SettingsItem(int setting, int nameId, int type, int value) { + mSetting = setting; + mName = getString(nameId); + mType = type; + mValue = value; + } + + public SettingsItem(int setting, String name, int type, int value) { + mSetting = setting; + mName = name; + mType = type; + mValue = value; + } + + public int getType() { + return mType; + } + + public int getSetting() { + return mSetting; + } + + public String getName() { + return mName; + } + + public int getValue() { + return mValue; + } + + public void setValue(int value) { + mValue = value; + } + } + + public abstract class SettingViewHolder + extends RecyclerView.ViewHolder implements View.OnClickListener { + public SettingViewHolder(View itemView) { + super(itemView); + itemView.setOnClickListener(this); + findViews(itemView); + } + + protected abstract void findViews(View root); + + public abstract void bind(SettingsItem item); + + public abstract void onClick(View clicked); + } + + public final class CheckBoxSettingViewHolder + extends SettingViewHolder implements CompoundButton.OnCheckedChangeListener { + SettingsItem mItem; + private TextView mTextSettingName; + private CheckBox mCheckbox; + + public CheckBoxSettingViewHolder(View itemView) { + super(itemView); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mCheckbox = root.findViewById(R.id.checkbox); + mCheckbox.setOnCheckedChangeListener(this); + } + + @Override + public void bind(SettingsItem item) { + mItem = item; + mTextSettingName.setText(item.getName()); + mCheckbox.setChecked(mItem.getValue() > 0); + } + + @Override + public void onClick(View clicked) { + mCheckbox.toggle(); + mItem.setValue(mCheckbox.isChecked() ? 1 : 0); + } + + @Override + public void onCheckedChanged(CompoundButton view, boolean isChecked) { + mItem.setValue(isChecked ? 1 : 0); + } + } + + public final class SeekBarSettingViewHolder extends SettingViewHolder { + SettingsItem mItem; + private TextView mTextSettingName; + private TextView mTextSettingValue; + private SeekBar mSeekBar; + + public SeekBarSettingViewHolder(View itemView) { + super(itemView); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingValue = root.findViewById(R.id.text_setting_value); + mSeekBar = root.findViewById(R.id.seekbar); + mSeekBar.setProgress(99); + } + + @Override + public void bind(SettingsItem item) { + mItem = item; + mTextSettingName.setText(item.getName()); + mSeekBar.setMax(100); + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean b) { + if (seekBar.getMax() > 99) { + progress = (progress / 5) * 5; + mTextSettingValue.setText(progress + "%"); + } else { + mTextSettingValue.setText(String.valueOf(progress)); + } + mItem.setValue(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }); + mSeekBar.setProgress(item.getValue()); + } + + @Override + public void onClick(View clicked) {} + } + + public class SettingsAdapter extends RecyclerView.Adapter { + private int[] mRunningSettings; + private int mJoystickRelative; + private int mControllerScale; + private int mControllerAlpha; + private int mEmulateMotionByTouch; + private ArrayList mSettings; + + public SettingsAdapter() { + int i = 0; + EmulationActivity activity = (EmulationActivity)NativeLibrary.getEmulationContext(); + mRunningSettings = NativeLibrary.getRunningSettings(); + mSettings = new ArrayList<>(); + + // pref settings + mJoystickRelative = InputOverlay.sJoystickRelative ? 1 : 0; + mSettings.add(new SettingsItem(SettingsItem.SETTING_JOYSTICK_RELATIVE, + R.string.joystick_relative_center, + SettingsItem.TYPE_CHECKBOX, mJoystickRelative)); + + mControllerScale = InputOverlay.sControllerScale; + mSettings.add(new SettingsItem(SettingsItem.SETTING_CONTROLLER_SCALE, + R.string.controller_scale, SettingsItem.TYPE_SEEK_BAR, + mControllerScale)); + + mControllerAlpha = InputOverlay.sControllerAlpha; + mSettings.add(new SettingsItem(SettingsItem.SETTING_CONTROLLER_ALPHA, + R.string.controller_alpha, SettingsItem.TYPE_SEEK_BAR, + mControllerAlpha)); + + mEmulateMotionByTouch = InputOverlay.sEmulateMotionByTouch ? 1 : 0; + mSettings.add(new SettingsItem(SettingsItem.SETTING_EMULATE_MOTION_BY_TOUCH, + R.string.emulate_motion_by_touch, + SettingsItem.TYPE_CHECKBOX, mEmulateMotionByTouch)); + + // native settings + mSettings.add(new SettingsItem(SettingsItem.SETTING_CORE_TICKS_HACK, + R.string.setting_core_ticks_hack, + SettingsItem.TYPE_CHECKBOX, mRunningSettings[i++])); + mSettings.add(new SettingsItem(SettingsItem.SETTING_LAYOUT_SINGLE_SCREEN, + R.string.single_screen, SettingsItem.TYPE_CHECKBOX, + mRunningSettings[i++])); + } + + @NonNull + @Override + public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView; + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (viewType) { + case SettingsItem.TYPE_CHECKBOX: + itemView = inflater.inflate(R.layout.list_item_running_checkbox, parent, false); + return new CheckBoxSettingViewHolder(itemView); + case SettingsItem.TYPE_SEEK_BAR: + itemView = inflater.inflate(R.layout.list_item_running_seekbar, parent, false); + return new SeekBarSettingViewHolder(itemView); + } + return null; + } + + @Override + public int getItemCount() { + return mSettings.size(); + } + + @Override + public int getItemViewType(int position) { + return mSettings.get(position).getType(); + } + + @Override + public void onBindViewHolder(@NonNull SettingViewHolder holder, int position) { + holder.bind(mSettings.get(position)); + } + + public void saveSettings() { + EmulationActivity activity = (EmulationActivity)NativeLibrary.getEmulationContext(); + + // pref settings + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(activity).edit(); + int relative = mSettings.get(0).getValue(); + if (mJoystickRelative != relative) { + editor.putBoolean(InputOverlay.PREF_JOYSTICK_RELATIVE, relative > 0); + InputOverlay.sJoystickRelative = relative > 0; + } + mSettings.remove(0); + + int scale = mSettings.get(0).getValue(); + if (mControllerScale != scale) { + editor.putInt(InputOverlay.PREF_CONTROLLER_SCALE, scale); + InputOverlay.sControllerScale = scale; + } + mSettings.remove(0); + + int alpha = mSettings.get(0).getValue(); + if (mControllerAlpha != alpha) { + editor.putInt(InputOverlay.PREF_CONTROLLER_ALPHA, alpha); + InputOverlay.sControllerAlpha = alpha; + } + mSettings.remove(0); + + int motion = mSettings.get(0).getValue(); + if (mEmulateMotionByTouch != motion) { + InputOverlay.sEmulateMotionByTouch = motion > 0; + } + mSettings.remove(0); + + // applay prefs + editor.apply(); + activity.refreshControls(); + + // native settings + boolean isChanged = false; + int[] newSettings = new int[mRunningSettings.length]; + for (int i = 0; i < mRunningSettings.length; ++i) { + newSettings[i] = mSettings.get(i).getValue(); + if (newSettings[i] != mRunningSettings[i]) { + isChanged = true; + } + } + if (isChanged) { + NativeLibrary.setRunningSettings(newSettings); + } + } + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/utils/ControllerMappingHelper.java b/src/android/app/src/main/java/org/citra/emu/utils/ControllerMappingHelper.java new file mode 100644 index 00000000000..c1c76004c5f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/utils/ControllerMappingHelper.java @@ -0,0 +1,52 @@ +package org.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 static 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 static 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; + } + } + return value; + } + + private static boolean isDualShock4(InputDevice inputDevice) { + // Sony DualShock 4 controller + return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; + } + + private static boolean isXboxOneWireless(InputDevice inputDevice) { + // Microsoft Xbox One controller + return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/citra/emu/utils/DirectoryInitialization.java new file mode 100644 index 00000000000..1a815c58aa6 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/utils/DirectoryInitialization.java @@ -0,0 +1,162 @@ +package org.citra.emu.utils; + +import android.content.Context; +import android.content.Intent; +import android.os.Environment; +import android.support.v4.content.LocalBroadcastManager; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; +import org.citra.emu.NativeLibrary; + +public final class DirectoryInitialization { + public static final String BROADCAST_ACTION = "org.citra.emu.DIRECTORY_INITIALIZATION"; + + public static final String EXTRA_STATE = "DirectoryState"; + private static volatile DirectoryInitializationState sDirectoryState; + private static String mUserPath; + private static AtomicBoolean mIsRunning = 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 (!mIsRunning.compareAndSet(false, true)) + return; + + if (sDirectoryState != DirectoryInitializationState.DIRECTORIES_INITIALIZED) { + if (PermissionsHandler.hasWriteAccess(context)) { + if (setUserDirectory()) { + initializeExternalStorage(context); + sDirectoryState = DirectoryInitializationState.DIRECTORIES_INITIALIZED; + } else { + sDirectoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; + } + } else { + sDirectoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; + } + } + + mIsRunning.set(false); + sendBroadcastState(sDirectoryState, context); + } + + private static boolean setUserDirectory() { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + File externalPath = Environment.getExternalStorageDirectory(); + if (externalPath != null) { + File userPath = new File(externalPath, "citra-emu"); + if (!userPath.isDirectory() && !userPath.mkdir()) { + return false; + } + mUserPath = userPath.getPath(); + NativeLibrary.SetUserPath(mUserPath); + return true; + } + } + return false; + } + + private static void initializeExternalStorage(Context context) { + File shaders = new File(getShadersDirectory()); + copyAssetFolder("shaders", shaders, false, context); + } + + private static void deleteDirectoryRecursively(File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + deleteDirectoryRecursively(child); + } + file.delete(); + } + + public static boolean isReady() { + return sDirectoryState == DirectoryInitializationState.DIRECTORIES_INITIALIZED; + } + + public static String getUserDirectory() { + return mUserPath; + } + + public static File getCheatFile(String programId) { + File cheatsPath = new File(mUserPath, "cheats"); + if (!cheatsPath.isDirectory() && !cheatsPath.mkdir()) { + return null; + } + return new File(cheatsPath, programId + ".txt"); + } + + public static String getConfigFile() { + return getUserDirectory() + File.separator + "config" + File.separator + "config-mmj.ini"; + } + + public static String getShadersDirectory() { + return getUserDirectory() + File.separator + "shaders"; + } + + 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) { + 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) { + } + } + + private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite, + Context context) { + 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) { + } + } + + public static void copyFile(String from, String to) { + try { + InputStream in = new FileInputStream(from); + OutputStream out = new FileOutputStream(to); + copyFile(in, out); + } catch (IOException e) { + } + } + + private static void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + + public enum DirectoryInitializationState { + DIRECTORIES_INITIALIZED, + EXTERNAL_STORAGE_PERMISSION_NEEDED, + CANT_FIND_EXTERNAL_STORAGE + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/emu/utils/FileBrowserHelper.java new file mode 100644 index 00000000000..1e9717d06bb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/utils/FileBrowserHelper.java @@ -0,0 +1,59 @@ +package org.citra.emu.utils; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.support.annotation.Nullable; +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.nononsenseapps.filepicker.Utils; +import java.io.File; +import java.util.List; +import org.citra.emu.ui.GameFilePickerActivity; +import org.citra.emu.ui.MainActivity; + +public final class FileBrowserHelper { + public static void openDirectoryPicker(Activity activity) { + Intent i = new Intent(activity, GameFilePickerActivity.class); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + activity.startActivityForResult(i, MainActivity.REQUEST_ADD_DIRECTORY); + } + + public static void openFilePicker(Activity activity, int requestCode) { + Intent i = new Intent(activity, GameFilePickerActivity.class); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, true); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + activity.startActivityForResult(i, requestCode); + } + + @Nullable + public static String getSelectedDirectory(Intent result) { + // Use the provided utility method to parse the result + List 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 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; + } +} diff --git a/src/android/app/src/main/java/org/citra/emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/citra/emu/utils/PermissionsHandler.java new file mode 100644 index 00000000000..4784a608845 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/emu/utils/PermissionsHandler.java @@ -0,0 +1,94 @@ +package org.citra.emu.utils; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Build; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; +import org.citra.emu.R; + +public final class PermissionsHandler { + public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + public static final int REQUEST_CODE_CAMERA_PERMISSION = 501; + + @TargetApi(Build.VERSION_CODES.M) + public static boolean checkWritePermission(final Activity activity) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + + int hasWritePermission = + ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE); + + if (hasWritePermission != PackageManager.PERMISSION_GRANTED) { + if (activity.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { + showMessageOKCancel( + activity, activity.getString(R.string.write_permission_needed), + (dialog, which) + -> activity.requestPermissions(new String[] {WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION)); + return false; + } + + activity.requestPermissions(new String[] {WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION); + return false; + } + + return true; + } + + @TargetApi(Build.VERSION_CODES.M) + public static boolean checkCameraPermission(final Activity activity) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + + int permission = ContextCompat.checkSelfPermission(activity, CAMERA); + if (permission != PackageManager.PERMISSION_GRANTED) { + if (activity.shouldShowRequestPermissionRationale(CAMERA)) { + showMessageOKCancel(activity, activity.getString(R.string.camera_permission_needed), + (dialog, which) + -> activity.requestPermissions( + new String[] {CAMERA}, REQUEST_CODE_CAMERA_PERMISSION)); + return false; + } + + activity.requestPermissions(new String[] {CAMERA}, REQUEST_CODE_CAMERA_PERMISSION); + return false; + } + + return true; + } + + public static boolean hasWriteAccess(Context context) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int hasWritePermission = + ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE); + return hasWritePermission == PackageManager.PERMISSION_GRANTED; + } + + return true; + } + + private static void showMessageOKCancel(final Context context, String message, + DialogInterface.OnClickListener okListener) { + new AlertDialog.Builder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton( + android.R.string.cancel, + (dialogInterface, i) + -> Toast.makeText(context, R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show()) + .create() + .show(); + } +} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java b/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java deleted file mode 100644 index 10cb5278308..00000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java +++ /dev/null @@ -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"); - } -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/LOG.java b/src/android/app/src/main/java/org/citra_emu/citra/LOG.java deleted file mode 100644 index c52f30b6844..00000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/LOG.java +++ /dev/null @@ -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); -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java deleted file mode 100644 index 5b4f3d3bc59..00000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java +++ /dev/null @@ -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(); -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java b/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java deleted file mode 100644 index 5346c53524b..00000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java +++ /dev/null @@ -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; - } -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java b/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java deleted file mode 100644 index 33c8129e5b8..00000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java +++ /dev/null @@ -1,32 +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.app.Activity; -import android.content.pm.PackageManager; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; - -public class PermissionUtil { - - /** - * Checks a permission, if needed shows a dialog to request it - * - * @param activity the activity requiring the permission - * @param permission the permission needed - * @param requestCode supplied to the callback to determine the next action - */ - public static void verifyPermission(Activity activity, String permission, int requestCode) { - if (ContextCompat.checkSelfPermission(activity, permission) == - PackageManager.PERMISSION_GRANTED) { - // call the callback called by requestPermissions - activity.onRequestPermissionsResult(requestCode, new String[] {permission}, - new int[] {PackageManager.PERMISSION_GRANTED}); - return; - } - - ActivityCompat.requestPermissions(activity, new String[] {permission}, requestCode); - } -} diff --git a/src/android/app/src/main/res/animator/settings_enter.xml b/src/android/app/src/main/res/animator/settings_enter.xml new file mode 100644 index 00000000000..23ba7b802b2 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_enter.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/settings_exit.xml b/src/android/app/src/main/res/animator/settings_exit.xml new file mode 100644 index 00000000000..fece2c34ea2 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_exit.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/settings_pop_enter.xml b/src/android/app/src/main/res/animator/settings_pop_enter.xml new file mode 100644 index 00000000000..36c8a27e7a9 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_pop_enter.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/setttings_pop_exit.xml b/src/android/app/src/main/res/animator/setttings_pop_exit.xml new file mode 100644 index 00000000000..dd16018f23a --- /dev/null +++ b/src/android/app/src/main/res/animator/setttings_pop_exit.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_a.png b/src/android/app/src/main/res/drawable-hdpi/classic_a.png new file mode 100644 index 00000000000..bd76083b92f Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_a.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_a_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_a_pressed.png new file mode 100644 index 00000000000..46a1010230c Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_b.png b/src/android/app/src/main/res/drawable-hdpi/classic_b.png new file mode 100644 index 00000000000..c85ea4f36d0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_b.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_b_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_b_pressed.png new file mode 100644 index 00000000000..3b1b2cd5313 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_l.png b/src/android/app/src/main/res/drawable-hdpi/classic_l.png new file mode 100644 index 00000000000..b4592bea96a Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_l.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_l_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_l_pressed.png new file mode 100644 index 00000000000..40872bea058 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_r.png b/src/android/app/src/main/res/drawable-hdpi/classic_r.png new file mode 100644 index 00000000000..04d27f82f45 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_r.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_r_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_r_pressed.png new file mode 100644 index 00000000000..bdbf9f78650 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_x.png b/src/android/app/src/main/res/drawable-hdpi/classic_x.png new file mode 100644 index 00000000000..0049add3d87 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_x.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_x_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_x_pressed.png new file mode 100644 index 00000000000..64692fb3008 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_y.png b/src/android/app/src/main/res/drawable-hdpi/classic_y.png new file mode 100644 index 00000000000..96a48a27513 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_y.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_y_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_y_pressed.png new file mode 100644 index 00000000000..51d6a31a1b2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_zl.png b/src/android/app/src/main/res/drawable-hdpi/classic_zl.png new file mode 100644 index 00000000000..f6053466854 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_zl.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_zl_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_zl_pressed.png new file mode 100644 index 00000000000..0b479c0f247 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_zr.png b/src/android/app/src/main/res/drawable-hdpi/classic_zr.png new file mode 100644 index 00000000000..57463183635 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_zr.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/classic_zr_pressed.png b/src/android/app/src/main/res/drawable-hdpi/classic_zr_pressed.png new file mode 100644 index 00000000000..6879b0dfb98 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/classic_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcpad_start.png b/src/android/app/src/main/res/drawable-hdpi/gcpad_start.png new file mode 100644 index 00000000000..0c2f395b8cc Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcpad_start.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcpad_start_pressed.png b/src/android/app/src/main/res/drawable-hdpi/gcpad_start_pressed.png new file mode 100644 index 00000000000..6be57cddc2c Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcpad_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad.png new file mode 100644 index 00000000000..560ba7527c2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_one_direction.png new file mode 100644 index 00000000000..b7efe0887ac Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_two_directions.png new file mode 100644 index 00000000000..2d5055674a6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick.png new file mode 100644 index 00000000000..d68e2990bd9 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_pressed.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_pressed.png new file mode 100644 index 00000000000..371581bae90 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_range.png b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_range.png new file mode 100644 index 00000000000..34a81aae481 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/gcwii_joystick_range.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/wiimote_home.png b/src/android/app/src/main/res/drawable-hdpi/wiimote_home.png new file mode 100644 index 00000000000..7a34ed6723c Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/wiimote_home.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/wiimote_home_pressed.png b/src/android/app/src/main/res/drawable-hdpi/wiimote_home_pressed.png new file mode 100644 index 00000000000..611a493f8ed Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/wiimote_home_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/wiimote_plus.png b/src/android/app/src/main/res/drawable-hdpi/wiimote_plus.png new file mode 100644 index 00000000000..033eb899bfe Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/wiimote_plus.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/wiimote_plus_pressed.png b/src/android/app/src/main/res/drawable-hdpi/wiimote_plus_pressed.png new file mode 100644 index 00000000000..74b3bddd8bc Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/wiimote_plus_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21dbd86..00000000000 --- a/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_a.png b/src/android/app/src/main/res/drawable-xhdpi/classic_a.png new file mode 100644 index 00000000000..831d5d45caa Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_a.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_a_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_a_pressed.png new file mode 100644 index 00000000000..bbea374ea3f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_b.png b/src/android/app/src/main/res/drawable-xhdpi/classic_b.png new file mode 100644 index 00000000000..4facae6f73c Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_b.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_b_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_b_pressed.png new file mode 100644 index 00000000000..5208a86fadd Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_l.png b/src/android/app/src/main/res/drawable-xhdpi/classic_l.png new file mode 100644 index 00000000000..14257ce5976 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_l.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_l_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_l_pressed.png new file mode 100644 index 00000000000..3c454feafab Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_r.png b/src/android/app/src/main/res/drawable-xhdpi/classic_r.png new file mode 100644 index 00000000000..e253ca591bc Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_r.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_r_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_r_pressed.png new file mode 100644 index 00000000000..29b7174f6b2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_x.png b/src/android/app/src/main/res/drawable-xhdpi/classic_x.png new file mode 100644 index 00000000000..7ec7e8e123f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_x.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_x_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_x_pressed.png new file mode 100644 index 00000000000..bbd92dd7aa6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_y.png b/src/android/app/src/main/res/drawable-xhdpi/classic_y.png new file mode 100644 index 00000000000..6a047253399 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_y.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_y_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_y_pressed.png new file mode 100644 index 00000000000..757e20ee5d0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_zl.png b/src/android/app/src/main/res/drawable-xhdpi/classic_zl.png new file mode 100644 index 00000000000..a7604e31d49 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_zl_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_zl_pressed.png new file mode 100644 index 00000000000..9dc62442d8e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_zr.png b/src/android/app/src/main/res/drawable-xhdpi/classic_zr.png new file mode 100644 index 00000000000..657e120afbb Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/classic_zr_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/classic_zr_pressed.png new file mode 100644 index 00000000000..33e8a3a0c15 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/classic_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcpad_start.png b/src/android/app/src/main/res/drawable-xhdpi/gcpad_start.png new file mode 100644 index 00000000000..e32c624992b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcpad_start.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcpad_start_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/gcpad_start_pressed.png new file mode 100644 index 00000000000..80ffee644b0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcpad_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad.png new file mode 100644 index 00000000000..fb04fd3fe1b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_one_direction.png new file mode 100644 index 00000000000..7cf85d806e9 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_two_directions.png new file mode 100644 index 00000000000..71eb4697673 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick.png new file mode 100644 index 00000000000..cc2d7c00f65 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_pressed.png new file mode 100644 index 00000000000..4e59c15545b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_range.png b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_range.png new file mode 100644 index 00000000000..aa0bacbdd9b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/gcwii_joystick_range.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/wiimote_home.png b/src/android/app/src/main/res/drawable-xhdpi/wiimote_home.png new file mode 100644 index 00000000000..ada72c21f11 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/wiimote_home.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/wiimote_home_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/wiimote_home_pressed.png new file mode 100644 index 00000000000..3e9e256c3e0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/wiimote_home_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus.png b/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus.png new file mode 100644 index 00000000000..3677e954eaf Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus_pressed.png new file mode 100644 index 00000000000..8740b706954 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/wiimote_plus_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_a.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_a.png new file mode 100644 index 00000000000..05c6357c546 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_a.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_a_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_a_pressed.png new file mode 100644 index 00000000000..010db7cbde6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_b.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_b.png new file mode 100644 index 00000000000..26721af2aa4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_b.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_b_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_b_pressed.png new file mode 100644 index 00000000000..cc03d77e98e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_l.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_l.png new file mode 100644 index 00000000000..5522bdd5144 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_l.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_l_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_l_pressed.png new file mode 100644 index 00000000000..f1cd89d2525 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_r.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_r.png new file mode 100644 index 00000000000..8beaf45f1d8 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_r.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_r_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_r_pressed.png new file mode 100644 index 00000000000..917c9d0f5cd Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_x.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_x.png new file mode 100644 index 00000000000..537ff8ce498 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_x.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_x_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_x_pressed.png new file mode 100644 index 00000000000..2cb3672fa1a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_y.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_y.png new file mode 100644 index 00000000000..6ab94a40bd7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_y.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_y_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_y_pressed.png new file mode 100644 index 00000000000..55a0ce7b45e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_zl.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_zl.png new file mode 100644 index 00000000000..5e507417c09 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_zl_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_zl_pressed.png new file mode 100644 index 00000000000..958e050d991 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_zr.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_zr.png new file mode 100644 index 00000000000..62167e8c060 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/classic_zr_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/classic_zr_pressed.png new file mode 100644 index 00000000000..10d4e653b04 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/classic_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start.png b/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start.png new file mode 100644 index 00000000000..7fa6e86c84f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start_pressed.png new file mode 100644 index 00000000000..ff1cd509ce0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcpad_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad.png new file mode 100644 index 00000000000..a2cc3e6af64 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_one_direction.png new file mode 100644 index 00000000000..cdf1ebbd0ac Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_two_directions.png new file mode 100644 index 00000000000..fb99808b6c1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick.png new file mode 100644 index 00000000000..97cc75a8687 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_pressed.png new file mode 100644 index 00000000000..ee22abc2c30 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_range.png b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_range.png new file mode 100644 index 00000000000..2f045b7e3cb Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/gcwii_joystick_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home.png b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home.png new file mode 100644 index 00000000000..9fcc3d2eadb Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home_pressed.png new file mode 100644 index 00000000000..ac4ba19c43f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_home_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus.png b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus.png new file mode 100644 index 00000000000..63ba736cea6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus_pressed.png new file mode 100644 index 00000000000..6748cf66d2d Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/wiimote_plus_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_a.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_a.png new file mode 100644 index 00000000000..6277c6bd924 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_a.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_a_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_a_pressed.png new file mode 100644 index 00000000000..ff2066c0863 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_b.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_b.png new file mode 100644 index 00000000000..19ef9ff81c0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_b.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_b_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_b_pressed.png new file mode 100644 index 00000000000..132ff7a963e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_l.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_l.png new file mode 100644 index 00000000000..0061173af2f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_l.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_l_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_l_pressed.png new file mode 100644 index 00000000000..d1729913638 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_r.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_r.png new file mode 100644 index 00000000000..883a7fb710b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_r.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_r_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_r_pressed.png new file mode 100644 index 00000000000..6725a034e2e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_x.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_x.png new file mode 100644 index 00000000000..36aae0bfca1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_x.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_x_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_x_pressed.png new file mode 100644 index 00000000000..e973dd4887e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_y.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_y.png new file mode 100644 index 00000000000..13993c9a191 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_y.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_y_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_y_pressed.png new file mode 100644 index 00000000000..43990d2e848 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl.png new file mode 100644 index 00000000000..5be5792a169 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl_pressed.png new file mode 100644 index 00000000000..89aa9859711 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr.png new file mode 100644 index 00000000000..0b87c026f82 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr_pressed.png new file mode 100644 index 00000000000..91f9b12477a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/classic_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start.png new file mode 100644 index 00000000000..482b1ff65e1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start_pressed.png new file mode 100644 index 00000000000..ee117dd865e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcpad_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad.png new file mode 100644 index 00000000000..11931ab1ab9 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_one_direction.png new file mode 100644 index 00000000000..4f0219b3a45 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_two_directions.png new file mode 100644 index 00000000000..230a1486ef8 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick.png new file mode 100644 index 00000000000..fd4d741b81c Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_pressed.png new file mode 100644 index 00000000000..62790f413e7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_range.png b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_range.png new file mode 100644 index 00000000000..0a69a3c6398 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/gcwii_joystick_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home.png b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home.png new file mode 100644 index 00000000000..ca593612791 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home_pressed.png new file mode 100644 index 00000000000..c42e1065cb2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_home_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus.png b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus.png new file mode 100644 index 00000000000..8006c97269a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus_pressed.png new file mode 100644 index 00000000000..ddf8427eb05 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/wiimote_plus_pressed.png differ diff --git a/src/android/app/src/main/res/drawable/ic_launcher_background.xml b/src/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc538c1..00000000000 --- a/src/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/android/app/src/main/res/drawable/line_divider.xml b/src/android/app/src/main/res/drawable/line_divider.xml new file mode 100644 index 00000000000..45edb2a9bbe --- /dev/null +++ b/src/android/app/src/main/res/drawable/line_divider.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/android/app/src/main/res/drawable/nnf_ic_folder_black_48dp.xml b/src/android/app/src/main/res/drawable/nnf_ic_folder_black_48dp.xml new file mode 100644 index 00000000000..62be2e5082b --- /dev/null +++ b/src/android/app/src/main/res/drawable/nnf_ic_folder_black_48dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/nnf_ic_save_black_24dp.xml b/src/android/app/src/main/res/drawable/nnf_ic_save_black_24dp.xml new file mode 100644 index 00000000000..a561d632a54 --- /dev/null +++ b/src/android/app/src/main/res/drawable/nnf_ic_save_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/no_banner.png b/src/android/app/src/main/res/drawable/no_banner.png new file mode 100644 index 00000000000..f296879e426 Binary files /dev/null and b/src/android/app/src/main/res/drawable/no_banner.png differ diff --git a/src/android/app/src/main/res/layout/activity_editor.xml b/src/android/app/src/main/res/layout/activity_editor.xml new file mode 100644 index 00000000000..f33ba67b604 --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_editor.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + +