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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml
new file mode 100644
index 00000000000..c5c9477f2fb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_emulation.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
index d13b8e03ef3..d8f63a32c94 100644
--- a/src/android/app/src/main/res/layout/activity_main.xml
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -1,50 +1,29 @@
-
+
-
+ android:layout_height="?attr/actionBarSize"
+ android:layout_alignParentTop="true" />
-
-
-
-
-
+
-
-
-
+ android:layout_height="match_parent"/>
-
+
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 00000000000..492fbf04ecc
--- /dev/null
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
new file mode 100644
index 00000000000..891d0e57b4c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/dialog_input_box.xml b/src/android/app/src/main/res/layout/dialog_input_box.xml
new file mode 100644
index 00000000000..b0bd78f512c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_input_box.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/dialog_mii_selector.xml b/src/android/app/src/main/res/layout/dialog_mii_selector.xml
new file mode 100644
index 00000000000..2c414552d3e
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_mii_selector.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/dialog_running_settings.xml b/src/android/app/src/main/res/layout/dialog_running_settings.xml
new file mode 100644
index 00000000000..977bb0c82c6
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_running_settings.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/dialog_seekbar.xml b/src/android/app/src/main/res/layout/dialog_seekbar.xml
new file mode 100644
index 00000000000..62490549e10
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_seekbar.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
new file mode 100644
index 00000000000..f687454c801
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml
new file mode 100644
index 00000000000..db3ab8e7429
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_running_checkbox.xml b/src/android/app/src/main/res/layout/list_item_running_checkbox.xml
new file mode 100644
index 00000000000..358854e52fe
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_running_checkbox.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_running_seekbar.xml b/src/android/app/src/main/res/layout/list_item_running_seekbar.xml
new file mode 100644
index 00000000000..dbe5b4ddf39
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_running_seekbar.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
new file mode 100644
index 00000000000..d8adc26476a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml
new file mode 100644
index 00000000000..66427451312
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_setting_seekbar.xml b/src/android/app/src/main/res/layout/list_item_setting_seekbar.xml
new file mode 100644
index 00000000000..06fe66b5a5a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_seekbar.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml
new file mode 100644
index 00000000000..13b1bf113f3
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/nnf_activity_filepicker.xml b/src/android/app/src/main/res/layout/nnf_activity_filepicker.xml
new file mode 100644
index 00000000000..b40b45b3831
--- /dev/null
+++ b/src/android/app/src/main/res/layout/nnf_activity_filepicker.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/nnf_dialog_folder_name.xml b/src/android/app/src/main/res/layout/nnf_dialog_folder_name.xml
new file mode 100644
index 00000000000..699066487f1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/nnf_dialog_folder_name.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/nnf_filepicker_listitem_checkable.xml b/src/android/app/src/main/res/layout/nnf_filepicker_listitem_checkable.xml
new file mode 100644
index 00000000000..4b06d7c0719
--- /dev/null
+++ b/src/android/app/src/main/res/layout/nnf_filepicker_listitem_checkable.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/nnf_filepicker_listitem_dir.xml b/src/android/app/src/main/res/layout/nnf_filepicker_listitem_dir.xml
new file mode 100644
index 00000000000..d34e5566695
--- /dev/null
+++ b/src/android/app/src/main/res/layout/nnf_filepicker_listitem_dir.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/nnf_fragment_filepicker.xml b/src/android/app/src/main/res/layout/nnf_fragment_filepicker.xml
new file mode 100644
index 00000000000..9a7be61452d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/nnf_fragment_filepicker.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml
new file mode 100644
index 00000000000..947c0a00434
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_emulation.xml
@@ -0,0 +1,23 @@
+
diff --git a/src/android/app/src/main/res/menu/menu_main.xml b/src/android/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000000..247a9313442
--- /dev/null
+++ b/src/android/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,32 @@
+
+
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_country.png b/src/android/app/src/main/res/mipmap-hdpi/ic_country.png
new file mode 100644
index 00000000000..6e71c3a40f8
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-hdpi/ic_country.png differ
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_screenshot.png b/src/android/app/src/main/res/mipmap-hdpi/ic_screenshot.png
new file mode 100644
index 00000000000..f93c09e4cce
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-hdpi/ic_screenshot.png differ
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_settings_core.png b/src/android/app/src/main/res/mipmap-hdpi/ic_settings_core.png
new file mode 100644
index 00000000000..adc23238181
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-hdpi/ic_settings_core.png differ
diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_settings_gcpad.png b/src/android/app/src/main/res/mipmap-hdpi/ic_settings_gcpad.png
new file mode 100644
index 00000000000..524a8c570a0
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-hdpi/ic_settings_gcpad.png differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_country.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_country.png
new file mode 100644
index 00000000000..5d500f5f041
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xhdpi/ic_country.png differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_screenshot.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_screenshot.png
new file mode 100644
index 00000000000..ae19dd41121
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xhdpi/ic_screenshot.png differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_core.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_core.png
new file mode 100644
index 00000000000..6d00c290ad0
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_core.png differ
diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_gcpad.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_gcpad.png
new file mode 100644
index 00000000000..81280d05b11
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xhdpi/ic_settings_gcpad.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_country.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_country.png
new file mode 100644
index 00000000000..077ae487a01
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_country.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_screenshot.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_screenshot.png
new file mode 100644
index 00000000000..1fe0b70f1fe
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_screenshot.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_core.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_core.png
new file mode 100644
index 00000000000..9d571a14f31
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_core.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_gcpad.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_gcpad.png
new file mode 100644
index 00000000000..1063b3d0ee1
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_settings_gcpad.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_country.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_country.png
new file mode 100644
index 00000000000..27c03a52325
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_country.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_screenshot.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_screenshot.png
new file mode 100644
index 00000000000..f9eaa0d2e4e
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_screenshot.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_core.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_core.png
new file mode 100644
index 00000000000..361bc89afd5
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_core.png differ
diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_gcpad.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_gcpad.png
new file mode 100644
index 00000000000..cb8e91181da
Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_settings_gcpad.png differ
diff --git a/src/android/app/src/main/res/values-zh/strings.xml b/src/android/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000000..88008da5180
--- /dev/null
+++ b/src/android/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,98 @@
+
+
+
+
+ Citra
+ Citra %1$s
+ app
+ citra
+
+ 错误
+ 输入长度限制:%1$d.
+
+ FMV Hack
+ 触摸屏模拟体感
+ 摇杆使用相对位置
+ 按键缩放
+ 按键透明度
+
+ 刷新游戏库
+ 手柄绑定
+ 安装 CIA
+ 设置
+
+ 编辑布局
+ 确定
+ 输入绑定
+ 请按键将其绑定到 <b>%1$s</b>。
+ 截图
+ 将文件夹添加到游戏库
+
+ 系统设置
+ 图形设置
+ 声音设置
+
+ New 3DS Mode
+ 使用虚拟SD卡
+ 模拟地区
+
+ 显示帧率
+ 屏幕布局
+ 内部分辨率
+ 开启硬件着色器
+ 精确乘法运算
+ 后处理效果
+
+ 音频输出
+ 音频拉伸
+
+ 单屏显示
+ 大屏显示
+ 并排显示
+ 默认
+ 自动
+ 关闭
+ 清除
+ 保存
+ 金手指
+ You need to allow write access to external storage for the emulator to work.
+ You need to allow take photos for the emulator to work.
+
+ 未知
+ 日本
+ 北美
+ 欧洲
+ 澳大利亚
+ 中国
+ 韩国
+ 台湾
+
+ 主摇杆
+ 方向键
+ 按键
+ A
+ B
+ X
+ Y
+ Up
+ Down
+ Left
+ Right
+ L
+ R
+ START
+ SELECT
+
+ New folder
+ Failed to create folder
+ Name
+ Please select something first
+ Please enter or select a valid filename
+ Permisson to access filesystem denied
+ Filename
+ @android:string/ok
+ @android:string/cancel
+ @android:string/ok
+ @android:string/cancel
+
+
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000000..e9a5f38e27f
--- /dev/null
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ - @string/default_value
+ - @string/single_screen
+ - @string/large_screen
+ - @string/side_screen
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+
+
+
+
+ - 1x (400x240)
+ - 2x (800x480)
+ - 3x (1200x720)
+ - 4x (1600x960)
+
+
+ - 1
+ - 2
+ - 3
+ - 4
+
+
+
+
+ - @string/auto
+ - @string/region_japan
+ - @string/region_north_america
+ - @string/region_europe
+ - @string/region_australia
+ - @string/region_china
+ - @string/region_korea
+ - @string/region_taiwan
+
+
+ - -1
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+
+
+
+
+ - @string/off
+ - @string/auto
+
+
+ - null
+ - auto
+
+
+
diff --git a/src/android/app/src/main/res/values/attr.xml b/src/android/app/src/main/res/values/attr.xml
new file mode 100644
index 00000000000..f7ccc9e4119
--- /dev/null
+++ b/src/android/app/src/main/res/values/attr.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/values/colors.xml b/src/android/app/src/main/res/values/colors.xml
index d0d2e5b1a24..f2459f03fc6 100644
--- a/src/android/app/src/main/res/values/colors.xml
+++ b/src/android/app/src/main/res/values/colors.xml
@@ -3,13 +3,20 @@
#fec303
#fe8a03
+ #f05053
#9e9e9e
#2979ff
#651fff
+ #fec303
+
#bdbdbd
+ #e6e6e6
- #444444
+
+ #1e000000
+
+ #1effffff
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000000..2d60158b076
--- /dev/null
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -0,0 +1,12 @@
+
+
+ 16dp
+
+ 1dp
+ 4dp
+
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+
diff --git a/src/android/app/src/main/res/values/ic_launcher_background.xml b/src/android/app/src/main/res/values/ic_launcher_background.xml
deleted file mode 100644
index c5d5899fdf0..00000000000
--- a/src/android/app/src/main/res/values/ic_launcher_background.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #FFFFFF
-
\ No newline at end of file
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
new file mode 100644
index 00000000000..c1ffba47fce
--- /dev/null
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -0,0 +1,23 @@
+
+
+ 75
+ 70
+ 50
+ 85
+ 50
+ 55
+ 25
+ 70
+ -90
+ 40
+ 90
+ 40
+ 15
+ 96
+ -15
+ 96
+ -55
+ 80
+ -20
+ 50
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..97a88f6881f
--- /dev/null
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,98 @@
+
+
+
+
+ Citra
+ Citra %1$s
+ app
+ citra
+
+ Error
+ Max input length: %1$d.
+
+ FMV Hack
+ Emulate motion by touch
+ Joystick use relative center
+ Controller Scale
+ Controller Opacity
+
+ Refresh Library
+ Input Binding
+ Install CIA
+ Settings
+
+ Edit Layout
+ Done
+ Input Binding
+ Press or move an input to bind it to <b>%1$s</b>.
+ Take Screenshot
+ Add Folder to Library
+
+ System
+ Renderer
+ Audio
+
+ New 3DS Mode
+ Use Virtual SD
+ Emulation Region
+
+ Show FPS
+ Screen Layout
+ Internal Resolution
+ Enable Hardware Shader
+ Accurate Multiplication
+ Post-Processing Effect
+
+ Audio Output
+ Enable Audio Stretching
+
+ Single Screen
+ Large Screen
+ Side Screen
+ Default
+ Auto
+ Off
+ Clear
+ Save
+ Cheat Code
+ You need to allow write access to external storage for the emulator to work.
+ You need to allow take photos for the emulator to work.
+
+ Unknown
+ Japan
+ North America
+ Europe
+ Australia
+ China
+ Korea
+ Taiwan
+
+ Main Stick
+ D-Pad
+ Buttons
+ A
+ B
+ X
+ Y
+ Up
+ Down
+ Left
+ Right
+ L
+ R
+ START
+ SELECT
+
+ New folder
+ Failed to create folder
+ Name
+ Please select something first
+ Please enter or select a valid filename
+ Permisson to access filesystem denied
+ Filename
+ @android:string/ok
+ @android:string/cancel
+ @android:string/ok
+ @android:string/cancel
+
+
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
index 2243a9a69ef..922c5737a81 100644
--- a/src/android/app/src/main/res/values/styles.xml
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -1,6 +1,30 @@
+
+
+
+
@@ -23,10 +45,6 @@
- @color/citra_orange_dark
-
-
-
-
-
-
-
-
+
+ - @drawable/line_divider
-
+
+ - @style/FilePickerAlertDialogTheme
+
+
-
+
diff --git a/src/android/app/src/main/res/xml/network_security_config.xml b/src/android/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000000..f30673bca5e
--- /dev/null
+++ b/src/android/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/xml/nnf_provider_paths.xml b/src/android/app/src/main/res/xml/nnf_provider_paths.xml
new file mode 100644
index 00000000000..96ce4065b48
--- /dev/null
+++ b/src/android/app/src/main/res/xml/nnf_provider_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java b/src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java
deleted file mode 100644
index 066ffe6fd30..00000000000
--- a/src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.citra_emu.citra;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() {
- assertEquals(4, 2 + 2);
- }
-}
diff --git a/src/android/build.gradle b/src/android/build.gradle
index df75d6a5219..a8d1d26be88 100644
--- a/src/android/build.gradle
+++ b/src/android/build.gradle
@@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.1'
+ classpath 'com.android.tools.build:gradle:3.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties
index fa4a78943a6..815bda58d32 100644
--- a/src/android/gradle/wrapper/gradle-wrapper.properties
+++ b/src/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Nov 09 14:15:11 CST 2018
+#Wed Aug 21 10:51:24 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/src/android/jni/CMakeLists.txt b/src/android/jni/CMakeLists.txt
new file mode 100644
index 00000000000..c5ab863b3f8
--- /dev/null
+++ b/src/android/jni/CMakeLists.txt
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.8)
+
+add_library(main SHARED
+ jni_common.cpp
+ jni_common.h
+ input_manager.cpp
+ input_manager.h
+ egl_android.cpp
+ egl_android.h
+ mii_selector.cpp
+ mii_selector.h
+ keyboard.cpp
+ keyboard.h
+ main_android.cpp
+ config/config.cpp
+ config/config.h
+ config/config_info.cpp
+ config/config_info.h
+ config/ini_file.cpp
+ config/ini_file.h
+ config/layer.cpp
+ config/layer.h
+ config/string_util.cpp
+ config/string_util.h
+ config/main_settings.cpp
+ config/main_settings.h
+ config/config_loader.cpp
+ config/config_loader.h
+ )
+
+target_link_libraries(main android EGL log core input_common)
+target_include_directories(main PRIVATE "./" "../../../externals/glad/include/")
diff --git a/src/android/jni/config/config.cpp b/src/android/jni/config/config.cpp
new file mode 100644
index 00000000000..0374d34309f
--- /dev/null
+++ b/src/android/jni/config/config.cpp
@@ -0,0 +1,55 @@
+// Copyright 2016 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include
+#include
+#include