Remove Android app
This repository exists solely for the desktop version now.
This commit is contained in:
parent
81da9be2d1
commit
6e49adb508
|
@ -1,14 +0,0 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
|
@ -1 +0,0 @@
|
|||
DuckStation
|
|
@ -1,116 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
|
@ -1,5 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,90 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "30.0.2"
|
||||
defaultConfig {
|
||||
applicationId "com.github.stenzek.duckstation"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode(getBuildVersionCode())
|
||||
versionName "${getVersion()}"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
ndk {
|
||||
debugSymbolLevel "FULL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "../../CMakeLists.txt"
|
||||
version "3.10.2"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DCMAKE_BUILD_TYPE=Release"
|
||||
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main.assets.srcDirs += "../../data"
|
||||
}
|
||||
|
||||
// Blocked on R21 until CMake is updated to 3.19 or later.
|
||||
ndkVersion '21.4.7075529'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
}
|
||||
|
||||
// Adapted from Dolphin.
|
||||
|
||||
def getVersion() {
|
||||
def versionNumber = '0.0-unknown'
|
||||
|
||||
try {
|
||||
versionNumber = 'git describe --tags --exclude latest --exclude preview'.execute([], project.rootDir).text
|
||||
.trim()
|
||||
.replaceAll(/(-0)?-[^-]+$/, "")
|
||||
} catch (Exception e) {
|
||||
logger.error('Cannot find git, defaulting to dummy version number')
|
||||
}
|
||||
|
||||
return versionNumber
|
||||
}
|
||||
|
||||
|
||||
def getBuildVersionCode() {
|
||||
try {
|
||||
def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text
|
||||
.trim()
|
||||
return Integer.valueOf(versionNumber);
|
||||
} catch (Exception e) {
|
||||
logger.error('Cannot find git, defaulting to dummy version number')
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,27 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.github.stenzek.duckstation", appContext.getPackageName());
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
set(SRCS
|
||||
android_controller_interface.cpp
|
||||
android_controller_interface.h
|
||||
android_host_interface.cpp
|
||||
android_host_interface.h
|
||||
android_http_downloader.cpp
|
||||
android_http_downloader.h
|
||||
android_progress_callback.cpp
|
||||
android_progress_callback.h
|
||||
android_settings_interface.cpp
|
||||
android_settings_interface.h
|
||||
)
|
||||
|
||||
add_library(duckstation-native SHARED ${SRCS})
|
||||
target_link_libraries(duckstation-native PRIVATE android frontend-common core common glad imgui)
|
||||
|
||||
find_package(OpenSLES)
|
||||
if(OPENSLES_FOUND)
|
||||
message("Enabling OpenSL ES audio stream")
|
||||
target_sources(duckstation-native PRIVATE
|
||||
opensles_audio_stream.cpp
|
||||
opensles_audio_stream.h)
|
||||
target_link_libraries(duckstation-native PRIVATE OpenSLES::OpenSLES)
|
||||
target_compile_definitions(duckstation-native PRIVATE "-DUSE_OPENSLES=1")
|
||||
endif()
|
|
@ -1,249 +0,0 @@
|
|||
#include "android_controller_interface.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(AndroidControllerInterface);
|
||||
|
||||
AndroidControllerInterface::AndroidControllerInterface() = default;
|
||||
|
||||
AndroidControllerInterface::~AndroidControllerInterface() = default;
|
||||
|
||||
ControllerInterface::Backend AndroidControllerInterface::GetBackend() const
|
||||
{
|
||||
return ControllerInterface::Backend::Android;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
if (!ControllerInterface::Initialize(host_interface))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::Shutdown()
|
||||
{
|
||||
ControllerInterface::Shutdown();
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::PollEvents() {}
|
||||
|
||||
void AndroidControllerInterface::ClearBindings()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
for (ControllerData& cd : m_controllers)
|
||||
{
|
||||
cd.axis_mapping.clear();
|
||||
cd.button_mapping.clear();
|
||||
cd.axis_button_mapping.clear();
|
||||
cd.button_axis_mapping.clear();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> AndroidControllerInterface::GetControllerIndex(const std::string_view& device)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
for (u32 i = 0; i < static_cast<u32>(m_device_names.size()); i++)
|
||||
{
|
||||
if (device == m_device_names[i])
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
Log_DevPrintf("Bound controller %d axis %d side %u", controller_index, axis_number, static_cast<unsigned>(axis_side));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
||||
Log_DevPrintf("Bound controller %d button %d", controller_index, button_number);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||
Log_DevPrintf("Bound controller %d axis %d to button", controller_index, axis_number);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||
AxisCallback callback)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
||||
Log_DevPrintf("Bound controller %d button %d to axis", controller_index, button_number);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::SetDeviceNames(std::vector<std::string> device_names)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
m_device_names = std::move(device_names);
|
||||
m_controllers.resize(m_device_names.size());
|
||||
|
||||
for (u32 i = 0; i < static_cast<u32>(m_device_names.size()); i++)
|
||||
Log_DevPrintf("Controller %u: %s", i, m_device_names[i].c_str());
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::SetDeviceRumble(u32 index, bool has_vibrator)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (index >= m_controllers.size())
|
||||
return;
|
||||
|
||||
m_controllers[index].has_rumble = has_vibrator;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (index >= m_controllers.size())
|
||||
return;
|
||||
|
||||
Log_DevPrintf("controller %u axis %u %f", index, axis, value);
|
||||
if (DoEventHook(Hook::Type::Axis, index, axis, value))
|
||||
return;
|
||||
|
||||
const ControllerData& cd = m_controllers[index];
|
||||
const auto am_iter = cd.axis_mapping.find(axis);
|
||||
if (am_iter != cd.axis_mapping.end())
|
||||
{
|
||||
const AxisCallback& cb = am_iter->second[AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(value) >= cd.deadzone);
|
||||
const bool positive = (value >= 0.0f);
|
||||
const auto bm_iter = cd.axis_button_mapping.find(axis);
|
||||
if (bm_iter != cd.axis_button_mapping.end())
|
||||
{
|
||||
const ButtonCallback& other_button_cb = bm_iter->second[BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb = bm_iter->second[BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
||||
{
|
||||
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
||||
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (index >= m_controllers.size())
|
||||
return;
|
||||
|
||||
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
||||
return;
|
||||
|
||||
const ControllerData& cd = m_controllers[index];
|
||||
const auto button_iter = cd.button_mapping.find(button);
|
||||
if (button_iter != cd.button_mapping.end() && button_iter->second)
|
||||
{
|
||||
button_iter->second(pressed);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto axis_iter = cd.button_axis_mapping.find(button);
|
||||
if (axis_iter != cd.button_axis_mapping.end() && axis_iter->second)
|
||||
{
|
||||
axis_iter->second(pressed ? 1.0f : -1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
Log_DevPrintf("controller %u button %u has no binding", index, button);
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::HasButtonBinding(u32 index, u32 button)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (index >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
const ControllerData& cd = m_controllers[index];
|
||||
return (cd.button_mapping.find(button) != cd.button_mapping.end() ||
|
||||
cd.button_axis_mapping.find(button) != cd.button_axis_mapping.end());
|
||||
}
|
||||
|
||||
u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
return m_controllers[static_cast<u32>(controller_index)].has_rumble ? NUM_RUMBLE_MOTORS : 0;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
||||
u32 num_motors)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return;
|
||||
|
||||
const float small_motor = strengths[0];
|
||||
const float large_motor = strengths[1];
|
||||
static_cast<AndroidHostInterface*>(m_host_interface)
|
||||
->SetControllerVibration(static_cast<u32>(controller_index), small_motor, large_motor);
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controllers_mutex);
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index,
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone);
|
||||
return true;
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
#pragma once
|
||||
#include "core/types.h"
|
||||
#include "frontend-common/controller_interface.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class AndroidControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
AndroidControllerInterface();
|
||||
~AndroidControllerInterface() override;
|
||||
|
||||
ALWAYS_INLINE u32 GetControllerCount() const { return static_cast<u32>(m_controllers.size()); }
|
||||
|
||||
Backend GetBackend() const override;
|
||||
bool Initialize(CommonHostInterface* host_interface) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
std::optional<int> GetControllerIndex(const std::string_view& device) override;
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
u32 GetControllerRumbleMotorCount(int controller_index) override;
|
||||
void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
bool SetControllerDeadzone(int controller_index, float size = 0.25f) override;
|
||||
|
||||
void PollEvents() override;
|
||||
|
||||
void SetDeviceNames(std::vector<std::string> device_names);
|
||||
void SetDeviceRumble(u32 index, bool has_vibrator);
|
||||
void HandleAxisEvent(u32 index, u32 axis, float value);
|
||||
void HandleButtonEvent(u32 index, u32 button, bool pressed);
|
||||
bool HasButtonBinding(u32 index, u32 button);
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
NUM_RUMBLE_MOTORS = 2
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::map<u32, std::array<AxisCallback, 3>> axis_mapping;
|
||||
std::map<u32, ButtonCallback> button_mapping;
|
||||
std::map<u32, std::array<ButtonCallback, 2>> axis_button_mapping;
|
||||
std::map<u32, AxisCallback> button_axis_mapping;
|
||||
bool has_rumble = false;
|
||||
};
|
||||
|
||||
std::vector<std::string> m_device_names;
|
||||
std::vector<ControllerData> m_controllers;
|
||||
std::mutex m_controllers_mutex;
|
||||
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -1,171 +0,0 @@
|
|||
#pragma once
|
||||
#include "android_settings_interface.h"
|
||||
#include "common/byte_stream.h"
|
||||
#include "common/event.h"
|
||||
#include "common/progress_callback.h"
|
||||
#include "core/host_display.h"
|
||||
#include "frontend-common/common_host_interface.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
struct ANativeWindow;
|
||||
|
||||
class Controller;
|
||||
|
||||
class AndroidHostInterface final : public CommonHostInterface
|
||||
{
|
||||
public:
|
||||
using CommonHostInterface::UpdateInputMap;
|
||||
|
||||
AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory);
|
||||
~AndroidHostInterface() override;
|
||||
|
||||
ALWAYS_INLINE ANativeWindow* GetSurface() const { return m_surface; }
|
||||
|
||||
bool Initialize() override;
|
||||
void Shutdown() override;
|
||||
|
||||
const char* GetFrontendName() const override;
|
||||
void RequestExit() override;
|
||||
void RunLater(std::function<void()> func) override;
|
||||
|
||||
void ReportError(const char* message) override;
|
||||
void ReportMessage(const char* message) override;
|
||||
|
||||
std::unique_ptr<ByteStream> OpenPackageFile(const char* path, u32 flags) override;
|
||||
|
||||
bool IsEmulationThreadRunning() const { return m_emulation_thread_running.load(); }
|
||||
bool IsEmulationThreadPaused() const;
|
||||
bool IsOnEmulationThread() const;
|
||||
void RunOnEmulationThread(std::function<void()> function, bool blocking = false);
|
||||
void PauseEmulationThread(bool paused);
|
||||
void StopEmulationThreadLoop();
|
||||
|
||||
void EmulationThreadEntryPoint(JNIEnv* env, jobject emulation_activity, SystemBootParameters boot_params,
|
||||
bool resume_state);
|
||||
|
||||
void SurfaceChanged(ANativeWindow* surface, int format, int width, int height);
|
||||
void SetDisplayAlignment(HostDisplay::Alignment alignment);
|
||||
|
||||
void SetControllerButtonState(u32 index, s32 button_code, bool pressed);
|
||||
void SetControllerAxisState(u32 index, s32 button_code, float value);
|
||||
void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed);
|
||||
void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value);
|
||||
bool HasControllerButtonBinding(u32 controller_index, u32 button);
|
||||
void SetControllerVibration(u32 controller_index, float small_motor, float large_motor);
|
||||
void SetFastForwardEnabled(bool enabled);
|
||||
|
||||
void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback);
|
||||
|
||||
bool ImportPatchCodesFromString(const std::string& str);
|
||||
|
||||
jobjectArray GetInputProfileNames(JNIEnv* env) const;
|
||||
|
||||
protected:
|
||||
void SetUserDirectory() override;
|
||||
void RegisterHotkeys() override;
|
||||
|
||||
bool AcquireHostDisplay() override;
|
||||
void ReleaseHostDisplay() override;
|
||||
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
|
||||
void UpdateControllerInterface() override;
|
||||
|
||||
void OnSystemPaused(bool paused) override;
|
||||
void OnSystemDestroyed() override;
|
||||
void OnRunningGameChanged(const std::string& path, CDImage* image, const std::string& game_code,
|
||||
const std::string& game_title) override;
|
||||
|
||||
private:
|
||||
void EmulationThreadLoop(JNIEnv* env);
|
||||
|
||||
void LoadSettings(SettingsInterface& si) override;
|
||||
void UpdateInputMap(SettingsInterface& si) override;
|
||||
void SetVibration(bool enabled);
|
||||
void UpdateVibration();
|
||||
float GetRefreshRate() const;
|
||||
float GetSurfaceScale(int width, int height) const;
|
||||
|
||||
jobject m_java_object = {};
|
||||
jobject m_emulation_activity_object = {};
|
||||
|
||||
ANativeWindow* m_surface = nullptr;
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_sleep_cv;
|
||||
std::deque<std::function<void()>> m_callback_queue;
|
||||
std::atomic_bool m_callbacks_outstanding{false};
|
||||
|
||||
std::atomic_bool m_emulation_thread_stop_request{false};
|
||||
std::atomic_bool m_emulation_thread_running{false};
|
||||
std::thread::id m_emulation_thread_id{};
|
||||
|
||||
HostDisplay::Alignment m_display_alignment = HostDisplay::Alignment::Center;
|
||||
|
||||
u64 m_last_vibration_update_time = 0;
|
||||
bool m_last_vibration_state = false;
|
||||
bool m_vibration_enabled = false;
|
||||
};
|
||||
|
||||
namespace AndroidHelpers {
|
||||
|
||||
JavaVM* GetJavaVM();
|
||||
JNIEnv* GetJNIEnv();
|
||||
AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
|
||||
std::string JStringToString(JNIEnv* env, jstring str);
|
||||
std::unique_ptr<GrowableMemoryByteStream> ReadInputStreamToMemory(JNIEnv* env, jobject obj, u32 chunk_size = 65536);
|
||||
jclass GetStringClass();
|
||||
std::vector<u8> ByteArrayToVector(JNIEnv* env, jbyteArray obj);
|
||||
jbyteArray NewByteArray(JNIEnv* env, const void* data, size_t size);
|
||||
jbyteArray VectorToByteArray(JNIEnv* env, const std::vector<u8>& data);
|
||||
jobjectArray CreateObjectArray(JNIEnv* env, jclass object_class, const jobject* objects, size_t num_objects,
|
||||
bool release_refs = false);
|
||||
} // namespace AndroidHelpers
|
||||
|
||||
template<typename T>
|
||||
class LocalRefHolder
|
||||
{
|
||||
public:
|
||||
LocalRefHolder() : m_env(nullptr), m_object(nullptr) {}
|
||||
|
||||
LocalRefHolder(JNIEnv* env, T object) : m_env(env), m_object(object) {}
|
||||
|
||||
LocalRefHolder(const LocalRefHolder<T>&) = delete;
|
||||
LocalRefHolder(LocalRefHolder&& move) : m_env(move.m_env), m_object(move.m_object)
|
||||
{
|
||||
move.m_env = nullptr;
|
||||
move.m_object = {};
|
||||
}
|
||||
|
||||
~LocalRefHolder()
|
||||
{
|
||||
if (m_object)
|
||||
m_env->DeleteLocalRef(m_object);
|
||||
}
|
||||
|
||||
operator T() const { return m_object; }
|
||||
T operator*() const { return m_object; }
|
||||
|
||||
LocalRefHolder& operator=(const LocalRefHolder&) = delete;
|
||||
LocalRefHolder& operator=(LocalRefHolder&& move)
|
||||
{
|
||||
if (m_object)
|
||||
m_env->DeleteLocalRef(m_object);
|
||||
m_env = move.m_env;
|
||||
m_object = move.m_object;
|
||||
move.m_env = nullptr;
|
||||
move.m_object = {};
|
||||
return *this;
|
||||
}
|
||||
|
||||
T Get() const { return m_object; }
|
||||
|
||||
private:
|
||||
JNIEnv* m_env;
|
||||
T m_object;
|
||||
};
|
|
@ -1,167 +0,0 @@
|
|||
#include "android_http_downloader.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/timer.h"
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
Log_SetChannel(AndroidHTTPDownloader);
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
AndroidHTTPDownloader::AndroidHTTPDownloader() : HTTPDownloader() {}
|
||||
|
||||
AndroidHTTPDownloader::~AndroidHTTPDownloader()
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
if (m_URLDownloader_class)
|
||||
env->DeleteGlobalRef(m_URLDownloader_class);
|
||||
}
|
||||
|
||||
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(const char* user_agent)
|
||||
{
|
||||
std::unique_ptr<AndroidHTTPDownloader> instance(std::make_unique<AndroidHTTPDownloader>());
|
||||
if (!instance->Initialize(user_agent))
|
||||
return {};
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool AndroidHTTPDownloader::Initialize(const char* user_agent)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
jclass klass = env->FindClass("com/github/stenzek/duckstation/URLDownloader");
|
||||
if (!klass)
|
||||
return false;
|
||||
|
||||
m_URLDownloader_class = static_cast<jclass>(env->NewGlobalRef(klass));
|
||||
if (!m_URLDownloader_class)
|
||||
return false;
|
||||
|
||||
m_URLDownloader_constructor = env->GetMethodID(klass, "<init>", "(Ljava/lang/String;)V");
|
||||
m_URLDownloader_get = env->GetMethodID(klass, "get", "(Ljava/lang/String;)Z");
|
||||
m_URLDownloader_post = env->GetMethodID(klass, "post", "(Ljava/lang/String;[B)Z");
|
||||
m_URLDownloader_getStatusCode = env->GetMethodID(klass, "getStatusCode", "()I");
|
||||
m_URLDownloader_getData = env->GetMethodID(klass, "getData", "()[B");
|
||||
if (!m_URLDownloader_constructor || !m_URLDownloader_get || !m_URLDownloader_post || !m_URLDownloader_getStatusCode ||
|
||||
!m_URLDownloader_getData)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_user_agent = user_agent;
|
||||
m_thread_pool = std::make_unique<cb::ThreadPool>(m_max_active_requests);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidHTTPDownloader::ProcessRequest(Request* req)
|
||||
{
|
||||
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
|
||||
if (req->closed.load())
|
||||
return;
|
||||
|
||||
cancel_lock.unlock();
|
||||
req->status_code = -1;
|
||||
req->start_time = Common::Timer::GetValue();
|
||||
|
||||
// TODO: Move to Java side...
|
||||
JNIEnv* env;
|
||||
if (AndroidHelpers::GetJavaVM()->AttachCurrentThread(&env, nullptr) == JNI_OK)
|
||||
{
|
||||
jstring url_string = env->NewStringUTF(req->url.c_str());
|
||||
jstring user_agent_string = env->NewStringUTF(m_user_agent.c_str());
|
||||
|
||||
jobject obj = env->NewObject(m_URLDownloader_class, m_URLDownloader_constructor, user_agent_string);
|
||||
jboolean result;
|
||||
if (req->post_data.empty())
|
||||
{
|
||||
result = env->CallBooleanMethod(obj, m_URLDownloader_get, url_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
jbyteArray post_data = env->NewByteArray(static_cast<jsize>(req->post_data.size()));
|
||||
env->SetByteArrayRegion(post_data, 0, static_cast<jsize>(req->post_data.size()),
|
||||
reinterpret_cast<const jbyte*>(req->post_data.data()));
|
||||
result = env->CallBooleanMethod(obj, m_URLDownloader_post, url_string, post_data);
|
||||
env->DeleteLocalRef(post_data);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(url_string);
|
||||
env->DeleteLocalRef(user_agent_string);
|
||||
|
||||
if (result)
|
||||
{
|
||||
req->status_code = env->CallIntMethod(obj, m_URLDownloader_getStatusCode);
|
||||
|
||||
jbyteArray data = reinterpret_cast<jbyteArray>(env->CallObjectMethod(obj, m_URLDownloader_getData));
|
||||
if (data)
|
||||
{
|
||||
const u32 size = static_cast<u32>(env->GetArrayLength(data));
|
||||
req->data.resize(size);
|
||||
if (size > 0)
|
||||
{
|
||||
jbyte* data_ptr = env->GetByteArrayElements(data, nullptr);
|
||||
std::memcpy(req->data.data(), data_ptr, size);
|
||||
env->ReleaseByteArrayElements(data, data_ptr, 0);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(data);
|
||||
}
|
||||
|
||||
Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes", req->url.c_str(), req->status_code,
|
||||
req->data.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("Request for '%s' failed", req->url.c_str());
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(obj);
|
||||
AndroidHelpers::GetJavaVM()->DetachCurrentThread();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_ErrorPrintf("AttachCurrentThread() failed");
|
||||
}
|
||||
|
||||
cancel_lock.lock();
|
||||
req->state = Request::State::Complete;
|
||||
if (req->closed.load())
|
||||
delete req;
|
||||
else
|
||||
req->closed.store(true);
|
||||
}
|
||||
|
||||
HTTPDownloader::Request* AndroidHTTPDownloader::InternalCreateRequest()
|
||||
{
|
||||
Request* req = new Request();
|
||||
return req;
|
||||
}
|
||||
|
||||
void AndroidHTTPDownloader::InternalPollRequests()
|
||||
{
|
||||
// noop - uses thread pool
|
||||
}
|
||||
|
||||
bool AndroidHTTPDownloader::StartRequest(HTTPDownloader::Request* request)
|
||||
{
|
||||
Request* req = static_cast<Request*>(request);
|
||||
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
|
||||
req->state = Request::State::Started;
|
||||
req->start_time = Common::Timer::GetValue();
|
||||
m_thread_pool->Schedule(std::bind(&AndroidHTTPDownloader::ProcessRequest, this, req));
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidHTTPDownloader::CloseRequest(HTTPDownloader::Request* request)
|
||||
{
|
||||
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
|
||||
Request* req = static_cast<Request*>(request);
|
||||
if (req->closed.load())
|
||||
delete req;
|
||||
else
|
||||
req->closed.store(true);
|
||||
}
|
||||
|
||||
} // namespace FrontendCommon
|
|
@ -1,45 +0,0 @@
|
|||
#pragma once
|
||||
#include "common/thirdparty/thread_pool.h"
|
||||
#include "frontend-common/http_downloader.h"
|
||||
#include <atomic>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
class AndroidHTTPDownloader final : public HTTPDownloader
|
||||
{
|
||||
public:
|
||||
AndroidHTTPDownloader();
|
||||
~AndroidHTTPDownloader() override;
|
||||
|
||||
bool Initialize(const char* user_agent);
|
||||
|
||||
protected:
|
||||
Request* InternalCreateRequest() override;
|
||||
void InternalPollRequests() override;
|
||||
bool StartRequest(HTTPDownloader::Request* request) override;
|
||||
void CloseRequest(HTTPDownloader::Request* request) override;
|
||||
|
||||
private:
|
||||
struct Request : HTTPDownloader::Request
|
||||
{
|
||||
std::atomic_bool closed{false};
|
||||
};
|
||||
|
||||
void ProcessRequest(Request* req);
|
||||
|
||||
std::string m_user_agent;
|
||||
std::unique_ptr<cb::ThreadPool> m_thread_pool;
|
||||
std::mutex m_cancel_mutex;
|
||||
|
||||
jclass m_URLDownloader_class = nullptr;
|
||||
jmethodID m_URLDownloader_constructor = nullptr;
|
||||
jmethodID m_URLDownloader_get = nullptr;
|
||||
jmethodID m_URLDownloader_post = nullptr;
|
||||
jmethodID m_URLDownloader_getStatusCode = nullptr;
|
||||
jmethodID m_URLDownloader_getData = nullptr;
|
||||
};
|
||||
|
||||
} // namespace FrontendCommon
|
|
@ -1,113 +0,0 @@
|
|||
#include "android_progress_callback.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
Log_SetChannel(AndroidProgressCallback);
|
||||
|
||||
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object) : m_java_object(java_object)
|
||||
{
|
||||
jclass cls = env->GetObjectClass(java_object);
|
||||
m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
|
||||
m_set_status_text_method = env->GetMethodID(cls, "setStatusText", "(Ljava/lang/String;)V");
|
||||
m_set_progress_range_method = env->GetMethodID(cls, "setProgressRange", "(I)V");
|
||||
m_set_progress_value_method = env->GetMethodID(cls, "setProgressValue", "(I)V");
|
||||
m_modal_error_method = env->GetMethodID(cls, "modalError", "(Ljava/lang/String;)V");
|
||||
m_modal_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V");
|
||||
m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z");
|
||||
Assert(m_set_status_text_method && m_set_progress_range_method && m_set_progress_value_method &&
|
||||
m_modal_error_method && m_modal_information_method && m_modal_confirmation_method);
|
||||
}
|
||||
|
||||
AndroidProgressCallback::~AndroidProgressCallback() = default;
|
||||
|
||||
bool AndroidProgressCallback::IsCancelled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetCancellable(bool cancellable)
|
||||
{
|
||||
if (m_cancellable == cancellable)
|
||||
return;
|
||||
|
||||
BaseProgressCallback::SetCancellable(cancellable);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetTitle(const char* title)
|
||||
{
|
||||
Assert(title);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> text_jstr(env, env->NewStringUTF(title));
|
||||
env->CallVoidMethod(m_java_object, m_set_title_method, text_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetStatusText(const char* text)
|
||||
{
|
||||
Assert(text);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> text_jstr(env, env->NewStringUTF(text));
|
||||
env->CallVoidMethod(m_java_object, m_set_status_text_method, text_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetProgressRange(u32 range)
|
||||
{
|
||||
BaseProgressCallback::SetProgressRange(range);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
env->CallVoidMethod(m_java_object, m_set_progress_range_method, static_cast<jint>(range));
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetProgressValue(u32 value)
|
||||
{
|
||||
const u32 old_value = m_progress_value;
|
||||
BaseProgressCallback::SetProgressValue(value);
|
||||
if (old_value == m_progress_value)
|
||||
return;
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
env->CallVoidMethod(m_java_object, m_set_progress_value_method, static_cast<jint>(value));
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayError(const char* message)
|
||||
{
|
||||
Log_ErrorPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayWarning(const char* message)
|
||||
{
|
||||
Log_WarningPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayInformation(const char* message)
|
||||
{
|
||||
Log_InfoPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayDebugMessage(const char* message)
|
||||
{
|
||||
Log_DevPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::ModalError(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
env->CallVoidMethod(m_java_object, m_modal_error_method, message_jstr.Get());
|
||||
}
|
||||
|
||||
bool AndroidProgressCallback::ModalConfirmation(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
return env->CallBooleanMethod(m_java_object, m_modal_confirmation_method, message_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::ModalInformation(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
env->CallVoidMethod(m_java_object, m_modal_information_method, message_jstr.Get());
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
#pragma once
|
||||
#include "common/progress_callback.h"
|
||||
#include <jni.h>
|
||||
|
||||
class AndroidProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
AndroidProgressCallback(JNIEnv* env, jobject java_object);
|
||||
~AndroidProgressCallback();
|
||||
|
||||
bool IsCancelled() const override;
|
||||
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
private:
|
||||
jobject m_java_object;
|
||||
|
||||
jmethodID m_set_title_method;
|
||||
jmethodID m_set_status_text_method;
|
||||
jmethodID m_set_progress_range_method;
|
||||
jmethodID m_set_progress_value_method;
|
||||
jmethodID m_modal_error_method;
|
||||
jmethodID m_modal_confirmation_method;
|
||||
jmethodID m_modal_information_method;
|
||||
};
|
|
@ -1,425 +0,0 @@
|
|||
#include "android_settings_interface.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string.h"
|
||||
#include "common/string_util.h"
|
||||
#include <algorithm>
|
||||
Log_SetChannel(AndroidSettingsInterface);
|
||||
|
||||
ALWAYS_INLINE TinyString GetSettingKey(const char* section, const char* key)
|
||||
{
|
||||
return TinyString::FromFormat("%s/%s", section, key);
|
||||
}
|
||||
|
||||
AndroidSettingsInterface::AndroidSettingsInterface(jobject java_context)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
jclass c_preference_manager = env->FindClass("androidx/preference/PreferenceManager");
|
||||
jclass c_preference_editor = env->FindClass("android/content/SharedPreferences$Editor");
|
||||
jclass c_set = env->FindClass("java/util/Set");
|
||||
jclass c_helper = env->FindClass("com/github/stenzek/duckstation/PreferenceHelpers");
|
||||
jmethodID m_get_default_shared_preferences =
|
||||
env->GetStaticMethodID(c_preference_manager, "getDefaultSharedPreferences",
|
||||
"(Landroid/content/Context;)Landroid/content/SharedPreferences;");
|
||||
Assert(c_preference_manager && c_preference_editor && c_set && c_helper && m_get_default_shared_preferences);
|
||||
m_set_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_set));
|
||||
m_shared_preferences_editor_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_preference_editor));
|
||||
m_helper_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_helper));
|
||||
Assert(m_set_class && m_shared_preferences_editor_class && m_helper_class);
|
||||
|
||||
env->DeleteLocalRef(c_set);
|
||||
env->DeleteLocalRef(c_preference_editor);
|
||||
env->DeleteLocalRef(c_helper);
|
||||
|
||||
jobject shared_preferences =
|
||||
env->CallStaticObjectMethod(c_preference_manager, m_get_default_shared_preferences, java_context);
|
||||
Assert(shared_preferences);
|
||||
m_java_shared_preferences = env->NewGlobalRef(shared_preferences);
|
||||
Assert(m_java_shared_preferences);
|
||||
env->DeleteLocalRef(c_preference_manager);
|
||||
env->DeleteLocalRef(shared_preferences);
|
||||
|
||||
jclass c_shared_preferences = env->GetObjectClass(m_java_shared_preferences);
|
||||
m_shared_preferences_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_shared_preferences));
|
||||
Assert(m_shared_preferences_class);
|
||||
env->DeleteLocalRef(c_shared_preferences);
|
||||
|
||||
m_get_boolean = env->GetMethodID(m_shared_preferences_class, "getBoolean", "(Ljava/lang/String;Z)Z");
|
||||
m_get_int = env->GetMethodID(m_shared_preferences_class, "getInt", "(Ljava/lang/String;I)I");
|
||||
m_get_float = env->GetMethodID(m_shared_preferences_class, "getFloat", "(Ljava/lang/String;F)F");
|
||||
m_get_string = env->GetMethodID(m_shared_preferences_class, "getString",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
m_get_string_set =
|
||||
env->GetMethodID(m_shared_preferences_class, "getStringSet", "(Ljava/lang/String;Ljava/util/Set;)Ljava/util/Set;");
|
||||
m_set_to_array = env->GetMethodID(m_set_class, "toArray", "()[Ljava/lang/Object;");
|
||||
Assert(m_get_boolean && m_get_int && m_get_float && m_get_string && m_get_string_set && m_set_to_array);
|
||||
|
||||
m_edit = env->GetMethodID(m_shared_preferences_class, "edit", "()Landroid/content/SharedPreferences$Editor;");
|
||||
m_edit_set_string =
|
||||
env->GetMethodID(m_shared_preferences_editor_class, "putString",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");
|
||||
m_edit_commit = env->GetMethodID(m_shared_preferences_editor_class, "commit", "()Z");
|
||||
m_edit_remove = env->GetMethodID(m_shared_preferences_editor_class, "remove",
|
||||
"(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");
|
||||
Assert(m_edit && m_edit_set_string && m_edit_commit && m_edit_remove);
|
||||
|
||||
m_helper_clear_section =
|
||||
env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V");
|
||||
m_helper_add_to_string_list = env->GetStaticMethodID(
|
||||
m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
||||
m_helper_remove_from_string_list =
|
||||
env->GetStaticMethodID(m_helper_class, "removeFromStringList",
|
||||
"(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
||||
m_helper_set_string_list = env->GetStaticMethodID(
|
||||
m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V");
|
||||
Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list &&
|
||||
m_helper_set_string_list);
|
||||
}
|
||||
|
||||
AndroidSettingsInterface::~AndroidSettingsInterface()
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
if (m_java_shared_preferences)
|
||||
env->DeleteGlobalRef(m_java_shared_preferences);
|
||||
if (m_shared_preferences_editor_class)
|
||||
env->DeleteGlobalRef(m_shared_preferences_editor_class);
|
||||
if (m_shared_preferences_class)
|
||||
env->DeleteGlobalRef(m_shared_preferences_class);
|
||||
if (m_set_class)
|
||||
env->DeleteGlobalRef(m_set_class);
|
||||
if (m_helper_class)
|
||||
env->DeleteGlobalRef(m_helper_class);
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::Clear()
|
||||
{
|
||||
Log_ErrorPrint("Not implemented");
|
||||
}
|
||||
|
||||
int AndroidSettingsInterface::GetIntValue(const char* section, const char* key, int default_value /*= 0*/)
|
||||
{
|
||||
// Some of these settings are string lists...
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%d", default_value)));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
|
||||
// it might actually be an int (e.g. seek bar preference)
|
||||
const int int_value =
|
||||
static_cast<int>(env->CallIntMethod(m_java_shared_preferences, m_get_int, key_string.Get(), default_value));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %d (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %d (int)", section, key, int_value);
|
||||
return int_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
return default_value;
|
||||
|
||||
const char* data = env->GetStringUTFChars(string_object, nullptr);
|
||||
Assert(data != nullptr);
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %s", section, key, data);
|
||||
|
||||
std::optional<int> value = StringUtil::FromChars<int>(data);
|
||||
env->ReleaseStringUTFChars(string_object, data);
|
||||
return value.value_or(default_value);
|
||||
}
|
||||
|
||||
float AndroidSettingsInterface::GetFloatValue(const char* section, const char* key, float default_value /*= 0.0f*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%f", default_value)));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %f (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
{
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %f (null)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
const char* data = env->GetStringUTFChars(string_object, nullptr);
|
||||
Assert(data != nullptr);
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %s", section, key, data);
|
||||
|
||||
std::optional<float> value = StringUtil::FromChars<float>(data);
|
||||
env->ReleaseStringUTFChars(string_object, data);
|
||||
return value.value_or(default_value);
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::GetBoolValue(const char* section, const char* key, bool default_value /*= false*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
jboolean bool_value = static_cast<bool>(
|
||||
env->CallBooleanMethod(m_java_shared_preferences, m_get_boolean, key_string.Get(), default_value));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
Log_DevPrintf("GetBoolValue(%s, %s) -> %u (exception)", section, key, static_cast<unsigned>(default_value));
|
||||
env->ExceptionClear();
|
||||
return default_value;
|
||||
}
|
||||
|
||||
Log_DevPrintf("GetBoolValue(%s, %s) -> %u", section, key, static_cast<unsigned>(bool_value));
|
||||
return bool_value;
|
||||
}
|
||||
|
||||
std::string AndroidSettingsInterface::GetStringValue(const char* section, const char* key,
|
||||
const char* default_value /*= ""*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(default_value));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
{
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s (null)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
const std::string ret(AndroidHelpers::JStringToString(env, string_object));
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s", section, key, ret.c_str());
|
||||
return ret;
|
||||
}
|
||||
|
||||
jobject AndroidSettingsInterface::GetPreferencesEditor(JNIEnv* env)
|
||||
{
|
||||
return env->CallObjectMethod(m_java_shared_preferences, m_edit);
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::CheckForException(JNIEnv* env, const char* task)
|
||||
{
|
||||
if (!env->ExceptionCheck())
|
||||
return;
|
||||
|
||||
Log_ErrorPrintf("JNI exception during %s", task);
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetIntValue(const char* section, const char* key, int value)
|
||||
{
|
||||
Log_DevPrintf("SetIntValue(\"%s\", \"%s\", %d)", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%d", value)));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env,
|
||||
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetIntValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetFloatValue(const char* section, const char* key, float value)
|
||||
{
|
||||
Log_DevPrintf("SetFloatValue(\"%s\", \"%s\", %f)", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%f", value)));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env,
|
||||
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetFloatValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
|
||||
{
|
||||
Log_DevPrintf("SetBoolValue(\"%s\", \"%s\", %u)", section, key, static_cast<unsigned>(value));
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value ? "true" : "false"));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env,
|
||||
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetBoolValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetStringValue(const char* section, const char* key, const char* value)
|
||||
{
|
||||
Log_DevPrintf("SetStringValue(\"%s\", \"%s\", \"%s\")", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env,
|
||||
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetStringValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::DeleteValue(const char* section, const char* key)
|
||||
{
|
||||
Log_DevPrintf("DeleteValue(\"%s\", \"%s\")", section, key);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_remove, key_string.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "DeleteValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::ClearSection(const char* section)
|
||||
{
|
||||
Log_DevPrintf("ClearSection(\"%s\")", section);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> str_section(env, env->NewStringUTF(section));
|
||||
env->CallStaticVoidMethod(m_helper_class, m_helper_clear_section, m_java_shared_preferences, str_section.Get());
|
||||
|
||||
CheckForException(env, "ClearSection");
|
||||
}
|
||||
|
||||
std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* section, const char* key)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jobject> values_set(
|
||||
env, env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, key_string.Get(), nullptr));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
|
||||
// this might just be a string, not a string set
|
||||
LocalRefHolder<jstring> string_object(env, reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||
m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
|
||||
|
||||
if (!env->ExceptionCheck())
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
if (string_object)
|
||||
ret.push_back(AndroidHelpers::JStringToString(env, string_object));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!values_set)
|
||||
return {};
|
||||
|
||||
LocalRefHolder<jobjectArray> values_array(
|
||||
env, reinterpret_cast<jobjectArray>(env->CallObjectMethod(values_set, m_set_to_array)));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!values_array)
|
||||
return {};
|
||||
|
||||
jsize size = env->GetArrayLength(values_array);
|
||||
std::vector<std::string> values;
|
||||
values.reserve(size);
|
||||
for (jsize i = 0; i < size; i++)
|
||||
{
|
||||
jstring str = reinterpret_cast<jstring>(env->GetObjectArrayElement(values_array, i));
|
||||
values.push_back(AndroidHelpers::JStringToString(env, str));
|
||||
env->DeleteLocalRef(str);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetStringList(const char* section, const char* key,
|
||||
const std::vector<std::string>& items)
|
||||
{
|
||||
Log_DevPrintf("SetStringList(\"%s\", \"%s\")", section, key);
|
||||
if (items.empty())
|
||||
{
|
||||
DeleteValue(section, key);
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobjectArray> items_array(
|
||||
env, env->NewObjectArray(static_cast<jsize>(items.size()), AndroidHelpers::GetStringClass(), nullptr));
|
||||
for (size_t i = 0; i < items.size(); i++)
|
||||
{
|
||||
LocalRefHolder<jstring> item_jstr(env, env->NewStringUTF(items[i].c_str()));
|
||||
env->SetObjectArrayElement(items_array, static_cast<jsize>(i), item_jstr);
|
||||
}
|
||||
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
env->CallStaticVoidMethod(m_helper_class, m_helper_set_string_list, m_java_shared_preferences, key_string.Get(),
|
||||
items_array.Get());
|
||||
|
||||
CheckForException(env, "SetStringList");
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
Log_DevPrintf("RemoveFromStringList(\"%s\", \"%s\", \"%s\")", section, key, item);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
||||
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_remove_from_string_list,
|
||||
m_java_shared_preferences, key_string.Get(), item_string.Get());
|
||||
CheckForException(env, "RemoveFromStringList");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
Log_DevPrintf("AddToStringList(\"%s\", \"%s\", \"%s\")", section, key, item);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
||||
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_add_to_string_list,
|
||||
m_java_shared_preferences, key_string.Get(), item_string.Get());
|
||||
CheckForException(env, "AddToStringList");
|
||||
return result;
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
#pragma once
|
||||
#include "core/settings.h"
|
||||
#include <jni.h>
|
||||
|
||||
class AndroidSettingsInterface : public SettingsInterface
|
||||
{
|
||||
public:
|
||||
AndroidSettingsInterface(jobject java_context);
|
||||
~AndroidSettingsInterface();
|
||||
|
||||
bool Save() override;
|
||||
void Clear() override;
|
||||
|
||||
int GetIntValue(const char* section, const char* key, int default_value = 0) override;
|
||||
float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) override;
|
||||
bool GetBoolValue(const char* section, const char* key, bool default_value = false) override;
|
||||
std::string GetStringValue(const char* section, const char* key, const char* default_value = "") override;
|
||||
|
||||
void SetIntValue(const char* section, const char* key, int value) override;
|
||||
void SetFloatValue(const char* section, const char* key, float value) override;
|
||||
void SetBoolValue(const char* section, const char* key, bool value) override;
|
||||
void SetStringValue(const char* section, const char* key, const char* value) override;
|
||||
void DeleteValue(const char* section, const char* key) override;
|
||||
void ClearSection(const char* section) override;
|
||||
|
||||
std::vector<std::string> GetStringList(const char* section, const char* key) override;
|
||||
void SetStringList(const char* section, const char* key, const std::vector<std::string>& items) override;
|
||||
bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
|
||||
bool AddToStringList(const char* section, const char* key, const char* item) override;
|
||||
|
||||
private:
|
||||
jobject GetPreferencesEditor(JNIEnv* env);
|
||||
void CheckForException(JNIEnv* env, const char* task);
|
||||
|
||||
jclass m_set_class{};
|
||||
jclass m_shared_preferences_class{};
|
||||
jclass m_shared_preferences_editor_class{};
|
||||
jclass m_helper_class{};
|
||||
jobject m_java_shared_preferences{};
|
||||
jmethodID m_get_boolean{};
|
||||
jmethodID m_get_int{};
|
||||
jmethodID m_get_float{};
|
||||
jmethodID m_get_string{};
|
||||
jmethodID m_get_string_set{};
|
||||
jmethodID m_edit{};
|
||||
jmethodID m_edit_set_string{};
|
||||
jmethodID m_edit_commit{};
|
||||
jmethodID m_edit_remove{};
|
||||
jmethodID m_set_to_array{};
|
||||
jmethodID m_helper_clear_section{};
|
||||
jmethodID m_helper_add_to_string_list{};
|
||||
jmethodID m_helper_remove_from_string_list{};
|
||||
jmethodID m_helper_set_string_list{};
|
||||
};
|
|
@ -1,210 +0,0 @@
|
|||
#include "opensles_audio_stream.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(OpenSLESAudioStream);
|
||||
|
||||
// Based off Dolphin's OpenSLESStream class.
|
||||
|
||||
OpenSLESAudioStream::OpenSLESAudioStream() = default;
|
||||
|
||||
OpenSLESAudioStream::~OpenSLESAudioStream()
|
||||
{
|
||||
if (IsOpen())
|
||||
OpenSLESAudioStream::CloseDevice();
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStream> OpenSLESAudioStream::Create()
|
||||
{
|
||||
return std::make_unique<OpenSLESAudioStream>();
|
||||
}
|
||||
|
||||
bool OpenSLESAudioStream::OpenDevice()
|
||||
{
|
||||
DebugAssert(!IsOpen());
|
||||
|
||||
SLresult res = slCreateEngine(&m_engine, 0, nullptr, 0, nullptr, nullptr);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("slCreateEngine failed: %d", res);
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine)->Realize(m_engine, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(Engine) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine)->GetInterface(m_engine, SL_IID_ENGINE, &m_engine_engine);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_ENGINE) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix, 0, 0, 0);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("CreateOutputMix failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_output_mix)->Realize(m_output_mix, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(OutputMix) mix failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
SLDataLocator_AndroidSimpleBufferQueue dloc_bq{SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NUM_BUFFERS};
|
||||
SLDataFormat_PCM format = {SL_DATAFORMAT_PCM,
|
||||
m_channels,
|
||||
m_output_sample_rate * 1000u,
|
||||
SL_PCMSAMPLEFORMAT_FIXED_16,
|
||||
SL_PCMSAMPLEFORMAT_FIXED_16,
|
||||
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
|
||||
SL_BYTEORDER_LITTLEENDIAN};
|
||||
SLDataSource dsrc{&dloc_bq, &format};
|
||||
SLDataLocator_OutputMix dloc_outputmix{SL_DATALOCATOR_OUTPUTMIX, m_output_mix};
|
||||
SLDataSink dsink{&dloc_outputmix, nullptr};
|
||||
|
||||
const std::array<SLInterfaceID, 2> ap_interfaces = {{SL_IID_BUFFERQUEUE, SL_IID_VOLUME}};
|
||||
const std::array<SLboolean, 2> ap_interfaces_req = {{true, true}};
|
||||
res = (*m_engine_engine)
|
||||
->CreateAudioPlayer(m_engine_engine, &m_player, &dsrc, &dsink, static_cast<u32>(ap_interfaces.size()),
|
||||
ap_interfaces.data(), ap_interfaces_req.data());
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("CreateAudioPlayer failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->Realize(m_player, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(AudioPlayer) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_PLAY, &m_play_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_PLAY) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_BUFFERQUEUE, &m_buffer_queue_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_BUFFERQUEUE) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_VOLUME, &m_volume_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_VOLUME) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_buffer_queue_interface)->RegisterCallback(m_buffer_queue_interface, BufferCallback, this);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to register callback: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < NUM_BUFFERS; i++)
|
||||
m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels);
|
||||
|
||||
Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers", m_output_sample_rate,
|
||||
m_channels, m_buffer_size, NUM_BUFFERS);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::PauseDevice(bool paused)
|
||||
{
|
||||
if (m_paused == paused)
|
||||
return;
|
||||
|
||||
SLresult res =
|
||||
(*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("SetPlayState failed: %d", res);
|
||||
|
||||
if (!paused && !m_buffer_enqueued)
|
||||
{
|
||||
m_buffer_enqueued = true;
|
||||
EnqueueBuffer();
|
||||
}
|
||||
|
||||
m_paused = paused;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::CloseDevice()
|
||||
{
|
||||
m_buffers = {};
|
||||
m_current_buffer = 0;
|
||||
m_paused = true;
|
||||
m_buffer_enqueued = false;
|
||||
|
||||
if (m_player)
|
||||
{
|
||||
(*m_player)->Destroy(m_player);
|
||||
m_volume_interface = {};
|
||||
m_buffer_queue_interface = {};
|
||||
m_play_interface = {};
|
||||
m_player = {};
|
||||
}
|
||||
if (m_output_mix)
|
||||
{
|
||||
(*m_output_mix)->Destroy(m_output_mix);
|
||||
m_output_mix = {};
|
||||
}
|
||||
(*m_engine)->Destroy(m_engine);
|
||||
m_engine_engine = {};
|
||||
m_engine = {};
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::SetOutputVolume(u32 volume)
|
||||
{
|
||||
const SLmillibel attenuation = (volume == 0) ?
|
||||
SL_MILLIBEL_MIN :
|
||||
static_cast<SLmillibel>(2000.0f * std::log10(static_cast<float>(volume) / 100.0f));
|
||||
SLresult res = (*m_volume_interface)->SetVolumeLevel(m_volume_interface, attenuation);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("SetVolumeLevel failed: %d", res);
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::EnqueueBuffer()
|
||||
{
|
||||
SampleType* samples = m_buffers[m_current_buffer].get();
|
||||
ReadFrames(samples, m_buffer_size, false);
|
||||
|
||||
SLresult res = (*m_buffer_queue_interface)
|
||||
->Enqueue(m_buffer_queue_interface, samples, m_buffer_size * m_channels * sizeof(SampleType));
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("Enqueue buffer failed: %d", res);
|
||||
|
||||
m_current_buffer = (m_current_buffer + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context)
|
||||
{
|
||||
OpenSLESAudioStream* const this_ptr = static_cast<OpenSLESAudioStream*>(context);
|
||||
this_ptr->EnqueueBuffer();
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::FramesAvailable() {}
|
|
@ -1,48 +0,0 @@
|
|||
#pragma once
|
||||
#include "common/audio_stream.h"
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <SLES/OpenSLES_Android.h>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
class OpenSLESAudioStream final : public AudioStream
|
||||
{
|
||||
public:
|
||||
OpenSLESAudioStream();
|
||||
~OpenSLESAudioStream();
|
||||
|
||||
static std::unique_ptr<AudioStream> Create();
|
||||
|
||||
void SetOutputVolume(u32 volume) override;
|
||||
|
||||
protected:
|
||||
enum : u32
|
||||
{
|
||||
NUM_BUFFERS = 2
|
||||
};
|
||||
|
||||
ALWAYS_INLINE bool IsOpen() const { return (m_engine != nullptr); }
|
||||
|
||||
bool OpenDevice() override;
|
||||
void PauseDevice(bool paused) override;
|
||||
void CloseDevice() override;
|
||||
void FramesAvailable() override;
|
||||
|
||||
void EnqueueBuffer();
|
||||
|
||||
static void BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context);
|
||||
|
||||
SLObjectItf m_engine{};
|
||||
SLEngineItf m_engine_engine{};
|
||||
SLObjectItf m_output_mix{};
|
||||
|
||||
SLObjectItf m_player{};
|
||||
SLPlayItf m_play_interface{};
|
||||
SLAndroidSimpleBufferQueueItf m_buffer_queue_interface{};
|
||||
SLVolumeItf m_volume_interface{};
|
||||
|
||||
std::array<std::unique_ptr<SampleType[]>, NUM_BUFFERS> m_buffers;
|
||||
u32 m_current_buffer = 0;
|
||||
bool m_paused = true;
|
||||
bool m_buffer_enqueued = false;
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.stenzek.duckstation">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MemoryCardEditorActivity"
|
||||
android:label="@string/title_activity_memory_card_editor"
|
||||
android:theme="@style/AppTheme.NoActionBar"></activity>
|
||||
<activity
|
||||
android:name=".GameDirectoriesActivity"
|
||||
android:label="@string/title_activity_game_directories"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".EmulationActivity"
|
||||
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
|
||||
android:exported="true"
|
||||
android:immersive="true"
|
||||
android:label="@string/title_activity_emulation"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ControllerSettingsActivity"
|
||||
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
|
||||
android:label="@string/controller_mapping_activity_title"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GamePropertiesActivity"
|
||||
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
|
||||
android:label="@string/activity_game_properties"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
|
@ -1,76 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
public final class Achievement {
|
||||
public static final int CATEGORY_LOCAL = 0;
|
||||
public static final int CATEGORY_CORE = 3;
|
||||
public static final int CATEGORY_UNOFFICIAL = 5;
|
||||
|
||||
private final int id;
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final String lockedBadgePath;
|
||||
private final String unlockedBadgePath;
|
||||
private final int points;
|
||||
private final boolean locked;
|
||||
|
||||
public Achievement(int id, String name, String description, String lockedBadgePath,
|
||||
String unlockedBadgePath, int points, boolean locked) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.lockedBadgePath = lockedBadgePath;
|
||||
this.unlockedBadgePath = unlockedBadgePath;
|
||||
this.points = points;
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if challenge mode will be enabled when a game is started.
|
||||
* Does not depend on the emulation running.
|
||||
*
|
||||
* @param context context to pull settings from
|
||||
* @return true if challenge mode will be used, false otherwise
|
||||
*/
|
||||
public static boolean willChallengeModeBeEnabled(Context context) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return prefs.getBoolean("Cheevos/Enabled", false) &&
|
||||
prefs.getBoolean("Cheevos/ChallengeMode", false);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getLockedBadgePath() {
|
||||
return lockedBadgePath;
|
||||
}
|
||||
|
||||
public String getUnlockedBadgePath() {
|
||||
return unlockedBadgePath;
|
||||
}
|
||||
|
||||
public int getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public String getBadgePath() {
|
||||
return locked ? lockedBadgePath : unlockedBadgePath;
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class AchievementListFragment extends DialogFragment {
|
||||
|
||||
private RecyclerView mRecyclerView;
|
||||
private AchievementListFragment.ViewAdapter mAdapter;
|
||||
private final Achievement[] mAchievements;
|
||||
private DialogInterface.OnDismissListener mOnDismissListener;
|
||||
|
||||
public AchievementListFragment(Achievement[] achievements) {
|
||||
mAchievements = achievements;
|
||||
sortAchievements();
|
||||
}
|
||||
|
||||
public void setOnDismissListener(DialogInterface.OnDismissListener l) {
|
||||
mOnDismissListener = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
if (mOnDismissListener != null)
|
||||
mOnDismissListener.onDismiss(dialog);
|
||||
|
||||
super.onDismiss(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (getDialog() == null)
|
||||
return;
|
||||
|
||||
final boolean isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
|
||||
final float scale = (float) getContext().getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT;
|
||||
final int width = Math.round((isLandscape ? 700.0f : 400.0f) * scale);
|
||||
final int height = Math.round((isLandscape ? 400.0f : 700.0f) * scale);
|
||||
getDialog().getWindow().setLayout(width, height);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_achievement_list, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
mAdapter = new AchievementListFragment.ViewAdapter(getContext(), mAchievements);
|
||||
|
||||
mRecyclerView = view.findViewById(R.id.recyclerView);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
|
||||
DividerItemDecoration.VERTICAL));
|
||||
|
||||
fillHeading(view);
|
||||
}
|
||||
|
||||
private void fillHeading(@NonNull View view) {
|
||||
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||
final String gameTitle = hi.getCheevoGameTitle();
|
||||
if (gameTitle != null) {
|
||||
final String formattedTitle = hi.isCheevosChallengeModeActive() ?
|
||||
String.format(getString(R.string.achievement_title_challenge_mode_format_string), gameTitle) :
|
||||
gameTitle;
|
||||
((TextView) view.findViewById(R.id.title)).setText(formattedTitle);
|
||||
}
|
||||
|
||||
final int cheevoCount = hi.getCheevoCount();
|
||||
final int unlockedCheevoCount = hi.getUnlockedCheevoCount();
|
||||
final String summary = String.format(getString(R.string.achievement_summary_format_string),
|
||||
unlockedCheevoCount, cheevoCount, hi.getCheevoPointsForGame(), hi.getCheevoMaximumPointsForGame());
|
||||
((TextView) view.findViewById(R.id.summary)).setText(summary);
|
||||
|
||||
ProgressBar pb = ((ProgressBar) view.findViewById(R.id.progressBar));
|
||||
pb.setMax(cheevoCount);
|
||||
pb.setProgress(unlockedCheevoCount);
|
||||
|
||||
final ImageView icon = ((ImageView) view.findViewById(R.id.icon));
|
||||
final String badgePath = hi.getCheevoGameIconPath();
|
||||
if (badgePath != null) {
|
||||
new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, badgePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void sortAchievements() {
|
||||
Arrays.sort(mAchievements, (o1, o2) -> {
|
||||
if (o2.isLocked() && !o1.isLocked())
|
||||
return -1;
|
||||
else if (o1.isLocked() && !o2.isLocked())
|
||||
return 1;
|
||||
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
});
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
private final View mItemView;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mItemView = itemView;
|
||||
mItemView.setOnClickListener(this);
|
||||
mItemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
public void bindToEntry(Achievement cheevo) {
|
||||
ImageView icon = ((ImageView) mItemView.findViewById(R.id.icon));
|
||||
icon.setImageDrawable(mItemView.getContext().getDrawable(R.drawable.ic_baseline_lock_24));
|
||||
|
||||
final String badgePath = cheevo.getBadgePath();
|
||||
if (badgePath != null) {
|
||||
new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, badgePath);
|
||||
}
|
||||
|
||||
((TextView) mItemView.findViewById(R.id.title)).setText(cheevo.getName());
|
||||
((TextView) mItemView.findViewById(R.id.description)).setText(cheevo.getDescription());
|
||||
|
||||
((ImageView) mItemView.findViewById(R.id.locked_icon)).setImageDrawable(
|
||||
mItemView.getContext().getDrawable(cheevo.isLocked() ? R.drawable.ic_baseline_lock_24 : R.drawable.ic_baseline_lock_open_24));
|
||||
|
||||
final String pointsString = String.format(mItemView.getContext().getString(R.string.achievement_points_format_string), cheevo.getPoints());
|
||||
((TextView) mItemView.findViewById(R.id.points)).setText(pointsString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewAdapter extends RecyclerView.Adapter<AchievementListFragment.ViewHolder> {
|
||||
private final LayoutInflater mInflater;
|
||||
private final Achievement[] mAchievements;
|
||||
|
||||
public ViewAdapter(@NonNull Context context, Achievement[] achievements) {
|
||||
mInflater = LayoutInflater.from(context);
|
||||
mAchievements = achievements;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AchievementListFragment.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new AchievementListFragment.ViewHolder(mInflater.inflate(R.layout.layout_achievement_entry, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AchievementListFragment.ViewHolder holder, int position) {
|
||||
holder.bindToEntry(mAchievements[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return (mAchievements != null) ? mAchievements.length : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_game_list_entry;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,266 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AchievementSettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener {
|
||||
private static final String REGISTER_URL = "http://retroachievements.org/createaccount.php";
|
||||
private static final String PROFILE_URL_PREFIX = "https://retroachievements.org/user/";
|
||||
|
||||
private boolean isLoggedIn = false;
|
||||
private String username;
|
||||
private String loginTokenTime;
|
||||
|
||||
public AchievementSettingsFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.achievement_preferences, rootKey);
|
||||
updateViews();
|
||||
}
|
||||
|
||||
private void updateViews() {
|
||||
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
|
||||
|
||||
username = prefs.getString("Cheevos/Username", "");
|
||||
isLoggedIn = (username != null && !username.isEmpty());
|
||||
if (isLoggedIn) {
|
||||
try {
|
||||
final String loginTokenTimeString = prefs.getString("Cheevos/LoginTimestamp", "");
|
||||
final long loginUnixTimestamp = Long.parseLong(loginTokenTimeString);
|
||||
|
||||
// TODO: Extract to a helper function.
|
||||
final Date date = new Date(loginUnixTimestamp * 1000);
|
||||
final DateFormat format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT, Locale.getDefault());
|
||||
loginTokenTime = format.format(date);
|
||||
} catch (Exception e) {
|
||||
loginTokenTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
final PreferenceScreen preferenceScreen = getPreferenceScreen();
|
||||
|
||||
Preference preference = preferenceScreen.findPreference("Cheevos/ChallengeMode");
|
||||
if (preference != null) {
|
||||
// toggling this is disabled while it's running to avoid the whole power off thing
|
||||
preference.setEnabled(!AndroidHostInterface.getInstance().isEmulationThreadRunning());
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/Login");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(!isLoggedIn);
|
||||
preference.setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/Register");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(!isLoggedIn);
|
||||
preference.setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/Logout");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(isLoggedIn);
|
||||
preference.setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/Username");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(isLoggedIn);
|
||||
preference.setSummary((username != null) ? username : "");
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/LoginTokenTime");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(isLoggedIn);
|
||||
preference.setSummary((loginTokenTime != null) ? loginTokenTime : "");
|
||||
}
|
||||
|
||||
preference = preferenceScreen.findPreference("Cheevos/ViewProfile");
|
||||
if (preference != null)
|
||||
{
|
||||
preference.setVisible(isLoggedIn);
|
||||
preference.setOnPreferenceClickListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final String key = preference.getKey();
|
||||
if (key == null)
|
||||
return false;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "Cheevos/Login":
|
||||
{
|
||||
handleLogin();
|
||||
return true;
|
||||
}
|
||||
|
||||
case "Cheevos/Logout":
|
||||
{
|
||||
handleLogout();
|
||||
return true;
|
||||
}
|
||||
|
||||
case "Cheevos/Register":
|
||||
{
|
||||
openUrl(REGISTER_URL);
|
||||
return true;
|
||||
}
|
||||
|
||||
case "Cheevos/ViewProfile":
|
||||
{
|
||||
final String profileUrl = getProfileUrl(username);
|
||||
if (profileUrl != null)
|
||||
openUrl(profileUrl);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void openUrl(String url) {
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private void handleLogin() {
|
||||
LoginDialogFragment loginDialog = new LoginDialogFragment(this);
|
||||
loginDialog.show(getFragmentManager(), "fragment_achievement_login");
|
||||
}
|
||||
|
||||
private void handleLogout() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setTitle(R.string.settings_achievements_confirm_logout_title);
|
||||
builder.setMessage(R.string.settings_achievements_confirm_logout_message);
|
||||
builder.setPositiveButton(R.string.settings_achievements_logout, (dialog, which) -> {
|
||||
AndroidHostInterface.getInstance().cheevosLogout();
|
||||
updateViews();
|
||||
});
|
||||
builder.setNegativeButton(R.string.achievement_settings_login_cancel_button, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private static String getProfileUrl(String username) {
|
||||
try {
|
||||
final String encodedUsername = URLEncoder.encode(username, "UTF-8");
|
||||
return PROFILE_URL_PREFIX + encodedUsername;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoginDialogFragment extends DialogFragment {
|
||||
private AchievementSettingsFragment mParent;
|
||||
|
||||
public LoginDialogFragment(AchievementSettingsFragment parent) {
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_achievements_login, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
((Button)view.findViewById(R.id.login)).setOnClickListener((View.OnClickListener) v -> doLogin());
|
||||
((Button)view.findViewById(R.id.cancel)).setOnClickListener((View.OnClickListener) v -> dismiss());
|
||||
}
|
||||
|
||||
private static class LoginTask extends AsyncTask<Void, Void, Void> {
|
||||
private LoginDialogFragment mParent;
|
||||
private String mUsername;
|
||||
private String mPassword;
|
||||
private boolean mResult;
|
||||
|
||||
public LoginTask(LoginDialogFragment parent, String username, String password) {
|
||||
mParent = parent;
|
||||
mUsername = username;
|
||||
mPassword = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
final Activity activity = mParent.getActivity();
|
||||
if (activity == null)
|
||||
return null;
|
||||
|
||||
mResult = AndroidHostInterface.getInstance().cheevosLogin(mUsername, mPassword);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
if (!mResult) {
|
||||
((TextView) mParent.getView().findViewById(R.id.error)).setText(R.string.achievement_settings_login_failed);
|
||||
mParent.enableUi(true);
|
||||
return;
|
||||
}
|
||||
|
||||
mParent.mParent.updateViews();
|
||||
mParent.dismiss();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void doLogin() {
|
||||
final View rootView = getView();
|
||||
final String username = ((EditText)rootView.findViewById(R.id.username)).getText().toString();
|
||||
final String password = ((EditText)rootView.findViewById(R.id.password)).getText().toString();
|
||||
if (username == null || username.length() == 0 || password == null || password.length() == 0)
|
||||
return;
|
||||
|
||||
enableUi(false);
|
||||
((TextView)rootView.findViewById(R.id.error)).setText("");
|
||||
new LoginDialogFragment.LoginTask(this, username, password).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void enableUi(boolean enabled) {
|
||||
final View rootView = getView();
|
||||
((EditText)rootView.findViewById(R.id.username)).setEnabled(enabled);
|
||||
((EditText)rootView.findViewById(R.id.password)).setEnabled(enabled);
|
||||
((Button)rootView.findViewById(R.id.login)).setEnabled(enabled);
|
||||
((Button)rootView.findViewById(R.id.cancel)).setEnabled(enabled);
|
||||
((ProgressBar)rootView.findViewById(R.id.progressBar)).setVisibility(enabled ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AndroidHostInterface {
|
||||
public final static int DISPLAY_ALIGNMENT_TOP_OR_LEFT = 0;
|
||||
public final static int DISPLAY_ALIGNMENT_CENTER = 1;
|
||||
public final static int DISPLAY_ALIGNMENT_RIGHT_OR_BOTTOM = 2;
|
||||
|
||||
public final static int CONTROLLER_AXIS_TYPE_FULL = 0;
|
||||
public final static int CONTROLLER_AXIS_TYPE_HALF = 1;
|
||||
|
||||
private long mNativePointer;
|
||||
private Context mContext;
|
||||
private FileHelper mFileHelper;
|
||||
private EmulationActivity mEmulationActivity;
|
||||
|
||||
public AndroidHostInterface(Context context, FileHelper fileHelper) {
|
||||
this.mContext = context;
|
||||
this.mFileHelper = fileHelper;
|
||||
}
|
||||
|
||||
public void reportError(String message) {
|
||||
Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void reportMessage(String message) {
|
||||
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public InputStream openAssetStream(String path) {
|
||||
try {
|
||||
return mContext.getAssets().open(path, AssetManager.ACCESS_STREAMING);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public EmulationActivity getEmulationActivity() { return mEmulationActivity; }
|
||||
|
||||
static public native String getScmVersion();
|
||||
|
||||
static public native String getFullScmVersion();
|
||||
|
||||
static public native boolean setThreadAffinity(int[] cpus);
|
||||
|
||||
static public native AndroidHostInterface create(Context context, FileHelper fileHelper, String userDirectory);
|
||||
|
||||
public native boolean isEmulationThreadRunning();
|
||||
|
||||
public native boolean runEmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String state_filename);
|
||||
|
||||
public native boolean isEmulationThreadPaused();
|
||||
|
||||
public native void pauseEmulationThread(boolean paused);
|
||||
|
||||
public native void stopEmulationThreadLoop();
|
||||
|
||||
public native boolean hasSurface();
|
||||
|
||||
public native void surfaceChanged(Surface surface, int format, int width, int height);
|
||||
|
||||
public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
|
||||
|
||||
public native void setControllerAxisState(int index, int axisCode, float value);
|
||||
|
||||
public native void setControllerAutoFireState(int controllerIndex, int autoFireIndex, boolean active);
|
||||
|
||||
public native void setMousePosition(int positionX, int positionY);
|
||||
|
||||
public static native int getControllerButtonCode(String controllerType, String buttonName);
|
||||
|
||||
public static native int getControllerAxisCode(String controllerType, String axisName);
|
||||
|
||||
public static native int getControllerAxisType(String controllerType, String axisName);
|
||||
|
||||
public static native String[] getControllerButtonNames(String controllerType);
|
||||
|
||||
public static native String[] getControllerAxisNames(String controllerType);
|
||||
|
||||
public static native int getControllerVibrationMotorCount(String controllerType);
|
||||
|
||||
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
||||
|
||||
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
||||
|
||||
public native boolean hasControllerButtonBinding(int controllerIndex, int buttonIndex);
|
||||
|
||||
public native void toggleControllerAnalogMode();
|
||||
|
||||
public native String[] getInputProfileNames();
|
||||
|
||||
public native boolean loadInputProfile(String name);
|
||||
|
||||
public native boolean saveInputProfile(String name);
|
||||
|
||||
public native HotkeyInfo[] getHotkeyInfoList();
|
||||
|
||||
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase, AndroidProgressCallback progressCallback);
|
||||
|
||||
public native GameListEntry[] getGameListEntries();
|
||||
|
||||
public native GameListEntry getGameListEntry(String path);
|
||||
|
||||
public native String getGameSettingValue(String path, String key);
|
||||
|
||||
public native void setGameSettingValue(String path, String key, String value);
|
||||
|
||||
public native void resetSystem();
|
||||
|
||||
public native void loadState(boolean global, int slot);
|
||||
|
||||
public native void saveState(boolean global, int slot);
|
||||
|
||||
public native void saveResumeState(boolean waitForCompletion);
|
||||
|
||||
public native void applySettings();
|
||||
public native void updateInputMap();
|
||||
|
||||
public native void setDisplayAlignment(int alignment);
|
||||
|
||||
public native PatchCode[] getPatchCodeList();
|
||||
|
||||
public native void setPatchCodeEnabled(int index, boolean enabled);
|
||||
|
||||
public native boolean importPatchCodesFromString(String str);
|
||||
|
||||
public native void addOSDMessage(String message, float duration);
|
||||
|
||||
public native boolean hasAnyBIOSImages();
|
||||
|
||||
public native String importBIOSImage(byte[] data);
|
||||
|
||||
public native boolean isFastForwardEnabled();
|
||||
|
||||
public native void setFastForwardEnabled(boolean enabled);
|
||||
|
||||
public native boolean hasMediaSubImages();
|
||||
|
||||
public native String[] getMediaSubImageTitles();
|
||||
|
||||
public native int getMediaSubImageIndex();
|
||||
|
||||
public native boolean switchMediaSubImage(int index);
|
||||
|
||||
public native boolean setMediaFilename(String filename);
|
||||
|
||||
public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty);
|
||||
|
||||
public native void setFullscreenUINotificationVerticalPosition(float position, float direction);
|
||||
|
||||
public native boolean isCheevosActive();
|
||||
public native boolean isCheevosChallengeModeActive();
|
||||
public native Achievement[] getCheevoList();
|
||||
public native int getCheevoCount();
|
||||
public native int getUnlockedCheevoCount();
|
||||
public native int getCheevoPointsForGame();
|
||||
public native int getCheevoMaximumPointsForGame();
|
||||
public native String getCheevoGameTitle();
|
||||
public native String getCheevoGameIconPath();
|
||||
public native boolean cheevosLogin(String username, String password);
|
||||
public native void cheevosLogout();
|
||||
|
||||
static {
|
||||
System.loadLibrary("duckstation-native");
|
||||
}
|
||||
|
||||
static private AndroidHostInterface mInstance;
|
||||
static private String mUserDirectory;
|
||||
|
||||
static public boolean createInstance(Context context) {
|
||||
// Set user path.
|
||||
mUserDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
if (mUserDirectory.isEmpty())
|
||||
mUserDirectory = "/sdcard";
|
||||
|
||||
mUserDirectory += "/duckstation";
|
||||
Log.i("AndroidHostInterface", "User directory: " + mUserDirectory);
|
||||
mInstance = create(context, new FileHelper(context), mUserDirectory);
|
||||
return mInstance != null;
|
||||
}
|
||||
|
||||
static public boolean hasInstance() {
|
||||
return mInstance != null;
|
||||
}
|
||||
|
||||
static public AndroidHostInterface getInstance() {
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
static public String getUserDirectory() { return mUserDirectory; }
|
||||
|
||||
static public boolean hasInstanceAndEmulationThreadIsRunning() {
|
||||
return hasInstance() && getInstance().isEmulationThreadRunning();
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class AndroidProgressCallback {
|
||||
private Activity mContext;
|
||||
private ProgressDialog mDialog;
|
||||
|
||||
public AndroidProgressCallback(Activity context) {
|
||||
mContext = context;
|
||||
mDialog = new ProgressDialog(context);
|
||||
mDialog.setCancelable(false);
|
||||
mDialog.setCanceledOnTouchOutside(false);
|
||||
mDialog.setMessage(context.getString(R.string.android_progress_callback_please_wait));
|
||||
mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
mDialog.setIndeterminate(false);
|
||||
mDialog.setMax(100);
|
||||
mDialog.setProgress(0);
|
||||
mDialog.show();
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
mDialog.dismiss();
|
||||
}
|
||||
|
||||
public void setTitle(String text) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setTitle(text);
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatusText(String text) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setMessage(text);
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgressRange(int range) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setMax(range);
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgressValue(int value) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setProgress(value);
|
||||
});
|
||||
}
|
||||
|
||||
public void modalError(String message) {
|
||||
Object lock = new Object();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle("Error")
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void modalInformation(String message) {
|
||||
Object lock = new Object();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getString(R.string.android_progress_callback_information))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfirmationResult {
|
||||
public boolean result = false;
|
||||
}
|
||||
|
||||
public boolean modalConfirmation(String message) {
|
||||
ConfirmationResult result = new ConfirmationResult();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getString(R.string.android_progress_callback_confirmation))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_yes), (dialog, button) -> {
|
||||
result.result = true;
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(mContext.getString(R.string.android_progress_callback_no), (dialog, button) -> {
|
||||
result.result = false;
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (result) {
|
||||
result.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (result) {
|
||||
try {
|
||||
result.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
return result.result;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
public enum ConsoleRegion {
|
||||
AutoDetect,
|
||||
NTSC_J,
|
||||
NTSC_U,
|
||||
PAL
|
||||
}
|
|
@ -1,295 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Vibrator;
|
||||
import android.text.InputType;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ControllerAutoMapper {
|
||||
public interface CompleteCallback {
|
||||
public void onComplete();
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final int port;
|
||||
private final CompleteCallback completeCallback;
|
||||
|
||||
private InputDevice device;
|
||||
private SharedPreferences prefs;
|
||||
private SharedPreferences.Editor editor;
|
||||
private StringBuilder log;
|
||||
private String keyBase;
|
||||
private String controllerType;
|
||||
|
||||
public ControllerAutoMapper(Context context, int port, CompleteCallback completeCallback) {
|
||||
this.context = context;
|
||||
this.port = port;
|
||||
this.completeCallback = completeCallback;
|
||||
}
|
||||
|
||||
private void log(String format, Object... args) {
|
||||
log.append(String.format(format, args));
|
||||
log.append('\n');
|
||||
}
|
||||
|
||||
private void setButtonBindingToKeyCode(String buttonName, int keyCode) {
|
||||
log("Binding button '%s' to key '%s' (%d)", buttonName, KeyEvent.keyCodeToString(keyCode), keyCode);
|
||||
|
||||
final String key = String.format("%sButton%s", keyBase, buttonName);
|
||||
final String value = String.format("%s/Button%d", device.getDescriptor(), keyCode);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void setButtonBindingToAxis(String buttonName, int axis, int direction) {
|
||||
final char directionIndicator = (direction < 0) ? '-' : '+';
|
||||
log("Binding button '%s' to axis '%s' (%d) direction %c", buttonName, MotionEvent.axisToString(axis), axis, directionIndicator);
|
||||
|
||||
final String key = String.format("%sButton%s", keyBase, buttonName);
|
||||
final String value = String.format("%s/%cAxis%d", device.getDescriptor(), directionIndicator, axis);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void setAxisBindingToAxis(String axisName, int axis) {
|
||||
log("Binding axis '%s' to axis '%s' (%d)", axisName, MotionEvent.axisToString(axis), axis);
|
||||
|
||||
final String key = String.format("%sAxis%s", keyBase, axisName);
|
||||
final String value = String.format("%s/Axis%d", device.getDescriptor(), axis);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void doAutoBindingButton(String buttonName, int[] keyCodes, int[][] axisCodes) {
|
||||
// Prefer the axis codes, as it dispatches to that first.
|
||||
if (axisCodes != null) {
|
||||
final List<InputDevice.MotionRange> motionRangeList = device.getMotionRanges();
|
||||
for (int[] axisAndDirection : axisCodes) {
|
||||
final int axis = axisAndDirection[0];
|
||||
final int direction = axisAndDirection[1];
|
||||
for (InputDevice.MotionRange range : motionRangeList) {
|
||||
if (range.getAxis() == axis) {
|
||||
setButtonBindingToAxis(buttonName, axis, direction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keyCodes != null) {
|
||||
final boolean[] keysPresent = device.hasKeys(keyCodes);
|
||||
for (int i = 0; i < keysPresent.length; i++) {
|
||||
if (keysPresent[i]) {
|
||||
setButtonBindingToKeyCode(buttonName, keyCodes[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log("No automatic bindings found for button '%s'", buttonName);
|
||||
}
|
||||
|
||||
private void doAutoBindingAxis(String axisName, int[] axisCodes) {
|
||||
// Prefer the axis codes, as it dispatches to that first.
|
||||
if (axisCodes != null) {
|
||||
final List<InputDevice.MotionRange> motionRangeList = device.getMotionRanges();
|
||||
for (final int axis : axisCodes) {
|
||||
for (InputDevice.MotionRange range : motionRangeList) {
|
||||
if (range.getAxis() == axis) {
|
||||
setAxisBindingToAxis(axisName, axis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.append(String.format("No automatic bindings found for axis '%s'\n", axisName));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
final ArrayList<InputDevice> deviceList = new ArrayList<>();
|
||||
for (final int deviceId : InputDevice.getDeviceIds()) {
|
||||
final InputDevice inputDevice = InputDevice.getDevice(deviceId);
|
||||
if (inputDevice == null || !EmulationSurfaceView.isBindableDevice(inputDevice) ||
|
||||
!EmulationSurfaceView.isGamepadDevice(inputDevice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deviceList.add(inputDevice);
|
||||
}
|
||||
|
||||
if (deviceList.isEmpty()) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.main_activity_error);
|
||||
builder.setMessage(R.string.controller_auto_mapping_no_devices);
|
||||
builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] deviceNames = new String[deviceList.size()];
|
||||
for (int i = 0; i < deviceList.size(); i++)
|
||||
deviceNames[i] = deviceList.get(i).getName();
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.controller_auto_mapping_select_device);
|
||||
builder.setItems(deviceNames, (dialog, which) -> {
|
||||
process(deviceList.get(which));
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void process(InputDevice device) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.editor = prefs.edit();
|
||||
this.log = new StringBuilder();
|
||||
this.device = device;
|
||||
|
||||
this.keyBase = String.format("Controller%d/", port);
|
||||
this.controllerType = ControllerSettingsCollectionFragment.getControllerType(prefs, port);
|
||||
|
||||
setButtonBindings();
|
||||
setAxisBindings();
|
||||
setVibrationBinding();
|
||||
|
||||
this.editor.commit();
|
||||
this.editor = null;
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.controller_auto_mapping_results);
|
||||
|
||||
final EditText editText = new EditText(context);
|
||||
editText.setText(log.toString());
|
||||
editText.setInputType(InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
editText.setSingleLine(false);
|
||||
builder.setView(editText);
|
||||
|
||||
builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
|
||||
if (completeCallback != null)
|
||||
completeCallback.onComplete();
|
||||
}
|
||||
|
||||
private void setButtonBindings() {
|
||||
final String[] buttonNames = AndroidHostInterface.getInstance().getControllerButtonNames(controllerType);
|
||||
if (buttonNames == null || buttonNames.length == 0) {
|
||||
log("No axes to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final String buttonName : buttonNames) {
|
||||
switch (buttonName) {
|
||||
case "Up":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_UP}, new int[][]{{MotionEvent.AXIS_HAT_Y, -1}});
|
||||
break;
|
||||
case "Down":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_DOWN}, new int[][]{{MotionEvent.AXIS_HAT_Y, 1}});
|
||||
break;
|
||||
case "Left":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_LEFT}, new int[][]{{MotionEvent.AXIS_HAT_X, -1}});
|
||||
break;
|
||||
case "Right":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_RIGHT}, new int[][]{{MotionEvent.AXIS_HAT_X, 1}});
|
||||
break;
|
||||
case "Select":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_SELECT}, null);
|
||||
break;
|
||||
case "Start":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_START}, null);
|
||||
break;
|
||||
case "Triangle":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_Y}, null);
|
||||
break;
|
||||
case "Cross":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_A}, null);
|
||||
break;
|
||||
case "Circle":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_B}, null);
|
||||
break;
|
||||
case "Square":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_X}, null);
|
||||
break;
|
||||
case "L1":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L1}, null);
|
||||
break;
|
||||
case "L2":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L2}, new int[][]{{MotionEvent.AXIS_LTRIGGER, 1}, {MotionEvent.AXIS_Z, 1}, {MotionEvent.AXIS_BRAKE, 1}});
|
||||
break;
|
||||
case "R1":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R1}, null);
|
||||
break;
|
||||
case "R2":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R2}, new int[][]{{MotionEvent.AXIS_RTRIGGER, 1}, {MotionEvent.AXIS_RZ, 1}, {MotionEvent.AXIS_GAS, 1}});
|
||||
break;
|
||||
case "L3":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBL}, null);
|
||||
break;
|
||||
case "R3":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBR}, null);
|
||||
break;
|
||||
case "Analog":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_MODE}, null);
|
||||
break;
|
||||
default:
|
||||
log("Button '%s' not supported by auto mapping.", buttonName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setAxisBindings() {
|
||||
final String[] axisNames = AndroidHostInterface.getInstance().getControllerAxisNames(controllerType);
|
||||
if (axisNames == null || axisNames.length == 0) {
|
||||
log("No axes to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final String axisName : axisNames) {
|
||||
switch (axisName) {
|
||||
case "LeftX":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_X});
|
||||
break;
|
||||
case "LeftY":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_Y});
|
||||
break;
|
||||
case "RightX":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_RX, MotionEvent.AXIS_Z});
|
||||
break;
|
||||
case "RightY":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_RY, MotionEvent.AXIS_RZ});
|
||||
break;
|
||||
default:
|
||||
log("Axis '%s' not supported by auto mapping.", axisName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setVibrationBinding() {
|
||||
final int motorCount = AndroidHostInterface.getInstance().getControllerVibrationMotorCount(controllerType);
|
||||
if (motorCount == 0) {
|
||||
log("No vibration motors to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Vibrator vibrator = device.getVibrator();
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
log("Selected device has no vibrator, cannot bind vibration.");
|
||||
return;
|
||||
}
|
||||
|
||||
log("Binding vibration to device '%s'.", device.getDescriptor());
|
||||
|
||||
final String key = String.format("%sRumble", keyBase);
|
||||
editor.putString(key, device.getDescriptor());
|
||||
}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.ArraySet;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class ControllerBindingDialog extends AlertDialog {
|
||||
final static float DETECT_THRESHOLD = 0.25f;
|
||||
private final ControllerBindingPreference.Type mType;
|
||||
private final String mSettingKey;
|
||||
private String mCurrentBinding;
|
||||
private int mUpdatedAxisCode = -1;
|
||||
private final HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||
|
||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, ControllerBindingPreference.Type type) {
|
||||
super(context);
|
||||
|
||||
mType = type;
|
||||
mSettingKey = settingKey;
|
||||
mCurrentBinding = currentBinding;
|
||||
if (mCurrentBinding == null)
|
||||
mCurrentBinding = getContext().getString(R.string.controller_binding_dialog_no_binding);
|
||||
|
||||
setTitle(buttonName);
|
||||
updateMessage();
|
||||
setButton(BUTTON_POSITIVE, context.getString(R.string.controller_binding_dialog_cancel), (dialogInterface, button) -> dismiss());
|
||||
setButton(BUTTON_NEGATIVE, context.getString(R.string.controller_binding_dialog_clear), (dialogInterface, button) -> {
|
||||
mCurrentBinding = null;
|
||||
updateBinding();
|
||||
dismiss();
|
||||
});
|
||||
|
||||
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||
return onKeyDown(keyCode, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMessage() {
|
||||
setMessage(String.format(getContext().getString(R.string.controller_binding_dialog_message), mCurrentBinding));
|
||||
}
|
||||
|
||||
private void updateBinding() {
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
if (mCurrentBinding != null) {
|
||||
ArraySet<String> values = new ArraySet<>();
|
||||
values.add(mCurrentBinding);
|
||||
editor.putStringSet(mSettingKey, values);
|
||||
} else {
|
||||
try {
|
||||
editor.remove(mSettingKey);
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
final InputDevice device = event.getDevice();
|
||||
if (!EmulationSurfaceView.isBindableDevice(device) || !EmulationSurfaceView.isBindableKeyEvent(event)) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
if (mType == ControllerBindingPreference.Type.BUTTON || mType == ControllerBindingPreference.Type.HALF_AXIS) {
|
||||
mCurrentBinding = String.format("%s/Button%d", device.getDescriptor(), event.getKeyCode());
|
||||
} else if (mType == ControllerBindingPreference.Type.VIBRATION) {
|
||||
if (device.getVibrator() == null || !device.getVibrator().hasVibrator()) {
|
||||
Toast.makeText(getContext(), getContext().getString(R.string.controller_settings_vibration_unsupported), Toast.LENGTH_LONG).show();
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
mCurrentBinding = device.getDescriptor();
|
||||
} else {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
updateMessage();
|
||||
updateBinding();
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setAxisCode(InputDevice device, int axisCode, boolean positive) {
|
||||
if (mUpdatedAxisCode >= 0)
|
||||
return;
|
||||
|
||||
mUpdatedAxisCode = axisCode;
|
||||
|
||||
final int controllerIndex = 0;
|
||||
if (mType == ControllerBindingPreference.Type.AXIS || mType == ControllerBindingPreference.Type.HALF_AXIS)
|
||||
mCurrentBinding = String.format("%s/Axis%d", device.getDescriptor(), axisCode);
|
||||
else
|
||||
mCurrentBinding = String.format("%s/%cAxis%d", device.getDescriptor(), (positive) ? '+' : '-', axisCode);
|
||||
|
||||
updateBinding();
|
||||
updateMessage();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private boolean doAxisDetection(MotionEvent event) {
|
||||
if (!EmulationSurfaceView.isBindableDevice(event.getDevice()) || !EmulationSurfaceView.isJoystickMotionEvent(event))
|
||||
return false;
|
||||
|
||||
final List<InputDevice.MotionRange> motionEventList = event.getDevice().getMotionRanges();
|
||||
if (motionEventList == null || motionEventList.isEmpty())
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
if (!mStartingAxisValues.containsKey(deviceId)) {
|
||||
final float[] axisValues = new float[motionEventList.size()];
|
||||
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||
|
||||
if (event.getHistorySize() > 0)
|
||||
axisValues[axisIndex] = event.getHistoricalAxisValue(axisCode, 0);
|
||||
else if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
||||
axisValues[axisIndex] = 0.0f;
|
||||
else
|
||||
axisValues[axisIndex] = event.getAxisValue(axisCode);
|
||||
}
|
||||
|
||||
mStartingAxisValues.put(deviceId, axisValues);
|
||||
}
|
||||
|
||||
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
||||
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||
final float newValue = event.getAxisValue(axisCode);
|
||||
final float delta = newValue - axisValues[axisIndex];
|
||||
if (Math.abs(delta) >= DETECT_THRESHOLD) {
|
||||
setAxisCode(event.getDevice(), axisCode, delta >= 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
if (mType != ControllerBindingPreference.Type.AXIS &&
|
||||
mType != ControllerBindingPreference.Type.HALF_AXIS &&
|
||||
mType != ControllerBindingPreference.Type.BUTTON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doAxisDetection(event))
|
||||
return true;
|
||||
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.InputDevice;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class ControllerBindingPreference extends Preference {
|
||||
public enum Type {
|
||||
BUTTON,
|
||||
AXIS,
|
||||
HALF_AXIS,
|
||||
VIBRATION
|
||||
}
|
||||
|
||||
private enum VisualType {
|
||||
BUTTON,
|
||||
AXIS,
|
||||
VIBRATION,
|
||||
HOTKEY
|
||||
}
|
||||
|
||||
private String mBindingName;
|
||||
private String mDisplayName;
|
||||
private String mValue;
|
||||
private TextView mValueView;
|
||||
private Type mType = Type.BUTTON;
|
||||
private VisualType mVisualType = VisualType.BUTTON;
|
||||
|
||||
private static int getIconForButton(String buttonName) {
|
||||
if (buttonName.equals("Up")) {
|
||||
return R.drawable.ic_controller_up_button_pressed;
|
||||
} else if (buttonName.equals("Right")) {
|
||||
return R.drawable.ic_controller_right_button_pressed;
|
||||
} else if (buttonName.equals("Down")) {
|
||||
return R.drawable.ic_controller_down_button_pressed;
|
||||
} else if (buttonName.equals("Left")) {
|
||||
return R.drawable.ic_controller_left_button_pressed;
|
||||
} else if (buttonName.equals("Triangle")) {
|
||||
return R.drawable.ic_controller_triangle_button_pressed;
|
||||
} else if (buttonName.equals("Circle")) {
|
||||
return R.drawable.ic_controller_circle_button_pressed;
|
||||
} else if (buttonName.equals("Cross")) {
|
||||
return R.drawable.ic_controller_cross_button_pressed;
|
||||
} else if (buttonName.equals("Square")) {
|
||||
return R.drawable.ic_controller_square_button_pressed;
|
||||
} else if (buttonName.equals("Start")) {
|
||||
return R.drawable.ic_controller_start_button_pressed;
|
||||
} else if (buttonName.equals("Select")) {
|
||||
return R.drawable.ic_controller_select_button_pressed;
|
||||
} else if (buttonName.equals("L1")) {
|
||||
return R.drawable.ic_controller_l1_button_pressed;
|
||||
} else if (buttonName.equals("L2")) {
|
||||
return R.drawable.ic_controller_l2_button_pressed;
|
||||
} else if (buttonName.equals("R1")) {
|
||||
return R.drawable.ic_controller_r1_button_pressed;
|
||||
} else if (buttonName.equals("R2")) {
|
||||
return R.drawable.ic_controller_r2_button_pressed;
|
||||
}
|
||||
|
||||
return R.drawable.ic_baseline_radio_button_unchecked_24;
|
||||
}
|
||||
|
||||
private static int getIconForAxis(String axisName) {
|
||||
return R.drawable.ic_baseline_radio_button_checked_24;
|
||||
}
|
||||
|
||||
private static int getIconForHotkey(String hotkeyDisplayName) {
|
||||
switch (hotkeyDisplayName) {
|
||||
case "FastForward":
|
||||
case "ToggleFastForward":
|
||||
case "Turbo":
|
||||
case "ToggleTurbo":
|
||||
return R.drawable.ic_controller_fast_forward;
|
||||
|
||||
default:
|
||||
return R.drawable.ic_baseline_category_24;
|
||||
}
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
ImageView iconView = ((ImageView) holder.findViewById(R.id.controller_binding_icon));
|
||||
TextView nameView = ((TextView) holder.findViewById(R.id.controller_binding_name));
|
||||
mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value));
|
||||
|
||||
int drawableId = R.drawable.ic_baseline_radio_button_checked_24;
|
||||
switch (mVisualType) {
|
||||
case BUTTON:
|
||||
drawableId = getIconForButton(mBindingName);
|
||||
break;
|
||||
case AXIS:
|
||||
drawableId = getIconForAxis(mBindingName);
|
||||
break;
|
||||
case HOTKEY:
|
||||
drawableId = getIconForHotkey(mBindingName);
|
||||
break;
|
||||
case VIBRATION:
|
||||
drawableId = R.drawable.ic_baseline_vibration_24;
|
||||
break;
|
||||
}
|
||||
|
||||
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId));
|
||||
nameView.setText(mDisplayName);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mType);
|
||||
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void initButton(int controllerIndex, String buttonName) {
|
||||
mBindingName = buttonName;
|
||||
mDisplayName = buttonName;
|
||||
mType = Type.BUTTON;
|
||||
mVisualType = VisualType.BUTTON;
|
||||
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initAxis(int controllerIndex, String axisName, int axisType) {
|
||||
mBindingName = axisName;
|
||||
mDisplayName = axisName;
|
||||
mType = (axisType == AndroidHostInterface.CONTROLLER_AXIS_TYPE_HALF) ? Type.HALF_AXIS : Type.AXIS;
|
||||
mVisualType = VisualType.AXIS;
|
||||
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initVibration(int controllerIndex) {
|
||||
mBindingName = "Rumble";
|
||||
mDisplayName = getContext().getString(R.string.controller_binding_device_for_vibration);
|
||||
mType = Type.VIBRATION;
|
||||
mVisualType = VisualType.VIBRATION;
|
||||
setKey(String.format("Controller%d/Rumble", controllerIndex));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initHotkey(HotkeyInfo hotkeyInfo) {
|
||||
mBindingName = hotkeyInfo.getName();
|
||||
mDisplayName = hotkeyInfo.getDisplayName();
|
||||
mType = Type.BUTTON;
|
||||
mVisualType = VisualType.HOTKEY;
|
||||
setKey(hotkeyInfo.getBindingConfigKey());
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initAutoFireButton(int controllerIndex, int autoFireSlot) {
|
||||
mBindingName = String.format("AutoFire%d", autoFireSlot);
|
||||
mDisplayName = getContext().getString(R.string.controller_binding_auto_fire_n, autoFireSlot);
|
||||
mType = Type.BUTTON;
|
||||
mVisualType = VisualType.BUTTON;
|
||||
setKey(String.format("Controller%d/AutoFire%d", controllerIndex, autoFireSlot));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private String prettyPrintBinding(String value) {
|
||||
final int index = value.indexOf('/');
|
||||
String device, binding;
|
||||
if (index >= 0) {
|
||||
device = value.substring(0, index);
|
||||
binding = value.substring(index);
|
||||
} else {
|
||||
device = value;
|
||||
binding = "";
|
||||
}
|
||||
|
||||
String humanName = device;
|
||||
int deviceIndex = -1;
|
||||
|
||||
final int[] deviceIds = InputDevice.getDeviceIds();
|
||||
for (int i = 0; i < deviceIds.length; i++) {
|
||||
final InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]);
|
||||
if (inputDevice == null || !inputDevice.getDescriptor().equals(device)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
humanName = inputDevice.getName();
|
||||
deviceIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
final int MAX_LENGTH = 40;
|
||||
if (humanName.length() > MAX_LENGTH) {
|
||||
final StringBuilder shortenedName = new StringBuilder();
|
||||
shortenedName.append(humanName, 0, MAX_LENGTH / 2);
|
||||
shortenedName.append("...");
|
||||
shortenedName.append(humanName, humanName.length() - (MAX_LENGTH / 2),
|
||||
humanName.length());
|
||||
|
||||
humanName = shortenedName.toString();
|
||||
}
|
||||
|
||||
if (deviceIndex < 0)
|
||||
return String.format("%s[??]%s", humanName, binding);
|
||||
else
|
||||
return String.format("%s[%d]%s", humanName, deviceIndex, binding);
|
||||
}
|
||||
|
||||
private void updateValue(String value) {
|
||||
mValue = value;
|
||||
if (mValueView != null) {
|
||||
if (value != null)
|
||||
mValueView.setText(value);
|
||||
else
|
||||
mValueView.setText(getContext().getString(R.string.controller_binding_dialog_no_binding));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateValue() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Set<String> values = PreferenceHelpers.getStringSet(prefs, getKey());
|
||||
if (values != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String value : values) {
|
||||
if (sb.length() > 0)
|
||||
sb.append(", ");
|
||||
sb.append(prettyPrintBinding(value));
|
||||
}
|
||||
|
||||
updateValue(sb.toString());
|
||||
} else {
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearBinding(SharedPreferences.Editor prefEditor) {
|
||||
try {
|
||||
prefEditor.remove(getKey());
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ControllerSettingsActivity extends AppCompatActivity {
|
||||
private ControllerSettingsCollectionFragment fragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.settings_activity);
|
||||
|
||||
fragment = new ControllerSettingsCollectionFragment();
|
||||
fragment.setMultitapModeChangedListener(this::recreate);
|
||||
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, fragment)
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.controller_mapping_activity_title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_controller_mapping, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
final int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_load_profile) {
|
||||
doLoadProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_save_profile) {
|
||||
doSaveProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_clear_bindings) {
|
||||
fragment.clearAllBindings();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile() {
|
||||
final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
if (profileNames == null) {
|
||||
displayError(getString(R.string.controller_mapping_activity_no_profiles_found));
|
||||
return;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.controller_mapping_activity_select_input_profile)
|
||||
.setItems(profileNames, (dialog, choice) -> {
|
||||
doLoadProfile(profileNames[choice]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(R.string.controller_mapping_activity_cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile(String profileName) {
|
||||
if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) {
|
||||
displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName));
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.updateAllBindings();
|
||||
}
|
||||
|
||||
private void doSaveProfile() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final EditText input = new EditText(this);
|
||||
builder.setTitle(R.string.controller_mapping_activity_input_profile_name);
|
||||
builder.setView(input);
|
||||
builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> {
|
||||
final String name = input.getText().toString();
|
||||
if (name.isEmpty()) {
|
||||
displayError(getString(R.string.controller_mapping_activity_name_must_be_provided));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AndroidHostInterface.getInstance().saveInputProfile(name)) {
|
||||
displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile));
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(ControllerSettingsActivity.this, String.format(ControllerSettingsActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name),
|
||||
Toast.LENGTH_LONG).show();
|
||||
});
|
||||
builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,459 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.SeekBarPreference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ControllerSettingsCollectionFragment extends Fragment {
|
||||
public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode";
|
||||
private static final int NUM_MAIN_CONTROLLER_PORTS = 2;
|
||||
private static final int NUM_SUB_CONTROLLER_PORTS = 4;
|
||||
private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[]{'A', 'B', 'C', 'D'};
|
||||
private static final int NUM_AUTO_FIRE_BUTTONS = 4;
|
||||
|
||||
public interface MultitapModeChangedListener {
|
||||
void onChanged();
|
||||
}
|
||||
|
||||
private final ArrayList<ControllerBindingPreference> preferences = new ArrayList<>();
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
private String[] controllerPortNames;
|
||||
|
||||
private MultitapModeChangedListener multitapModeChangedListener;
|
||||
|
||||
public ControllerSettingsCollectionFragment() {
|
||||
}
|
||||
|
||||
public static String getControllerTypeKey(int port) {
|
||||
return String.format("Controller%d/Type", port);
|
||||
}
|
||||
|
||||
public static String getControllerType(SharedPreferences prefs, int port) {
|
||||
final String defaultControllerType = (port == 1) ? "DigitalController" : "None";
|
||||
return prefs.getString(getControllerTypeKey(port), defaultControllerType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
final String multitapMode = PreferenceManager.getDefaultSharedPreferences(getContext()).getString(
|
||||
MULTITAP_MODE_SETTINGS_KEY, "Disabled");
|
||||
|
||||
final ArrayList<String> portNames = new ArrayList<>();
|
||||
for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) {
|
||||
final boolean isMultitap = (multitapMode.equals("BothPorts") ||
|
||||
(i == 0 && multitapMode.equals("Port1Only")) ||
|
||||
(i == 1 && multitapMode.equals("Port2Only")));
|
||||
|
||||
if (isMultitap) {
|
||||
for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) {
|
||||
portNames.add(getContext().getString(
|
||||
R.string.controller_settings_sub_port_format,
|
||||
i + 1, SUB_CONTROLLER_PORT_NAMES[j]));
|
||||
}
|
||||
} else {
|
||||
portNames.add(getContext().getString(
|
||||
R.string.controller_settings_main_port_format,
|
||||
i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
controllerPortNames = new String[portNames.size()];
|
||||
portNames.toArray(controllerPortNames);
|
||||
|
||||
adapter = new SettingsCollectionAdapter(this, controllerPortNames.length);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
if (position == 0)
|
||||
tab.setText(R.string.controller_settings_tab_settings);
|
||||
else if (position <= controllerPortNames.length)
|
||||
tab.setText(controllerPortNames[position - 1]);
|
||||
else
|
||||
tab.setText(R.string.controller_settings_tab_hotkeys);
|
||||
}).attach();
|
||||
}
|
||||
|
||||
public void setMultitapModeChangedListener(MultitapModeChangedListener multitapModeChangedListener) {
|
||||
this.multitapModeChangedListener = multitapModeChangedListener;
|
||||
}
|
||||
|
||||
public void clearAllBindings() {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
for (ControllerBindingPreference pref : preferences)
|
||||
pref.clearBinding(prefEdit);
|
||||
prefEdit.commit();
|
||||
}
|
||||
|
||||
public void updateAllBindings() {
|
||||
for (ControllerBindingPreference pref : preferences)
|
||||
pref.updateValue();
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private final ControllerSettingsCollectionFragment parent;
|
||||
|
||||
public SettingsFragment(ControllerSettingsCollectionFragment parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.controllers_preferences, rootKey);
|
||||
|
||||
final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY);
|
||||
if (multitapModePreference != null) {
|
||||
multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
if (parent.multitapModeChangedListener != null)
|
||||
parent.multitapModeChangedListener.onChanged();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ControllerPortFragment extends PreferenceFragmentCompat {
|
||||
private final ControllerSettingsCollectionFragment parent;
|
||||
private final int controllerIndex;
|
||||
private PreferenceCategory mButtonsCategory;
|
||||
private PreferenceCategory mAxisCategory;
|
||||
private PreferenceCategory mSettingsCategory;
|
||||
private PreferenceCategory mAutoFireCategory;
|
||||
private PreferenceCategory mAutoFireBindingsCategory;
|
||||
|
||||
public ControllerPortFragment(ControllerSettingsCollectionFragment parent, int controllerIndex) {
|
||||
this.parent = parent;
|
||||
this.controllerIndex = controllerIndex;
|
||||
}
|
||||
|
||||
private static void clearBindingsInCategory(SharedPreferences.Editor editor, PreferenceCategory category) {
|
||||
for (int i = 0; i < category.getPreferenceCount(); i++) {
|
||||
final Preference preference = category.getPreference(i);
|
||||
if (preference instanceof ControllerBindingPreference)
|
||||
((ControllerBindingPreference) preference).clearBinding(editor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
setPreferenceScreen(ps);
|
||||
createPreferences();
|
||||
}
|
||||
|
||||
private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) {
|
||||
final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext());
|
||||
pref.setKey(key);
|
||||
pref.setTitle(title);
|
||||
pref.setSummary(summary);
|
||||
pref.setIconSpaceReserved(false);
|
||||
pref.setDefaultValue(defaultValue);
|
||||
return pref;
|
||||
}
|
||||
|
||||
private String getAutoToggleSummary(SharedPreferences sp, int slot) {
|
||||
final String button = sp.getString(String.format("AutoFire%dButton", slot), null);
|
||||
if (button == null || button.length() == 0)
|
||||
return "Not Configured";
|
||||
|
||||
return String.format("%s every %d frames", button, sp.getInt("AutoFire%dFrequency", 2));
|
||||
}
|
||||
|
||||
private void createPreferences() {
|
||||
final PreferenceScreen ps = getPreferenceScreen();
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
final String controllerType = getControllerType(sp, controllerIndex);
|
||||
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||
|
||||
final ListPreference typePreference = new ListPreference(getContext());
|
||||
typePreference.setEntries(R.array.settings_controller_type_entries);
|
||||
typePreference.setEntryValues(R.array.settings_controller_type_values);
|
||||
typePreference.setKey(getControllerTypeKey(controllerIndex));
|
||||
typePreference.setValue(controllerType);
|
||||
typePreference.setTitle(R.string.settings_controller_type);
|
||||
typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
typePreference.setIconSpaceReserved(false);
|
||||
typePreference.setOnPreferenceChangeListener((pref, value) -> {
|
||||
removePreferences();
|
||||
createPreferences(value.toString());
|
||||
return true;
|
||||
});
|
||||
ps.addPreference(typePreference);
|
||||
|
||||
final Preference autoBindPreference = new Preference(getContext());
|
||||
autoBindPreference.setTitle(R.string.controller_settings_automatic_mapping);
|
||||
autoBindPreference.setSummary(R.string.controller_settings_summary_automatic_mapping);
|
||||
autoBindPreference.setIconSpaceReserved(false);
|
||||
autoBindPreference.setOnPreferenceClickListener(preference -> {
|
||||
final ControllerAutoMapper mapper = new ControllerAutoMapper(getContext(), controllerIndex, () -> {
|
||||
removePreferences();
|
||||
createPreferences(typePreference.getValue());
|
||||
});
|
||||
mapper.start();
|
||||
return true;
|
||||
});
|
||||
ps.addPreference(autoBindPreference);
|
||||
|
||||
final Preference clearBindingsPreference = new Preference(getContext());
|
||||
clearBindingsPreference.setTitle(R.string.controller_settings_clear_controller_bindings);
|
||||
clearBindingsPreference.setSummary(R.string.controller_settings_summary_clear_controller_bindings);
|
||||
clearBindingsPreference.setIconSpaceReserved(false);
|
||||
clearBindingsPreference.setOnPreferenceClickListener(preference -> {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setMessage(R.string.controller_settings_clear_controller_bindings_confirm);
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
clearBindings();
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
return true;
|
||||
});
|
||||
ps.addPreference(clearBindingsPreference);
|
||||
|
||||
mButtonsCategory = new PreferenceCategory(getContext());
|
||||
mButtonsCategory.setTitle(R.string.controller_settings_category_button_bindings);
|
||||
mButtonsCategory.setIconSpaceReserved(false);
|
||||
ps.addPreference(mButtonsCategory);
|
||||
|
||||
mAxisCategory = new PreferenceCategory(getContext());
|
||||
mAxisCategory.setTitle(R.string.controller_settings_category_axis_bindings);
|
||||
mAxisCategory.setIconSpaceReserved(false);
|
||||
ps.addPreference(mAxisCategory);
|
||||
|
||||
mSettingsCategory = new PreferenceCategory(getContext());
|
||||
mSettingsCategory.setTitle(R.string.controller_settings_category_settings);
|
||||
mSettingsCategory.setIconSpaceReserved(false);
|
||||
ps.addPreference(mSettingsCategory);
|
||||
|
||||
mAutoFireCategory = new PreferenceCategory(getContext());
|
||||
mAutoFireCategory.setTitle(R.string.controller_settings_category_auto_fire_buttons);
|
||||
mAutoFireCategory.setIconSpaceReserved(false);
|
||||
ps.addPreference(mAutoFireCategory);
|
||||
|
||||
mAutoFireBindingsCategory = new PreferenceCategory(getContext());
|
||||
mAutoFireBindingsCategory.setTitle(R.string.controller_settings_category_auto_fire_bindings);
|
||||
mAutoFireBindingsCategory.setIconSpaceReserved(false);
|
||||
ps.addPreference(mAutoFireBindingsCategory);
|
||||
|
||||
createPreferences(controllerType);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void createPreferences(String controllerType) {
|
||||
final PreferenceScreen ps = getPreferenceScreen();
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
final String[] buttonNames = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
final String[] axisNames = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||
|
||||
if (buttonNames != null) {
|
||||
for (String buttonName : buttonNames) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initButton(controllerIndex, buttonName);
|
||||
mButtonsCategory.addPreference(cbp);
|
||||
parent.preferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
if (axisNames != null) {
|
||||
for (String axisName : axisNames) {
|
||||
final int axisType = AndroidHostInterface.getControllerAxisType(controllerType, axisName);
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initAxis(controllerIndex, axisName, axisType);
|
||||
mAxisCategory.addPreference(cbp);
|
||||
parent.preferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
if (vibrationMotors > 0) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initVibration(controllerIndex);
|
||||
mSettingsCategory.addPreference(cbp);
|
||||
parent.preferences.add(cbp);
|
||||
}
|
||||
|
||||
if (controllerType.equals("AnalogController")) {
|
||||
mSettingsCategory.addPreference(
|
||||
createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex),
|
||||
R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_enable_analog_mode_on_reset, true));
|
||||
|
||||
mSettingsCategory.addPreference(
|
||||
createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex),
|
||||
R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_use_analog_sticks_for_dpad, true));
|
||||
}
|
||||
|
||||
if (buttonNames != null) {
|
||||
for (int autoFireSlot = 1; autoFireSlot <= NUM_AUTO_FIRE_BUTTONS; autoFireSlot++) {
|
||||
final ListPreference autoFirePreference = new ListPreference(getContext());
|
||||
autoFirePreference.setEntries(buttonNames);
|
||||
autoFirePreference.setEntryValues(buttonNames);
|
||||
autoFirePreference.setKey(String.format("Controller%d/AutoFire%dButton", controllerIndex, autoFireSlot));
|
||||
autoFirePreference.setTitle(getContext().getString(R.string.controller_settings_auto_fire_n_button, autoFireSlot));
|
||||
autoFirePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
autoFirePreference.setIconSpaceReserved(false);
|
||||
mAutoFireCategory.addPreference(autoFirePreference);
|
||||
|
||||
final SeekBarPreference frequencyPreference = new SeekBarPreference(getContext());
|
||||
frequencyPreference.setMin(1);
|
||||
frequencyPreference.setMax(60);
|
||||
frequencyPreference.setKey(String.format("Controller%d/AutoFire%dFrequency", controllerIndex, autoFireSlot));
|
||||
frequencyPreference.setDefaultValue(2);
|
||||
frequencyPreference.setTitle(getContext().getString(R.string.controller_settings_auto_fire_n_frequency, autoFireSlot));
|
||||
frequencyPreference.setIconSpaceReserved(false);
|
||||
frequencyPreference.setShowSeekBarValue(true);
|
||||
mAutoFireCategory.addPreference(frequencyPreference);
|
||||
}
|
||||
|
||||
for (int autoFireSlot = 1; autoFireSlot <= NUM_AUTO_FIRE_BUTTONS; autoFireSlot++) {
|
||||
final ControllerBindingPreference bindingPreference = new ControllerBindingPreference(getContext(), null);
|
||||
bindingPreference.initAutoFireButton(controllerIndex, autoFireSlot);
|
||||
mAutoFireBindingsCategory.addPreference(bindingPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removePreferences() {
|
||||
for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) {
|
||||
parent.preferences.remove(mButtonsCategory.getPreference(i));
|
||||
}
|
||||
mButtonsCategory.removeAll();
|
||||
|
||||
for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) {
|
||||
parent.preferences.remove(mAxisCategory.getPreference(i));
|
||||
}
|
||||
mAxisCategory.removeAll();
|
||||
|
||||
for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) {
|
||||
parent.preferences.remove(mSettingsCategory.getPreference(i));
|
||||
}
|
||||
mSettingsCategory.removeAll();
|
||||
|
||||
for (int i = 0; i < mAutoFireCategory.getPreferenceCount(); i++) {
|
||||
parent.preferences.remove(mAutoFireCategory.getPreference(i));
|
||||
}
|
||||
mAutoFireCategory.removeAll();
|
||||
|
||||
for (int i = 0; i < mAutoFireBindingsCategory.getPreferenceCount(); i++) {
|
||||
parent.preferences.remove(mAutoFireBindingsCategory.getPreference(i));
|
||||
}
|
||||
mAutoFireBindingsCategory.removeAll();
|
||||
}
|
||||
|
||||
private void clearBindings() {
|
||||
final SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
|
||||
clearBindingsInCategory(editor, mButtonsCategory);
|
||||
clearBindingsInCategory(editor, mAxisCategory);
|
||||
clearBindingsInCategory(editor, mSettingsCategory);
|
||||
editor.commit();
|
||||
|
||||
Toast.makeText(parent.getContext(), parent.getString(
|
||||
R.string.controller_settings_clear_controller_bindings_done, controllerIndex),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static class HotkeyFragment extends PreferenceFragmentCompat {
|
||||
private final ControllerSettingsCollectionFragment parent;
|
||||
private final HotkeyInfo[] mHotkeyInfo;
|
||||
|
||||
public HotkeyFragment(ControllerSettingsCollectionFragment parent) {
|
||||
this.parent = parent;
|
||||
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
if (mHotkeyInfo != null) {
|
||||
final HashMap<String, PreferenceCategory> categoryMap = new HashMap<>();
|
||||
|
||||
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
|
||||
PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ?
|
||||
categoryMap.get(hotkeyInfo.getCategory()) : null;
|
||||
if (category == null) {
|
||||
category = new PreferenceCategory(getContext());
|
||||
category.setTitle(hotkeyInfo.getCategory());
|
||||
category.setIconSpaceReserved(false);
|
||||
categoryMap.put(hotkeyInfo.getCategory(), category);
|
||||
ps.addPreference(category);
|
||||
}
|
||||
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initHotkey(hotkeyInfo);
|
||||
category.addPreference(cbp);
|
||||
parent.preferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private final ControllerSettingsCollectionFragment parent;
|
||||
private final int controllerPorts;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull ControllerSettingsCollectionFragment parent, int controllerPorts) {
|
||||
super(parent);
|
||||
this.parent = parent;
|
||||
this.controllerPorts = controllerPorts;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
if (position == 0)
|
||||
return new SettingsFragment(parent);
|
||||
else if (position <= controllerPorts)
|
||||
return new ControllerPortFragment(parent, position);
|
||||
else
|
||||
return new HotkeyFragment(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return controllerPorts + 2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
public enum DiscRegion {
|
||||
NTSC_J,
|
||||
NTSC_U,
|
||||
PAL,
|
||||
Other
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class EmptyGameListFragment extends Fragment {
|
||||
private static final String SUPPORTED_FORMATS_STRING =
|
||||
".cue (Cue Sheets)\n" +
|
||||
".iso/.img (Single Track Image)\n" +
|
||||
".ecm (Error Code Modeling Image)\n" +
|
||||
".mds (Media Descriptor Sidecar)\n" +
|
||||
".chd (Compressed Hunks of Data)\n" +
|
||||
".pbp (PlayStation Portable, Only Decrypted)";
|
||||
|
||||
private MainActivity parent;
|
||||
|
||||
public EmptyGameListFragment(MainActivity parent) {
|
||||
super(R.layout.fragment_empty_game_list);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
((TextView) view.findViewById(R.id.supported_formats)).setText(
|
||||
getString(R.string.main_activity_empty_game_list_supported_formats, SUPPORTED_FORMATS_STRING));
|
||||
((Button) view.findViewById(R.id.add_game_directory)).setOnClickListener(v -> parent.startAddGameDirectory());
|
||||
}
|
||||
}
|
|
@ -1,988 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
/**
|
||||
* An example full-screen activity that shows and hides the system UI (i.e.
|
||||
* status bar and navigation/system bar) with user interaction.
|
||||
*/
|
||||
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
|
||||
/**
|
||||
* Settings interfaces.
|
||||
*/
|
||||
private SharedPreferences mPreferences;
|
||||
private boolean mWasDestroyed = false;
|
||||
private boolean mStopRequested = false;
|
||||
private boolean mApplySettingsOnSurfaceRestored = false;
|
||||
private String mGamePath = null;
|
||||
private String mGameCode = null;
|
||||
private String mGameTitle = null;
|
||||
private String mGameCoverPath = null;
|
||||
private EmulationSurfaceView mContentView;
|
||||
private MenuDialogFragment mPauseMenu;
|
||||
|
||||
private boolean getBooleanSetting(String key, boolean defaultValue) {
|
||||
return mPreferences.getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
private void setBooleanSetting(String key, boolean value) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private String getStringSetting(String key, String defaultValue) {
|
||||
return mPreferences.getString(key, defaultValue);
|
||||
}
|
||||
|
||||
private int getIntSetting(String key, int defaultValue) {
|
||||
try {
|
||||
return mPreferences.getInt(key, defaultValue);
|
||||
} catch (ClassCastException e) {
|
||||
try {
|
||||
final String stringValue = mPreferences.getString(key, Integer.toString(defaultValue));
|
||||
return Integer.parseInt(stringValue);
|
||||
} catch (Exception e2) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStringSetting(String key, String value) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putString(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private void reportErrorOnUIThread(String message) {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_LONG);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
enableFullscreenImmersive();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public void reportError(String message) {
|
||||
Log.e("EmulationActivity", message);
|
||||
|
||||
Object lock = new Object();
|
||||
runOnUiThread(() -> {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_LONG);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
enableFullscreenImmersive();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EmulationThread mEmulationThread;
|
||||
|
||||
private void stopEmulationThread() {
|
||||
if (mEmulationThread == null)
|
||||
return;
|
||||
|
||||
mEmulationThread.stopAndJoin();
|
||||
mEmulationThread = null;
|
||||
}
|
||||
|
||||
public void onEmulationStarted() {
|
||||
runOnUiThread(() -> {
|
||||
updateRequestedOrientation();
|
||||
updateOrientation();
|
||||
});
|
||||
}
|
||||
|
||||
public void onEmulationStopped() {
|
||||
runOnUiThread(() -> {
|
||||
if (!mWasDestroyed && !mStopRequested)
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
public void onRunningGameChanged(String path, String code, String title, String coverPath) {
|
||||
runOnUiThread(() -> {
|
||||
mGamePath = path;
|
||||
mGameTitle = title;
|
||||
mGameCode = code;
|
||||
mGameCoverPath = coverPath;
|
||||
});
|
||||
}
|
||||
|
||||
public float getRefreshRate() {
|
||||
WindowManager windowManager = getWindowManager();
|
||||
if (windowManager == null) {
|
||||
windowManager = ((WindowManager) getSystemService(Context.WINDOW_SERVICE));
|
||||
if (windowManager == null)
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
if (display == null)
|
||||
return -1.0f;
|
||||
|
||||
return display.getRefreshRate();
|
||||
}
|
||||
|
||||
public void openPauseMenu() {
|
||||
runOnUiThread(() -> {
|
||||
showPauseMenu();
|
||||
});
|
||||
}
|
||||
|
||||
public String[] getInputDeviceNames() {
|
||||
return (mContentView != null) ? mContentView.getInputDeviceNames() : null;
|
||||
}
|
||||
|
||||
public boolean hasInputDeviceVibration(int controllerIndex) {
|
||||
return (mContentView != null) ? mContentView.hasInputDeviceVibration(controllerIndex) : null;
|
||||
}
|
||||
|
||||
public void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||
if (mContentView != null)
|
||||
mContentView.setInputDeviceVibration(controllerIndex, smallMotor, largeMotor);
|
||||
}
|
||||
|
||||
private void doApplySettings() {
|
||||
AndroidHostInterface.getInstance().applySettings();
|
||||
updateRequestedOrientation();
|
||||
updateControllers();
|
||||
updateSustainedPerformanceMode();
|
||||
updateDisplayInCutout();
|
||||
}
|
||||
|
||||
private void applySettings() {
|
||||
if (!AndroidHostInterface.getInstance().isEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
if (AndroidHostInterface.getInstance().hasSurface()) {
|
||||
doApplySettings();
|
||||
} else {
|
||||
mApplySettingsOnSurfaceRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the activity if it was restored without properly being created.
|
||||
private boolean checkActivityIsValid() {
|
||||
if (!AndroidHostInterface.hasInstance() || !AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
// Once we get a surface, we can boot.
|
||||
AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height);
|
||||
|
||||
if (mEmulationThread != null) {
|
||||
updateOrientation();
|
||||
|
||||
if (mApplySettingsOnSurfaceRestored) {
|
||||
mApplySettingsOnSurfaceRestored = false;
|
||||
doApplySettings();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final String bootPath = getIntent().getStringExtra("bootPath");
|
||||
final boolean saveStateOnExit = getBooleanSetting("Main/SaveStateOnExit", true);
|
||||
final boolean resumeState = getIntent().getBooleanExtra("resumeState", saveStateOnExit);
|
||||
final String bootSaveStatePath = getIntent().getStringExtra("saveStatePath");
|
||||
|
||||
mEmulationThread = EmulationThread.create(this, bootPath, resumeState, bootSaveStatePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.i("EmulationActivity", "Surface destroyed");
|
||||
|
||||
if (mPauseMenu != null)
|
||||
mPauseMenu.close(false);
|
||||
|
||||
// Save the resume state in case we never get back again...
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadRunning() && !mStopRequested)
|
||||
AndroidHostInterface.getInstance().saveResumeState(true);
|
||||
|
||||
AndroidHostInterface.getInstance().surfaceChanged(null, 0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Log.i("EmulationActivity", "OnCreate");
|
||||
|
||||
// we might be coming from a third-party launcher if the host interface isn't setup
|
||||
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
enableFullscreenImmersive();
|
||||
setContentView(R.layout.activity_emulation);
|
||||
|
||||
mContentView = findViewById(R.id.fullscreen_content);
|
||||
mContentView.getHolder().addCallback(this);
|
||||
mContentView.setFocusableInTouchMode(true);
|
||||
mContentView.setFocusable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mContentView.setFocusedByDefault(true);
|
||||
}
|
||||
mContentView.requestFocus();
|
||||
|
||||
// Sort out rotation.
|
||||
updateOrientation();
|
||||
updateSustainedPerformanceMode();
|
||||
updateDisplayInCutout();
|
||||
|
||||
// Hook up controller input.
|
||||
updateControllers();
|
||||
registerInputDeviceListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
enableFullscreenImmersive();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
enableFullscreenImmersive();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.i("EmulationActivity", "OnStop");
|
||||
if (mEmulationThread != null) {
|
||||
mWasDestroyed = true;
|
||||
stopEmulationThread();
|
||||
}
|
||||
|
||||
unregisterInputDeviceListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (!checkActivityIsValid()) {
|
||||
// we must've got killed off in the background :(
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestCode == REQUEST_CODE_SETTINGS) {
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
|
||||
applySettings();
|
||||
}
|
||||
} else if (requestCode == REQUEST_IMPORT_PATCH_CODES) {
|
||||
if (data == null || data.getData() == null)
|
||||
return;
|
||||
|
||||
importPatchesFromFile(data.getData());
|
||||
} else if (requestCode == REQUEST_CHANGE_DISC_FILE) {
|
||||
if (data == null || data.getData() == null)
|
||||
return;
|
||||
|
||||
AndroidHostInterface.getInstance().setMediaFilename(data.getDataString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
showPauseMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (mContentView.onKeyDown(event.getKeyCode(), event))
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (mContentView.onKeyUp(event.getKeyCode(), event))
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
|
||||
if (mContentView.onGenericMotionEvent(ev))
|
||||
return true;
|
||||
|
||||
return super.dispatchGenericMotionEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (checkActivityIsValid())
|
||||
updateOrientation(newConfig.orientation);
|
||||
}
|
||||
|
||||
private void updateRequestedOrientation() {
|
||||
final String orientation = getStringSetting("Main/EmulationScreenOrientation", "unspecified");
|
||||
if (orientation.equals("portrait"))
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
|
||||
else if (orientation.equals("landscape"))
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
|
||||
else if (orientation.equals("sensor"))
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
|
||||
else
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
|
||||
private void updateOrientation() {
|
||||
final int orientation = getResources().getConfiguration().orientation;
|
||||
updateOrientation(orientation);
|
||||
}
|
||||
|
||||
private void updateOrientation(int newOrientation) {
|
||||
if (newOrientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_TOP_OR_LEFT);
|
||||
else
|
||||
AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_CENTER);
|
||||
|
||||
if (mTouchscreenController != null)
|
||||
mTouchscreenController.updateOrientation();
|
||||
}
|
||||
|
||||
private void enableFullscreenImmersive() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
if (mContentView != null)
|
||||
mContentView.requestFocus();
|
||||
}
|
||||
|
||||
private void updateDisplayInCutout() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||
return;
|
||||
|
||||
final boolean shouldExpand = getBooleanSetting("Display/ExpandToCutout", false);
|
||||
final boolean isExpanded = getWindow().getAttributes().layoutInDisplayCutoutMode ==
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
|
||||
if (shouldExpand == isExpanded)
|
||||
return;
|
||||
|
||||
WindowManager.LayoutParams attribs = getWindow().getAttributes();
|
||||
attribs.layoutInDisplayCutoutMode = shouldExpand ?
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES :
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
|
||||
getWindow().setAttributes(attribs);
|
||||
}
|
||||
|
||||
private static final int REQUEST_CODE_SETTINGS = 0;
|
||||
private static final int REQUEST_IMPORT_PATCH_CODES = 1;
|
||||
private static final int REQUEST_CHANGE_DISC_FILE = 2;
|
||||
|
||||
private void onMenuClosed() {
|
||||
enableFullscreenImmersive();
|
||||
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadPaused())
|
||||
AndroidHostInterface.getInstance().pauseEmulationThread(false);
|
||||
}
|
||||
|
||||
private boolean disableDialogMenuItem(AlertDialog dialog, int index) {
|
||||
final ListView listView = dialog.getListView();
|
||||
if (listView == null)
|
||||
return false;
|
||||
|
||||
final View childItem = listView.getChildAt(index);
|
||||
if (childItem == null)
|
||||
return false;
|
||||
|
||||
childItem.setEnabled(false);
|
||||
childItem.setClickable(false);
|
||||
childItem.setOnClickListener((v) -> {});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showPauseMenu() {
|
||||
if (!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
|
||||
AndroidHostInterface.getInstance().pauseEmulationThread(true);
|
||||
}
|
||||
|
||||
if (mPauseMenu != null)
|
||||
mPauseMenu.close(false);
|
||||
|
||||
mPauseMenu = new MenuDialogFragment(this);
|
||||
mPauseMenu.show(getSupportFragmentManager(), "MenuDialogFragment");
|
||||
}
|
||||
|
||||
private void showSaveStateMenu(boolean saving) {
|
||||
final SaveStateInfo[] infos = AndroidHostInterface.getInstance().getSaveStateInfo(true);
|
||||
if (infos == null) {
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final ListView listView = new ListView(this);
|
||||
listView.setAdapter(new SaveStateInfo.ListAdapter(this, infos));
|
||||
builder.setView(listView);
|
||||
builder.setOnDismissListener((dialog) -> {
|
||||
onMenuClosed();
|
||||
});
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
SaveStateInfo info = infos[position];
|
||||
if (saving) {
|
||||
AndroidHostInterface.getInstance().saveState(info.isGlobal(), info.getSlot());
|
||||
} else {
|
||||
AndroidHostInterface.getInstance().loadState(info.isGlobal(), info.getSlot());
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showTouchscreenControllerMenu() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.dialog_touchscreen_controller_settings);
|
||||
builder.setItems(R.array.emulation_touchscreen_menu, (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0: // Change Type
|
||||
{
|
||||
final String currentValue = getStringSetting("Controller1/TouchscreenControllerView", "");
|
||||
final String[] values = getResources().getStringArray(R.array.settings_touchscreen_controller_view_values);
|
||||
int currentIndex = -1;
|
||||
for (int k = 0; k < values.length; k++) {
|
||||
if (currentValue.equals(values[k])) {
|
||||
currentIndex = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final AlertDialog.Builder subBuilder = new AlertDialog.Builder(this);
|
||||
subBuilder.setTitle(R.string.dialog_touchscreen_controller_type);
|
||||
subBuilder.setSingleChoiceItems(R.array.settings_touchscreen_controller_view_entries, currentIndex, (dialog, j) -> {
|
||||
setStringSetting("Controller1/TouchscreenControllerView", values[j]);
|
||||
updateControllers();
|
||||
});
|
||||
subBuilder.setNegativeButton(R.string.dialog_done, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
subBuilder.setOnDismissListener(dialog -> onMenuClosed());
|
||||
subBuilder.create().show();
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // Change Opacity
|
||||
{
|
||||
if (mTouchscreenController != null) {
|
||||
AlertDialog.Builder subBuilder = mTouchscreenController.createOpacityDialog(this);
|
||||
subBuilder.setOnDismissListener(dialog -> onMenuClosed());
|
||||
subBuilder.create().show();
|
||||
} else {
|
||||
onMenuClosed();
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Add/Remove Buttons
|
||||
{
|
||||
if (mTouchscreenController != null) {
|
||||
AlertDialog.Builder subBuilder = mTouchscreenController.createAddRemoveButtonDialog(this);
|
||||
subBuilder.setOnDismissListener(dialog -> onMenuClosed());
|
||||
subBuilder.create().show();
|
||||
} else {
|
||||
onMenuClosed();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // Edit Positions
|
||||
case 4: // Edit Scale
|
||||
{
|
||||
if (mTouchscreenController != null) {
|
||||
// we deliberately don't call onMenuClosed() here to keep the system paused.
|
||||
// but we need to re-enable immersive mode to get proper editing.
|
||||
enableFullscreenImmersive();
|
||||
mTouchscreenController.startLayoutEditing(
|
||||
(i == 4) ? TouchscreenControllerView.EditMode.SCALE :
|
||||
TouchscreenControllerView.EditMode.POSITION);
|
||||
} else {
|
||||
// no controller
|
||||
onMenuClosed();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showPatchesMenu() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
|
||||
final PatchCode[] codes = AndroidHostInterface.getInstance().getPatchCodeList();
|
||||
if (codes != null) {
|
||||
CharSequence[] items = new CharSequence[codes.length];
|
||||
boolean[] itemsChecked = new boolean[codes.length];
|
||||
for (int i = 0; i < codes.length; i++) {
|
||||
final PatchCode cc = codes[i];
|
||||
items[i] = cc.getDisplayText();
|
||||
itemsChecked[i] = cc.isEnabled();
|
||||
}
|
||||
|
||||
builder.setMultiChoiceItems(items, itemsChecked, (dialogInterface, i, checked) -> {
|
||||
AndroidHostInterface.getInstance().setPatchCodeEnabled(i, checked);
|
||||
});
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.emulation_activity_ok, (dialogInterface, i) -> {
|
||||
dialogInterface.dismiss();
|
||||
});
|
||||
builder.setNeutralButton(R.string.emulation_activity_import_patch_codes, (dialogInterface, i) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.emulation_activity_choose_patch_code_file)), REQUEST_IMPORT_PATCH_CODES);
|
||||
});
|
||||
|
||||
builder.setOnDismissListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void importPatchesFromFile(Uri uri) {
|
||||
String str = FileHelper.readStringFromUri(this, uri, 512 * 1024);
|
||||
if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) {
|
||||
reportErrorOnUIThread(getString(R.string.emulation_activity_failed_to_import_patch_codes));
|
||||
}
|
||||
}
|
||||
|
||||
private void startDiscChangeFromFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_CHANGE_DISC_FILE);
|
||||
}
|
||||
|
||||
private void showDiscChangeMenu() {
|
||||
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||
|
||||
if (!hi.hasMediaSubImages()) {
|
||||
startDiscChangeFromFile();
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] paths = AndroidHostInterface.getInstance().getMediaSubImageTitles();
|
||||
final int currentPath = AndroidHostInterface.getInstance().getMediaSubImageIndex();
|
||||
final int numPaths = (paths != null) ? paths.length : 0;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
|
||||
CharSequence[] items = new CharSequence[numPaths + 1];
|
||||
for (int i = 0; i < numPaths; i++)
|
||||
items[i] = FileHelper.getFileNameForPath(paths[i]);
|
||||
items[numPaths] = getString(R.string.emulation_activity_change_disc_select_new_file);
|
||||
|
||||
builder.setSingleChoiceItems(items, (currentPath < numPaths) ? currentPath : -1, (dialogInterface, i) -> {
|
||||
dialogInterface.dismiss();
|
||||
onMenuClosed();
|
||||
|
||||
if (i < numPaths) {
|
||||
AndroidHostInterface.getInstance().switchMediaSubImage(i);
|
||||
} else {
|
||||
startDiscChangeFromFile();
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showAchievementsPopup() {
|
||||
final Achievement[] achievements = AndroidHostInterface.getInstance().getCheevoList();
|
||||
if (achievements == null) {
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
final AchievementListFragment alf = new AchievementListFragment(achievements);
|
||||
alf.show(getSupportFragmentManager(), "fragment_achievement_list");
|
||||
alf.setOnDismissListener(dialog -> onMenuClosed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Touchscreen controller overlay
|
||||
*/
|
||||
TouchscreenControllerView mTouchscreenController;
|
||||
|
||||
public void updateControllers() {
|
||||
final int touchscreenControllerIndex = getIntSetting("TouchscreenController/PortIndex", 0);
|
||||
final String touchscreenControllerPrefix = String.format("Controller%d/", touchscreenControllerIndex + 1);
|
||||
final String controllerType = getStringSetting(touchscreenControllerPrefix + "Type", "DigitalController");
|
||||
final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital");
|
||||
final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false);
|
||||
final boolean touchGliding = getBooleanSetting("Controller1/TouchGliding", false);
|
||||
final boolean hapticFeedback = getBooleanSetting("Controller1/HapticFeedback", false);
|
||||
final boolean vibration = getBooleanSetting("Controller1/Vibration", false);
|
||||
final FrameLayout activityLayout = findViewById(R.id.frameLayout);
|
||||
|
||||
Log.i("EmulationActivity", "Controller type: " + controllerType);
|
||||
Log.i("EmulationActivity", "View type: " + viewType);
|
||||
|
||||
mContentView.updateInputDevices();
|
||||
AndroidHostInterface.getInstance().updateInputMap();
|
||||
|
||||
final boolean hasAnyControllers = mContentView.hasAnyGamePads();
|
||||
if (controllerType.equals("None") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) {
|
||||
if (mTouchscreenController != null) {
|
||||
activityLayout.removeView(mTouchscreenController);
|
||||
mTouchscreenController = null;
|
||||
}
|
||||
} else {
|
||||
if (mTouchscreenController == null) {
|
||||
mTouchscreenController = new TouchscreenControllerView(this);
|
||||
activityLayout.addView(mTouchscreenController);
|
||||
}
|
||||
|
||||
mTouchscreenController.init(touchscreenControllerIndex, controllerType, viewType, hapticFeedback, touchGliding);
|
||||
}
|
||||
|
||||
if (vibration)
|
||||
mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||
else
|
||||
mVibratorService = null;
|
||||
|
||||
// Place notifications in the middle of the screen, rather then the bottom (because touchscreen).
|
||||
float notificationVerticalPosition = 1.0f;
|
||||
float notificationVerticalDirection = -1.0f;
|
||||
if (mTouchscreenController != null) {
|
||||
notificationVerticalPosition = 0.3f;
|
||||
notificationVerticalDirection = -1.0f;
|
||||
}
|
||||
AndroidHostInterface.getInstance().setFullscreenUINotificationVerticalPosition(
|
||||
notificationVerticalPosition, notificationVerticalDirection);
|
||||
}
|
||||
|
||||
private InputManager.InputDeviceListener mInputDeviceListener;
|
||||
|
||||
private void registerInputDeviceListener() {
|
||||
if (mInputDeviceListener != null)
|
||||
return;
|
||||
|
||||
mInputDeviceListener = new InputManager.InputDeviceListener() {
|
||||
@Override
|
||||
public void onInputDeviceAdded(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceAdded %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceRemoved %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceChanged %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
};
|
||||
|
||||
InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE));
|
||||
if (inputManager != null)
|
||||
inputManager.registerInputDeviceListener(mInputDeviceListener, null);
|
||||
}
|
||||
|
||||
private void unregisterInputDeviceListener() {
|
||||
if (mInputDeviceListener == null)
|
||||
return;
|
||||
|
||||
InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE));
|
||||
if (inputManager != null)
|
||||
inputManager.unregisterInputDeviceListener(mInputDeviceListener);
|
||||
|
||||
mInputDeviceListener = null;
|
||||
}
|
||||
|
||||
private Vibrator mVibratorService;
|
||||
|
||||
public void setVibration(boolean enabled) {
|
||||
if (mVibratorService == null)
|
||||
return;
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (mVibratorService == null)
|
||||
return;
|
||||
|
||||
if (enabled)
|
||||
mVibratorService.vibrate(1000);
|
||||
else
|
||||
mVibratorService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean mSustainedPerformanceModeEnabled = false;
|
||||
|
||||
private void updateSustainedPerformanceMode() {
|
||||
final boolean enabled = getBooleanSetting("Main/SustainedPerformanceMode", false);
|
||||
if (mSustainedPerformanceModeEnabled == enabled)
|
||||
return;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
getWindow().setSustainedPerformanceMode(enabled);
|
||||
Log.i("EmulationActivity", String.format("%s sustained performance mode.", enabled ? "enabling" : "disabling"));
|
||||
} else {
|
||||
Log.e("EmulationActivity", "Sustained performance mode not supported.");
|
||||
}
|
||||
mSustainedPerformanceModeEnabled = enabled;
|
||||
|
||||
}
|
||||
|
||||
public static class MenuDialogFragment extends DialogFragment {
|
||||
private EmulationActivity emulationActivity;
|
||||
private boolean settingsChanged = false;
|
||||
|
||||
public MenuDialogFragment(EmulationActivity emulationActivity) {
|
||||
this.emulationActivity = emulationActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStyle(STYLE_NO_FRAME, R.style.EmulationActivityOverlay);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_emulation_activity_overlay, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
setContentFragment(new MenuSettingsFragment(this, emulationActivity), false);
|
||||
|
||||
final ImageView coverView =((ImageView)view.findViewById(R.id.cover_image));
|
||||
if (emulationActivity.mGameCoverPath != null && !emulationActivity.mGameCoverPath.isEmpty()) {
|
||||
new ImageLoadTask(coverView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
emulationActivity.mGameCoverPath);
|
||||
} else if (emulationActivity.mGameTitle != null) {
|
||||
new GenerateCoverTask(getContext(), coverView, emulationActivity.mGameTitle)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
coverView.setOnClickListener(v -> close(true));
|
||||
|
||||
if (emulationActivity.mGameTitle != null)
|
||||
((TextView)view.findViewById(R.id.title)).setText(emulationActivity.mGameTitle);
|
||||
|
||||
if (emulationActivity.mGameCode != null && emulationActivity.mGamePath != null)
|
||||
{
|
||||
final String subtitle = String.format("%s - %s", emulationActivity.mGameCode,
|
||||
FileHelper.getFileNameForPath(emulationActivity.mGamePath));
|
||||
((TextView)view.findViewById(R.id.subtitle)).setText(subtitle);
|
||||
}
|
||||
|
||||
((ImageButton)view.findViewById(R.id.menu)).setOnClickListener(v -> onMenuClicked());
|
||||
((ImageButton)view.findViewById(R.id.controller_settings)).setOnClickListener(v -> onControllerSettingsClicked());
|
||||
((ImageButton)view.findViewById(R.id.settings)).setOnClickListener(v -> onSettingsClicked());
|
||||
((ImageButton)view.findViewById(R.id.close)).setOnClickListener(v -> close(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull DialogInterface dialog) {
|
||||
onClosed(true);
|
||||
}
|
||||
|
||||
private void onClosed(boolean resumeGame) {
|
||||
if (settingsChanged)
|
||||
emulationActivity.applySettings();
|
||||
|
||||
if (resumeGame)
|
||||
emulationActivity.onMenuClosed();
|
||||
|
||||
emulationActivity.mPauseMenu = null;
|
||||
}
|
||||
|
||||
public void close(boolean resumeGame) {
|
||||
dismiss();
|
||||
onClosed(resumeGame);
|
||||
}
|
||||
|
||||
private void setContentFragment(Fragment fragment, boolean transition) {
|
||||
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
|
||||
if (transition)
|
||||
transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
transaction.replace(R.id.content, fragment).commit();
|
||||
}
|
||||
|
||||
private void onMenuClicked() {
|
||||
setContentFragment(new MenuSettingsFragment(this, emulationActivity), true);
|
||||
}
|
||||
|
||||
private void onControllerSettingsClicked() {
|
||||
ControllerSettingsCollectionFragment fragment = new ControllerSettingsCollectionFragment();
|
||||
setContentFragment(fragment, true);
|
||||
fragment.setMultitapModeChangedListener(this::onControllerSettingsClicked);
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
private void onSettingsClicked() {
|
||||
setContentFragment(new SettingsCollectionFragment(), true);
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MenuSettingsFragment extends PreferenceFragmentCompat {
|
||||
private MenuDialogFragment menuDialogFragment;
|
||||
private EmulationActivity emulationActivity;
|
||||
|
||||
public MenuSettingsFragment(MenuDialogFragment menuDialogFragment, EmulationActivity emulationActivity) {
|
||||
this.menuDialogFragment = menuDialogFragment;
|
||||
this.emulationActivity = emulationActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
|
||||
|
||||
final boolean cheevosActive = AndroidHostInterface.getInstance().isCheevosActive();
|
||||
final boolean cheevosChallengeModeEnabled = AndroidHostInterface.getInstance().isCheevosChallengeModeActive();
|
||||
|
||||
createPreference(R.string.emulation_menu_load_state, R.drawable.ic_baseline_folder_open_24, !cheevosChallengeModeEnabled, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showSaveStateMenu(false);
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_save_state, R.drawable.ic_baseline_save_24, true, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showSaveStateMenu(true);
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_toggle_fast_forward, R.drawable.ic_baseline_fast_forward_24, !cheevosChallengeModeEnabled, preference -> {
|
||||
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
|
||||
menuDialogFragment.close(true);
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_achievements, R.drawable.ic_baseline_trophy_24, cheevosActive, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showAchievementsPopup();
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_exit_game, R.drawable.ic_baseline_exit_to_app_24, true, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.mStopRequested = true;
|
||||
emulationActivity.finish();
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_patch_codes, R.drawable.ic_baseline_tips_and_updates_24, !cheevosChallengeModeEnabled, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showPatchesMenu();
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_change_disc, R.drawable.ic_baseline_album_24, true, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showDiscChangeMenu();
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_touchscreen_controller_settings, R.drawable.ic_baseline_touch_app_24, true, preference -> {
|
||||
menuDialogFragment.close(false);
|
||||
emulationActivity.showTouchscreenControllerMenu();
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_toggle_analog_mode, R.drawable.ic_baseline_gamepad_24, true, preference -> {
|
||||
AndroidHostInterface.getInstance().toggleControllerAnalogMode();
|
||||
menuDialogFragment.close(true);
|
||||
return true;
|
||||
});
|
||||
createPreference(R.string.emulation_menu_reset_console, R.drawable.ic_baseline_restart_alt_24, true, preference -> {
|
||||
AndroidHostInterface.getInstance().resetSystem();
|
||||
menuDialogFragment.close(true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void createPreference(int titleId, int icon, boolean enabled, Preference.OnPreferenceClickListener action) {
|
||||
final Preference preference = new Preference(getContext());
|
||||
preference.setTitle(titleId);
|
||||
preference.setIcon(icon);
|
||||
preference.setOnPreferenceClickListener(action);
|
||||
preference.setEnabled(enabled);
|
||||
getPreferenceScreen().addPreference(preference);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,302 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Vibrator;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmulationSurfaceView extends SurfaceView {
|
||||
public EmulationSurfaceView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EmulationSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EmulationSurfaceView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public static boolean isBindableDevice(InputDevice inputDevice) {
|
||||
if (inputDevice == null)
|
||||
return false;
|
||||
|
||||
// Accept all devices with an axis or buttons, filter in events.
|
||||
final int sources = inputDevice.getSources();
|
||||
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) ||
|
||||
((sources & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON);
|
||||
}
|
||||
|
||||
public static boolean isGamepadDevice(InputDevice inputDevice) {
|
||||
final int sources = (inputDevice != null) ? inputDevice.getSources() : 0;
|
||||
return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||
}
|
||||
|
||||
public static boolean isJoystickMotionEvent(MotionEvent event) {
|
||||
final int source = event.getSource();
|
||||
return ((source & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK);
|
||||
}
|
||||
|
||||
public static boolean isBindableKeyEvent(KeyEvent event) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_HOME:
|
||||
case KeyEvent.KEYCODE_POWER:
|
||||
// We're okay if we get these from a gamepad.
|
||||
return isGamepadDevice(event.getDevice());
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSystemKeyCode(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
case KeyEvent.KEYCODE_SOFT_RIGHT:
|
||||
case KeyEvent.KEYCODE_HOME:
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_CALL:
|
||||
case KeyEvent.KEYCODE_ENDCALL:
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
case KeyEvent.KEYCODE_VOLUME_MUTE:
|
||||
case KeyEvent.KEYCODE_MUTE:
|
||||
case KeyEvent.KEYCODE_POWER:
|
||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||
case KeyEvent.KEYCODE_MEDIA_PLAY:
|
||||
case KeyEvent.KEYCODE_MEDIA_PAUSE:
|
||||
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
||||
case KeyEvent.KEYCODE_MEDIA_STOP:
|
||||
case KeyEvent.KEYCODE_MEDIA_NEXT:
|
||||
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
|
||||
case KeyEvent.KEYCODE_MEDIA_REWIND:
|
||||
case KeyEvent.KEYCODE_MEDIA_RECORD:
|
||||
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
|
||||
case KeyEvent.KEYCODE_CAMERA:
|
||||
case KeyEvent.KEYCODE_FOCUS:
|
||||
case KeyEvent.KEYCODE_SEARCH:
|
||||
case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
|
||||
case KeyEvent.KEYCODE_BRIGHTNESS_UP:
|
||||
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
|
||||
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP:
|
||||
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
|
||||
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
|
||||
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class InputDeviceData {
|
||||
private int deviceId;
|
||||
private String descriptor;
|
||||
private int[] axes;
|
||||
private float[] axisValues;
|
||||
private int controllerIndex;
|
||||
private Vibrator vibrator;
|
||||
|
||||
public InputDeviceData(InputDevice device, int controllerIndex) {
|
||||
deviceId = device.getId();
|
||||
descriptor = device.getDescriptor();
|
||||
this.controllerIndex = controllerIndex;
|
||||
|
||||
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
||||
if (motionRanges != null && !motionRanges.isEmpty()) {
|
||||
axes = new int[motionRanges.size()];
|
||||
axisValues = new float[motionRanges.size()];
|
||||
for (int i = 0; i < motionRanges.size(); i++)
|
||||
axes[i] = motionRanges.get(i).getAxis();
|
||||
}
|
||||
|
||||
// device.getVibrator() always returns null, but might return a "null vibrator".
|
||||
final Vibrator potentialVibrator = device.getVibrator();
|
||||
if (potentialVibrator != null && potentialVibrator.hasVibrator())
|
||||
vibrator = potentialVibrator;
|
||||
}
|
||||
}
|
||||
|
||||
private InputDeviceData[] mInputDevices = null;
|
||||
private String[] mControllerDescriptors = null;
|
||||
private boolean mHasAnyGamepads = false;
|
||||
|
||||
public boolean hasAnyGamePads() {
|
||||
return mHasAnyGamepads;
|
||||
}
|
||||
|
||||
public synchronized void updateInputDevices() {
|
||||
mInputDevices = null;
|
||||
mControllerDescriptors = null;
|
||||
mHasAnyGamepads = false;
|
||||
|
||||
final ArrayList<InputDeviceData> inputDeviceIds = new ArrayList<>();
|
||||
final ArrayList<String> controllerDescriptors = new ArrayList<>();
|
||||
|
||||
for (int deviceId : InputDevice.getDeviceIds()) {
|
||||
final InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device == null || !isBindableDevice(device)) {
|
||||
Log.d("EmulationSurfaceView",
|
||||
String.format("Skipping device %s sources %d",
|
||||
(device != null) ? device.toString() : "",
|
||||
(device != null) ? device.getSources() : 0));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isGamepadDevice(device))
|
||||
mHasAnyGamepads = true;
|
||||
|
||||
// Some phones seem to have duplicate descriptors for multiple devices.
|
||||
// Combine them all into one controller index if so.
|
||||
final String descriptor = device.getDescriptor();
|
||||
int controllerIndex = controllerDescriptors.size();
|
||||
for (int i = 0; i < controllerDescriptors.size(); i++) {
|
||||
if (controllerDescriptors.get(i).equals(descriptor)) {
|
||||
controllerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (controllerIndex == controllerDescriptors.size()) {
|
||||
controllerDescriptors.add(descriptor);
|
||||
}
|
||||
|
||||
Log.d("EmulationSurfaceView", String.format("Tracking device %d/%s (%s, sources %d, controller %d)",
|
||||
controllerIndex, descriptor, device.getName(), device.getSources(), controllerIndex));
|
||||
inputDeviceIds.add(new InputDeviceData(device, controllerIndex));
|
||||
}
|
||||
|
||||
if (inputDeviceIds.isEmpty())
|
||||
return;
|
||||
|
||||
mInputDevices = new InputDeviceData[inputDeviceIds.size()];
|
||||
inputDeviceIds.toArray(mInputDevices);
|
||||
|
||||
mControllerDescriptors = new String[controllerDescriptors.size()];
|
||||
controllerDescriptors.toArray(mControllerDescriptors);
|
||||
}
|
||||
|
||||
public synchronized String[] getInputDeviceNames() {
|
||||
return mControllerDescriptors;
|
||||
}
|
||||
|
||||
public synchronized boolean hasInputDeviceVibration(int controllerIndex) {
|
||||
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||
return false;
|
||||
|
||||
return (mInputDevices[controllerIndex].vibrator != null);
|
||||
}
|
||||
|
||||
public synchronized void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||
return;
|
||||
|
||||
// shouldn't get here
|
||||
final InputDeviceData data = mInputDevices[controllerIndex];
|
||||
if (data.vibrator == null)
|
||||
return;
|
||||
|
||||
final float MINIMUM_INTENSITY = 0.1f;
|
||||
if (smallMotor >= MINIMUM_INTENSITY || largeMotor >= MINIMUM_INTENSITY)
|
||||
data.vibrator.vibrate(1000);
|
||||
else
|
||||
data.vibrator.cancel();
|
||||
}
|
||||
|
||||
public InputDeviceData getDataForDeviceId(int deviceId) {
|
||||
if (mInputDevices == null)
|
||||
return null;
|
||||
|
||||
for (InputDeviceData data : mInputDevices) {
|
||||
if (data.deviceId == deviceId)
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getControllerIndexForDeviceId(int deviceId) {
|
||||
final InputDeviceData data = getDataForDeviceId(deviceId);
|
||||
return (data != null) ? data.controllerIndex : -1;
|
||||
}
|
||||
|
||||
private boolean handleKeyEvent(int deviceId, int repeatCount, int keyCode, boolean pressed) {
|
||||
final int controllerIndex = getControllerIndexForDeviceId(deviceId);
|
||||
Log.d("EmulationSurfaceView", String.format("Controller %d Code %d RC %d Pressed %d",
|
||||
controllerIndex, keyCode, repeatCount, pressed? 1 : 0));
|
||||
|
||||
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||
if (repeatCount == 0 && controllerIndex >= 0)
|
||||
hi.handleControllerButtonEvent(controllerIndex, keyCode, pressed);
|
||||
|
||||
// We don't want to eat external button events unless it's actually bound.
|
||||
if (isSystemKeyCode(keyCode))
|
||||
return (controllerIndex >= 0 && hi.hasControllerButtonBinding(controllerIndex, keyCode));
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return handleKeyEvent(event.getDeviceId(), event.getRepeatCount(), keyCode, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
return handleKeyEvent(event.getDeviceId(), 0, keyCode, false);
|
||||
}
|
||||
|
||||
private float clamp(float value, float min, float max) {
|
||||
return (value < min) ? min : ((value > max) ? max : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if (!isJoystickMotionEvent(event))
|
||||
return false;
|
||||
|
||||
final InputDeviceData data = getDataForDeviceId(event.getDeviceId());
|
||||
if (data == null || data.axes == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < data.axes.length; i++) {
|
||||
final int axis = data.axes[i];
|
||||
final float axisValue = event.getAxisValue(axis);
|
||||
float emuValue;
|
||||
|
||||
switch (axis) {
|
||||
case MotionEvent.AXIS_BRAKE:
|
||||
case MotionEvent.AXIS_GAS:
|
||||
case MotionEvent.AXIS_LTRIGGER:
|
||||
case MotionEvent.AXIS_RTRIGGER:
|
||||
// Scale 0..1 -> -1..1.
|
||||
emuValue = (clamp(axisValue, 0.0f, 1.0f) * 2.0f) - 1.0f;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Everything else should already by -1..1 as per Android documentation.
|
||||
emuValue = clamp(axisValue, -1.0f, 1.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
if (data.axisValues[i] == emuValue)
|
||||
continue;
|
||||
|
||||
Log.d("EmulationSurfaceView",
|
||||
String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));
|
||||
|
||||
data.axisValues[i] = emuValue;
|
||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class EmulationThread extends Thread {
|
||||
private EmulationActivity emulationActivity;
|
||||
private String filename;
|
||||
private boolean resumeState;
|
||||
private String stateFilename;
|
||||
|
||||
private EmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) {
|
||||
super("EmulationThread");
|
||||
this.emulationActivity = emulationActivity;
|
||||
this.filename = filename;
|
||||
this.resumeState = resumeState;
|
||||
this.stateFilename = stateFilename;
|
||||
}
|
||||
|
||||
public static EmulationThread create(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) {
|
||||
Log.i("EmulationThread", String.format("Starting emulation thread (%s)...", filename));
|
||||
|
||||
EmulationThread thread = new EmulationThread(emulationActivity, filename, resumeState, stateFilename);
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private void setExclusiveCores() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
int[] cores = Process.getExclusiveCores();
|
||||
if (cores == null || cores.length == 0)
|
||||
throw new Exception("Invalid return value from getExclusiveCores()");
|
||||
|
||||
AndroidHostInterface.setThreadAffinity(cores);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("EmulationThread", "getExclusiveCores() failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
|
||||
setExclusiveCores();
|
||||
} catch (Exception e) {
|
||||
Log.i("EmulationThread", "Failed to set priority for emulation thread: " + e.getMessage());
|
||||
}
|
||||
|
||||
AndroidHostInterface.getInstance().runEmulationThread(emulationActivity, filename, resumeState, stateFilename);
|
||||
Log.i("EmulationThread", "Emulation thread exiting.");
|
||||
}
|
||||
|
||||
public void stopAndJoin() {
|
||||
AndroidHostInterface.getInstance().stopEmulationThreadLoop();
|
||||
try {
|
||||
join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.i("EmulationThread", "join() interrupted: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,613 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* File helper class - used to bridge native code to Java storage access framework APIs.
|
||||
*/
|
||||
public class FileHelper {
|
||||
/**
|
||||
* Native filesystem flags.
|
||||
*/
|
||||
public static final int FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY = 1;
|
||||
public static final int FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY = 2;
|
||||
public static final int FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED = 4;
|
||||
|
||||
/**
|
||||
* Native filesystem find result flags.
|
||||
*/
|
||||
public static final int FILESYSTEM_FIND_RECURSIVE = (1 << 0);
|
||||
public static final int FILESYSTEM_FIND_RELATIVE_PATHS = (1 << 1);
|
||||
public static final int FILESYSTEM_FIND_HIDDEN_FILES = (1 << 2);
|
||||
public static final int FILESYSTEM_FIND_FOLDERS = (1 << 3);
|
||||
public static final int FILESYSTEM_FIND_FILES = (1 << 4);
|
||||
public static final int FILESYSTEM_FIND_KEEP_ARRAY = (1 << 5);
|
||||
|
||||
/**
|
||||
* Projection used when searching for files.
|
||||
*/
|
||||
private static final String[] findProjection = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
};
|
||||
|
||||
/**
|
||||
* Projection used when getting the display name for a file.
|
||||
*/
|
||||
private static final String[] getDisplayNameProjection = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
};
|
||||
|
||||
/**
|
||||
* Projection used when getting a relative file for a file.
|
||||
*/
|
||||
private static final String[] getRelativeFileProjection = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
/**
|
||||
* File helper class - used to bridge native code to Java storage access framework APIs.
|
||||
*
|
||||
* @param context Context in which to perform file actions as.
|
||||
*/
|
||||
public FileHelper(Context context) {
|
||||
this.context = context;
|
||||
this.contentResolver = context.getContentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the specified file as a string, under the specified context.
|
||||
*
|
||||
* @param context context to access file under
|
||||
* @param uri uri to write data to
|
||||
* @param maxSize maximum file size to read
|
||||
* @return String containing the file data, otherwise null
|
||||
*/
|
||||
public static String readStringFromUri(final Context context, final Uri uri, int maxSize) {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder os = new StringBuilder();
|
||||
try {
|
||||
char[] buffer = new char[1024];
|
||||
InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name()));
|
||||
int len;
|
||||
while ((len = reader.read(buffer)) > 0) {
|
||||
os.append(buffer, 0, len);
|
||||
if (os.length() > maxSize)
|
||||
return null;
|
||||
}
|
||||
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (os.length() == 0)
|
||||
return null;
|
||||
|
||||
return os.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the specified file as a byte array, under the specified context.
|
||||
*
|
||||
* @param context context to access file under
|
||||
* @param uri uri to write data to
|
||||
* @param maxSize maximum file size to read
|
||||
* @return byte array containing the file data, otherwise null
|
||||
*/
|
||||
public static byte[] readBytesFromUri(final Context context, final Uri uri, int maxSize) {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
byte[] buffer = new byte[512 * 1024];
|
||||
int len;
|
||||
while ((len = stream.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, len);
|
||||
if (maxSize > 0 && os.size() > maxSize) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (os.size() == 0)
|
||||
return null;
|
||||
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified data to a file referenced by the URI, as the specified context.
|
||||
*
|
||||
* @param context context to access file under
|
||||
* @param uri uri to write data to
|
||||
* @param bytes data to write file to
|
||||
* @return true if write was succesful, otherwise false
|
||||
*/
|
||||
public static boolean writeBytesToUri(final Context context, final Uri uri, final byte[] bytes) {
|
||||
OutputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openOutputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
try {
|
||||
stream.write(bytes);
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file referenced by the URI, under the specified context.
|
||||
*
|
||||
* @param context context to delete file under
|
||||
* @param uri uri to delete
|
||||
* @return
|
||||
*/
|
||||
public static boolean deleteFileAtUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
if (uri.getScheme() == "file") {
|
||||
final File file = new File(uri.getPath());
|
||||
if (!file.isFile())
|
||||
return false;
|
||||
|
||||
return file.delete();
|
||||
}
|
||||
return (context.getContentResolver().delete(uri, null, null) > 0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the file pointed at by a SAF URI.
|
||||
*
|
||||
* @param context context to access file under
|
||||
* @param uri uri to retrieve file name for
|
||||
* @return the name of the file, or null
|
||||
*/
|
||||
public static String getDocumentNameFromUri(final Context context, final Uri uri) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
final String[] proj = {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
|
||||
cursor = context.getContentResolver().query(uri, proj, null, null, null);
|
||||
final int columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME);
|
||||
cursor.moveToFirst();
|
||||
return cursor.getString(columnIndex);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a bitmap from the provided SAF URI.
|
||||
*
|
||||
* @param context context to access file under
|
||||
* @param uri uri to retrieve file name for
|
||||
* @return a decoded bitmap for the file, or null
|
||||
*/
|
||||
public static Bitmap loadBitmapFromUri(final Context context, final Uri uri) {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
final ImageDecoder.Source source = ImageDecoder.createSource(context.getContentResolver(), uri);
|
||||
return ImageDecoder.decodeBitmap(source);
|
||||
} else {
|
||||
return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name component of a path or URI.
|
||||
*
|
||||
* @param path Path/URI to examine.
|
||||
* @return File name component of path/URI.
|
||||
*/
|
||||
public static String getFileNameForPath(String path) {
|
||||
if (path.startsWith("content:/") || path.startsWith("file:/")) {
|
||||
try {
|
||||
final Uri uri = Uri.parse(path);
|
||||
final String lastPathSegment = uri.getLastPathSegment();
|
||||
if (lastPathSegment != null)
|
||||
path = lastPathSegment;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash > 0 && lastSlash < path.length() - 1)
|
||||
return path.substring(lastSlash + 1);
|
||||
else
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given URI represents a {@link DocumentsContract.Document} tree.
|
||||
*/
|
||||
public static boolean isTreeUri(Uri uri) {
|
||||
final List<String> paths = uri.getPathSegments();
|
||||
return (paths.size() >= 2 && paths.get(0).equals("tree"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file descriptor for a content URI string. Called by native code.
|
||||
*
|
||||
* @param uriString string of the URI to open
|
||||
* @param mode Java open mode
|
||||
* @return file descriptor for URI, or -1
|
||||
*/
|
||||
public int openURIAsFileDescriptor(String uriString, String mode) {
|
||||
try {
|
||||
final Uri uri = Uri.parse(uriString);
|
||||
final ParcelFileDescriptor fd = contentResolver.openFileDescriptor(uri, mode);
|
||||
if (fd == null)
|
||||
return -1;
|
||||
return fd.detachFd();
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterates documents in the specified tree, searching for files.
|
||||
*
|
||||
* @param treeUri Root tree in which to search for documents.
|
||||
* @param documentId Document ID representing the directory to start searching.
|
||||
* @param flags Native search flags.
|
||||
* @param results Cumulative result array.
|
||||
*/
|
||||
private void doFindFiles(Uri treeUri, String documentId, int flags, ArrayList<FindResult> results) {
|
||||
try {
|
||||
final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId);
|
||||
final Cursor cursor = contentResolver.query(queryUri, findProjection, null, null, null);
|
||||
final int count = cursor.getCount();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final String mimeType = cursor.getString(2);
|
||||
final String childDocumentId = cursor.getString(0);
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId);
|
||||
final long size = cursor.getLong(3);
|
||||
final long lastModified = cursor.getLong(4);
|
||||
|
||||
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
if ((flags & FILESYSTEM_FIND_FOLDERS) != 0) {
|
||||
results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY));
|
||||
}
|
||||
|
||||
if ((flags & FILESYSTEM_FIND_RECURSIVE) != 0)
|
||||
doFindFiles(treeUri, childDocumentId, flags, results);
|
||||
} else {
|
||||
if ((flags & FILESYSTEM_FIND_FILES) != 0) {
|
||||
results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, 0));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterates documents in the specified URI, searching for files.
|
||||
*
|
||||
* @param uriString URI containing directory to search.
|
||||
* @param flags Native filter flags.
|
||||
* @return Array of find results.
|
||||
*/
|
||||
public FindResult[] findFiles(String uriString, int flags) {
|
||||
try {
|
||||
final Uri fullUri = Uri.parse(uriString);
|
||||
final String documentId = DocumentsContract.getTreeDocumentId(fullUri);
|
||||
final ArrayList<FindResult> results = new ArrayList<>();
|
||||
doFindFiles(fullUri, documentId, flags, results);
|
||||
if (results.isEmpty())
|
||||
return null;
|
||||
|
||||
final FindResult[] resultsArray = new FindResult[results.size()];
|
||||
results.toArray(resultsArray);
|
||||
return resultsArray;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name for the given URI.
|
||||
*
|
||||
* @param uriString URI to resolve display name for.
|
||||
* @return display name for the URI, or null.
|
||||
*/
|
||||
public String getDisplayNameForURIPath(String uriString) {
|
||||
try {
|
||||
final Uri fullUri = Uri.parse(uriString);
|
||||
final Cursor cursor = contentResolver.query(fullUri, getDisplayNameProjection,
|
||||
null, null, null);
|
||||
if (cursor.getCount() == 0 || !cursor.moveToNext())
|
||||
return null;
|
||||
|
||||
return cursor.getString(0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path for a sibling file relative to another URI.
|
||||
*
|
||||
* @param uriString URI to find the file relative to.
|
||||
* @param newFileName Sibling file name.
|
||||
* @return URI for the sibling file name, or null.
|
||||
*/
|
||||
public String getRelativePathForURIPath(String uriString, String newFileName) {
|
||||
try {
|
||||
final Uri fullUri = Uri.parse(uriString);
|
||||
|
||||
// if this is a document (expected)...
|
||||
Uri treeUri;
|
||||
String treeDocId;
|
||||
if (DocumentsContract.isDocumentUri(context, fullUri)) {
|
||||
// we need to remove the last part of the URI (the specific document ID) to get the parent
|
||||
final String lastPathSegment = fullUri.getLastPathSegment();
|
||||
int lastSeparatorIndex = lastPathSegment.lastIndexOf('/');
|
||||
if (lastSeparatorIndex < 0)
|
||||
lastSeparatorIndex = lastPathSegment.lastIndexOf(':');
|
||||
if (lastSeparatorIndex < 0)
|
||||
return null;
|
||||
|
||||
// the parent becomes the document ID
|
||||
treeDocId = lastPathSegment.substring(0, lastSeparatorIndex);
|
||||
|
||||
// but, we need to access it through the subtree if this was a tree URI (permissions...)
|
||||
if (isTreeUri(fullUri)) {
|
||||
treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), DocumentsContract.getTreeDocumentId(fullUri));
|
||||
} else {
|
||||
treeUri = DocumentsContract.buildTreeDocumentUri(fullUri.getAuthority(), treeDocId);
|
||||
}
|
||||
} else {
|
||||
treeDocId = DocumentsContract.getDocumentId(fullUri);
|
||||
treeUri = fullUri;
|
||||
}
|
||||
|
||||
final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, treeDocId);
|
||||
final Cursor cursor = contentResolver.query(queryUri, getRelativeFileProjection, null, null, null);
|
||||
final int count = cursor.getCount();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final String displayName = cursor.getString(1);
|
||||
if (!displayName.equalsIgnoreCase(newFileName))
|
||||
continue;
|
||||
|
||||
final String childDocumentId = cursor.getString(0);
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId);
|
||||
cursor.close();
|
||||
return uri.toString();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PRIMARY_VOLUME_NAME = "primary";
|
||||
|
||||
@Nullable
|
||||
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
|
||||
if (treeUri == null) return null;
|
||||
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
|
||||
if (volumePath == null) return File.separator;
|
||||
if (volumePath.endsWith(File.separator))
|
||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
||||
|
||||
String documentPath = getDocumentPathFromTreeUri(treeUri);
|
||||
if (documentPath.endsWith(File.separator))
|
||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
||||
|
||||
if (documentPath.length() > 0) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
return volumePath + documentPath;
|
||||
else
|
||||
return volumePath + File.separator + documentPath;
|
||||
} else return volumePath;
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private static String getVolumePath(final String volumeId, Context context) {
|
||||
if (volumeId == null)
|
||||
return null;
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
|
||||
try {
|
||||
StorageManager mStorageManager =
|
||||
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
|
||||
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
|
||||
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
|
||||
Method getUuid = storageVolumeClazz.getMethod("getUuid");
|
||||
Method getPath = storageVolumeClazz.getMethod("getPath");
|
||||
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
|
||||
Object result = getVolumeList.invoke(mStorageManager);
|
||||
|
||||
final int length = Array.getLength(result);
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object storageVolumeElement = Array.get(result, i);
|
||||
String uuid = (String) getUuid.invoke(storageVolumeElement);
|
||||
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
|
||||
|
||||
// primary volume?
|
||||
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
|
||||
// other volumes?
|
||||
if (uuid != null && uuid.equals(volumeId))
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
}
|
||||
// not found.
|
||||
return null;
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
|
||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if (split.length > 0) return split[0];
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
|
||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
||||
else return File.separator;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getFullPathFromUri(@Nullable final Uri treeUri, Context con) {
|
||||
if (treeUri == null) return null;
|
||||
String volumePath = getVolumePath(getVolumeIdFromUri(treeUri), con);
|
||||
if (volumePath == null) return File.separator;
|
||||
if (volumePath.endsWith(File.separator))
|
||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
||||
|
||||
String documentPath = getDocumentPathFromUri(treeUri);
|
||||
if (documentPath.endsWith(File.separator))
|
||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
||||
|
||||
if (documentPath.length() > 0) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
return volumePath + documentPath;
|
||||
else
|
||||
return volumePath + File.separator + documentPath;
|
||||
} else return volumePath;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getVolumeIdFromUri(final Uri treeUri) {
|
||||
try {
|
||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if (split.length > 0) return split[0];
|
||||
else return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getDocumentPathFromUri(final Uri treeUri) {
|
||||
try {
|
||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
||||
else return File.separator;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Java class containing the data for a file in a find operation.
|
||||
*/
|
||||
public static class FindResult {
|
||||
public String name;
|
||||
public String relativeName;
|
||||
public long size;
|
||||
public long modifiedTime;
|
||||
public int flags;
|
||||
|
||||
public FindResult(String relativeName, String name, long size, long modifiedTime, int flags) {
|
||||
this.relativeName = relativeName;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.modifiedTime = modifiedTime;
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,348 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.FileUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Property;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class GameDirectoriesActivity extends AppCompatActivity {
|
||||
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 1;
|
||||
private static final String FORCE_SAF_CONFIG_KEY = "GameList/ForceStorageAccessFramework";
|
||||
|
||||
private class DirectoryListAdapter extends RecyclerView.Adapter {
|
||||
private class Entry {
|
||||
private String mPath;
|
||||
private boolean mRecursive;
|
||||
|
||||
public Entry(String path, boolean recursive) {
|
||||
mPath = path;
|
||||
mRecursive = recursive;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public boolean isRecursive() {
|
||||
return mRecursive;
|
||||
}
|
||||
|
||||
public void toggleRecursive() {
|
||||
mRecursive = !mRecursive;
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryComparator implements Comparator<Entry> {
|
||||
@Override
|
||||
public int compare(Entry left, Entry right) {
|
||||
return left.getPath().compareTo(right.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private Entry[] mEntries;
|
||||
|
||||
public DirectoryListAdapter(Context context) {
|
||||
mContext = context;
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
|
||||
ArrayList<Entry> entries = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Set<String> paths = prefs.getStringSet("GameList/Paths", null);
|
||||
if (paths != null) {
|
||||
for (String path : paths)
|
||||
entries.add(new Entry(path, false));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
try {
|
||||
Set<String> paths = prefs.getStringSet("GameList/RecursivePaths", null);
|
||||
if (paths != null) {
|
||||
for (String path : paths)
|
||||
entries.add(new Entry(path, true));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
mEntries = new Entry[entries.size()];
|
||||
entries.toArray(mEntries);
|
||||
Arrays.sort(mEntries, new EntryComparator());
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
private int mPosition;
|
||||
private Entry mEntry;
|
||||
private TextView mPathView;
|
||||
private TextView mRecursiveView;
|
||||
private ImageButton mToggleRecursiveView;
|
||||
private ImageButton mRemoveView;
|
||||
|
||||
public ViewHolder(View rootView) {
|
||||
super(rootView);
|
||||
mPathView = rootView.findViewById(R.id.path);
|
||||
mRecursiveView = rootView.findViewById(R.id.recursive);
|
||||
mToggleRecursiveView = rootView.findViewById(R.id.toggle_recursive);
|
||||
mToggleRecursiveView.setOnClickListener(this);
|
||||
mRemoveView = rootView.findViewById(R.id.remove);
|
||||
mRemoveView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
public void bindData(int position, Entry entry) {
|
||||
mPosition = position;
|
||||
mEntry = entry;
|
||||
updateText();
|
||||
}
|
||||
|
||||
private void updateText() {
|
||||
mPathView.setText(mEntry.getPath());
|
||||
mRecursiveView.setText(getString(mEntry.isRecursive() ? R.string.game_directories_scanning_subdirectories : R.string.game_directories_not_scanning_subdirectories));
|
||||
mToggleRecursiveView.setImageDrawable(getDrawable(mEntry.isRecursive() ? R.drawable.ic_baseline_folder_24 : R.drawable.ic_baseline_folder_open_24));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mToggleRecursiveView == v) {
|
||||
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
mEntry.toggleRecursive();
|
||||
addSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
updateText();
|
||||
} else if (mRemoveView == v) {
|
||||
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
((ViewHolder) holder).bindData(position, mEntries[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_game_directory_entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mEntries[position].getPath().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mEntries.length;
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryListAdapter mDirectoryListAdapter;
|
||||
RecyclerView mRecyclerView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_game_directories);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
mDirectoryListAdapter = new DirectoryListAdapter(this);
|
||||
mRecyclerView = findViewById(R.id.recycler_view);
|
||||
mRecyclerView.setAdapter(mDirectoryListAdapter);
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
|
||||
DividerItemDecoration.VERTICAL));
|
||||
|
||||
findViewById(R.id.fab).setOnClickListener((v) -> startAddGameDirectory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_edit_game_directories, menu);
|
||||
|
||||
menu.findItem(R.id.force_saf)
|
||||
.setEnabled(android.os.Build.VERSION.SDK_INT < 30)
|
||||
.setChecked(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
|
||||
FORCE_SAF_CONFIG_KEY, false))
|
||||
.setOnMenuItemClickListener(item -> {
|
||||
final SharedPreferences sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final boolean newValue =!sharedPreferences.getBoolean(
|
||||
FORCE_SAF_CONFIG_KEY, false);
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(FORCE_SAF_CONFIG_KEY, newValue)
|
||||
.commit();
|
||||
item.setChecked(newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.add_directory) {
|
||||
startAddGameDirectory();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.add_path) {
|
||||
startAddPath();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public static boolean useStorageAccessFramework(Context context) {
|
||||
// Use legacy storage on devices older than Android 11... apparently some of them
|
||||
// have broken storage access framework....
|
||||
if (android.os.Build.VERSION.SDK_INT >= 30)
|
||||
return true;
|
||||
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
||||
"GameList/ForceStorageAccessFramework", false);
|
||||
}
|
||||
|
||||
public static void addSearchDirectory(Context context, String path, boolean recursive) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
||||
PreferenceHelpers.addToStringList(prefs, key, path);
|
||||
Log.i("GameDirectoriesActivity", "Added path '" + path + "' to game list search directories");
|
||||
}
|
||||
|
||||
public static void removeSearchDirectory(Context context, String path, boolean recursive) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
||||
PreferenceHelpers.removeFromStringList(prefs, key, path);
|
||||
Log.i("GameDirectoriesActivity", "Removed path '" + path + "' from game list search directories");
|
||||
}
|
||||
|
||||
private void startAddGameDirectory() {
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||
}
|
||||
|
||||
private void startAddPath() {
|
||||
final EditText text = new EditText(this);
|
||||
text.setSingleLine();
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.edit_game_directories_add_path)
|
||||
.setMessage(R.string.edit_game_directories_add_path_summary)
|
||||
.setView(text)
|
||||
.setPositiveButton("Add", (dialog, which) -> {
|
||||
final String path = text.getText().toString();
|
||||
if (!path.isEmpty()) {
|
||||
addSearchDirectory(GameDirectoriesActivity.this, path, true);
|
||||
mDirectoryListAdapter.reload();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||
if (resultCode != RESULT_OK || data.getData() == null)
|
||||
return;
|
||||
|
||||
// Use legacy storage on devices older than Android 11... apparently some of them
|
||||
// have broken storage access framework....
|
||||
if (!useStorageAccessFramework(this)) {
|
||||
final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this);
|
||||
if (path != null) {
|
||||
addSearchDirectory(this, path, true);
|
||||
mDirectoryListAdapter.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
getContentResolver().takePersistableUriPermission(data.getData(),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
addSearchDirectory(this, data.getDataString(), true);
|
||||
mDirectoryListAdapter.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GameGridFragment extends Fragment implements GameList.OnRefreshListener {
|
||||
private static final int SPACING_DIPS = 25;
|
||||
private static final int WIDTH_DIPS = 160;
|
||||
|
||||
private final MainActivity mParent;
|
||||
private RecyclerView mRecyclerView;
|
||||
private ViewAdapter mAdapter;
|
||||
private GridAutofitLayoutManager mLayoutManager;
|
||||
|
||||
public GameGridFragment(MainActivity parent) {
|
||||
super(R.layout.fragment_game_grid);
|
||||
this.mParent = parent;
|
||||
}
|
||||
|
||||
private GameList getGameList() {
|
||||
return mParent.getGameList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
mAdapter = new ViewAdapter(mParent, getGameList());
|
||||
getGameList().addRefreshListener(this);
|
||||
|
||||
mRecyclerView = view.findViewById(R.id.game_list_view);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
|
||||
final int columnWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
WIDTH_DIPS + SPACING_DIPS, getResources().getDisplayMetrics()));
|
||||
mLayoutManager = new GridAutofitLayoutManager(getContext(), columnWidth);
|
||||
mRecyclerView.setLayoutManager(mLayoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
getGameList().removeRefreshListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGameListRefresh() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
private final MainActivity mParent;
|
||||
private final ImageView mImageView;
|
||||
private GameListEntry mEntry;
|
||||
|
||||
public ViewHolder(@NonNull MainActivity parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
mParent = parent;
|
||||
mImageView = itemView.findViewById(R.id.imageView);
|
||||
mImageView.setOnClickListener(this);
|
||||
mImageView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
public void bindToEntry(GameListEntry entry) {
|
||||
mEntry = entry;
|
||||
|
||||
// while it loads/generates
|
||||
mImageView.setImageDrawable(ContextCompat.getDrawable(mParent, R.drawable.ic_media_cdrom));
|
||||
|
||||
final String coverPath = entry.getCoverPath();
|
||||
if (coverPath == null) {
|
||||
new GenerateCoverTask(mParent, mImageView, mEntry.getTitle()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
return;
|
||||
}
|
||||
|
||||
new ImageLoadTask(mImageView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coverPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mParent.startEmulation(mEntry.getPath(), mParent.shouldResumeStateByDefault());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
mParent.openGamePopupMenu(v, mEntry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
private final MainActivity mParent;
|
||||
private final LayoutInflater mInflater;
|
||||
private final GameList mGameList;
|
||||
|
||||
public ViewAdapter(@NonNull MainActivity parent, @NonNull GameList gameList) {
|
||||
mParent = parent;
|
||||
mInflater = LayoutInflater.from(parent);
|
||||
mGameList = gameList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(mParent, mInflater.inflate(R.layout.layout_game_grid_entry, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
GameListEntry entry = mGameList.getEntry(position);
|
||||
holder.bindToEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mGameList.getEntryCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_game_grid_entry;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class GameList {
|
||||
public interface OnRefreshListener {
|
||||
void onGameListRefresh();
|
||||
}
|
||||
|
||||
private Activity mContext;
|
||||
private GameListEntry[] mEntries;
|
||||
private ArrayList<OnRefreshListener> mRefreshListeners = new ArrayList<>();
|
||||
|
||||
public GameList(Activity context) {
|
||||
mContext = context;
|
||||
mEntries = new GameListEntry[0];
|
||||
}
|
||||
|
||||
public void addRefreshListener(OnRefreshListener listener) {
|
||||
mRefreshListeners.add(listener);
|
||||
}
|
||||
public void removeRefreshListener(OnRefreshListener listener) {
|
||||
mRefreshListeners.remove(listener);
|
||||
}
|
||||
public void fireRefreshListeners() {
|
||||
for (OnRefreshListener listener : mRefreshListeners)
|
||||
listener.onGameListRefresh();
|
||||
}
|
||||
|
||||
private class GameListEntryComparator implements Comparator<GameListEntry> {
|
||||
@Override
|
||||
public int compare(GameListEntry left, GameListEntry right) {
|
||||
return left.getTitle().compareTo(right.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh(boolean invalidateCache, boolean invalidateDatabase, Activity parentActivity) {
|
||||
// Search and get entries from native code
|
||||
AndroidProgressCallback progressCallback = new AndroidProgressCallback(mContext);
|
||||
AsyncTask.execute(() -> {
|
||||
AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase, progressCallback);
|
||||
GameListEntry[] newEntries = AndroidHostInterface.getInstance().getGameListEntries();
|
||||
Arrays.sort(newEntries, new GameListEntryComparator());
|
||||
|
||||
mContext.runOnUiThread(() -> {
|
||||
try {
|
||||
progressCallback.dismiss();
|
||||
} catch (Exception e) {
|
||||
Log.e("GameList", "Exception dismissing refresh progress");
|
||||
e.printStackTrace();
|
||||
}
|
||||
mEntries = newEntries;
|
||||
fireRefreshListeners();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public int getEntryCount() {
|
||||
return mEntries.length;
|
||||
}
|
||||
|
||||
public GameListEntry getEntry(int index) {
|
||||
return mEntries[index];
|
||||
}
|
||||
|
||||
public GameListEntry getEntryForPath(String path) {
|
||||
for (final GameListEntry entry : mEntries) {
|
||||
if (entry.getPath().equals(path))
|
||||
return entry;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class GameListEntry {
|
||||
public enum EntryType {
|
||||
Disc,
|
||||
PSExe,
|
||||
Playlist,
|
||||
PSF
|
||||
}
|
||||
|
||||
public enum CompatibilityRating {
|
||||
Unknown,
|
||||
DoesntBoot,
|
||||
CrashesInIntro,
|
||||
CrashesInGame,
|
||||
GraphicalAudioIssues,
|
||||
NoIssues,
|
||||
}
|
||||
|
||||
private String mPath;
|
||||
private String mCode;
|
||||
private String mTitle;
|
||||
private long mSize;
|
||||
private String mModifiedTime;
|
||||
private DiscRegion mRegion;
|
||||
private EntryType mType;
|
||||
private CompatibilityRating mCompatibilityRating;
|
||||
private String mCoverPath;
|
||||
|
||||
public GameListEntry(String path, String code, String title, long size, String modifiedTime, String region,
|
||||
String type, String compatibilityRating, String coverPath) {
|
||||
mPath = path;
|
||||
mCode = code;
|
||||
mTitle = title;
|
||||
mSize = size;
|
||||
mModifiedTime = modifiedTime;
|
||||
mCoverPath = coverPath;
|
||||
|
||||
try {
|
||||
mRegion = DiscRegion.valueOf(region);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mRegion = DiscRegion.NTSC_U;
|
||||
}
|
||||
|
||||
try {
|
||||
mType = EntryType.valueOf(type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mType = EntryType.Disc;
|
||||
}
|
||||
|
||||
try {
|
||||
mCompatibilityRating = CompatibilityRating.valueOf(compatibilityRating);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mCompatibilityRating = CompatibilityRating.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return mCode;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public long getSize() { return mSize; }
|
||||
|
||||
public String getModifiedTime() {
|
||||
return mModifiedTime;
|
||||
}
|
||||
|
||||
public DiscRegion getRegion() {
|
||||
return mRegion;
|
||||
}
|
||||
|
||||
public EntryType getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public CompatibilityRating getCompatibilityRating() {
|
||||
return mCompatibilityRating;
|
||||
}
|
||||
|
||||
public String getCoverPath() { return mCoverPath; }
|
||||
|
||||
public void setCoverPath(String coverPath) { mCoverPath = coverPath; }
|
||||
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GameListFragment extends Fragment implements GameList.OnRefreshListener {
|
||||
private MainActivity mParent;
|
||||
private RecyclerView mRecyclerView;
|
||||
private GameListFragment.ViewAdapter mAdapter;
|
||||
|
||||
public GameListFragment(MainActivity parent) {
|
||||
super(R.layout.fragment_game_list);
|
||||
this.mParent = parent;
|
||||
}
|
||||
|
||||
private GameList getGameList() {
|
||||
return mParent.getGameList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
mAdapter = new GameListFragment.ViewAdapter(mParent, getGameList());
|
||||
getGameList().addRefreshListener(this);
|
||||
|
||||
mRecyclerView = view.findViewById(R.id.game_list_view);
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(mParent));
|
||||
mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
|
||||
DividerItemDecoration.VERTICAL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
getGameList().removeRefreshListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGameListRefresh() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
private MainActivity mParent;
|
||||
private View mItemView;
|
||||
private GameListEntry mEntry;
|
||||
|
||||
public ViewHolder(@NonNull MainActivity parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
mParent = parent;
|
||||
mItemView = itemView;
|
||||
mItemView.setOnClickListener(this);
|
||||
mItemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
private String getSubTitle() {
|
||||
String fileName = FileHelper.getFileNameForPath(mEntry.getPath());
|
||||
String sizeString = String.format("%.2f MB", (double) mEntry.getSize() / 1048576.0);
|
||||
return String.format("%s (%s)", fileName, sizeString);
|
||||
}
|
||||
|
||||
public void bindToEntry(GameListEntry entry) {
|
||||
mEntry = entry;
|
||||
|
||||
((TextView) mItemView.findViewById(R.id.game_list_view_entry_title)).setText(entry.getTitle());
|
||||
((TextView) mItemView.findViewById(R.id.game_list_view_entry_subtitle)).setText(getSubTitle());
|
||||
|
||||
int regionDrawableId;
|
||||
switch (entry.getRegion()) {
|
||||
case NTSC_J:
|
||||
regionDrawableId = R.drawable.flag_jp;
|
||||
break;
|
||||
case PAL:
|
||||
regionDrawableId = R.drawable.flag_eu;
|
||||
break;
|
||||
case Other:
|
||||
regionDrawableId = R.drawable.ic_baseline_help_24;
|
||||
break;
|
||||
case NTSC_U:
|
||||
default:
|
||||
regionDrawableId = R.drawable.flag_us;
|
||||
break;
|
||||
}
|
||||
|
||||
((ImageView) mItemView.findViewById(R.id.game_list_view_entry_region_icon))
|
||||
.setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), regionDrawableId));
|
||||
|
||||
int typeDrawableId;
|
||||
switch (entry.getType()) {
|
||||
case PSExe:
|
||||
typeDrawableId = R.drawable.ic_emblem_system;
|
||||
break;
|
||||
|
||||
case Playlist:
|
||||
typeDrawableId = R.drawable.ic_baseline_playlist_play_24;
|
||||
break;
|
||||
|
||||
case PSF:
|
||||
typeDrawableId = R.drawable.ic_baseline_library_music_24;
|
||||
break;
|
||||
|
||||
case Disc:
|
||||
default:
|
||||
typeDrawableId = R.drawable.ic_media_cdrom;
|
||||
break;
|
||||
}
|
||||
|
||||
ImageView icon = ((ImageView) mItemView.findViewById(R.id.game_list_view_entry_type_icon));
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), typeDrawableId));
|
||||
|
||||
final String coverPath = entry.getCoverPath();
|
||||
if (coverPath != null) {
|
||||
new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coverPath);
|
||||
}
|
||||
|
||||
int compatibilityDrawableId;
|
||||
switch (entry.getCompatibilityRating()) {
|
||||
case DoesntBoot:
|
||||
compatibilityDrawableId = R.drawable.ic_star_1;
|
||||
break;
|
||||
case CrashesInIntro:
|
||||
compatibilityDrawableId = R.drawable.ic_star_2;
|
||||
break;
|
||||
case CrashesInGame:
|
||||
compatibilityDrawableId = R.drawable.ic_star_3;
|
||||
break;
|
||||
case GraphicalAudioIssues:
|
||||
compatibilityDrawableId = R.drawable.ic_star_4;
|
||||
break;
|
||||
case NoIssues:
|
||||
compatibilityDrawableId = R.drawable.ic_star_5;
|
||||
break;
|
||||
case Unknown:
|
||||
default:
|
||||
compatibilityDrawableId = R.drawable.ic_star_0;
|
||||
break;
|
||||
}
|
||||
|
||||
((ImageView) mItemView.findViewById(R.id.game_list_view_compatibility_icon))
|
||||
.setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), compatibilityDrawableId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mParent.startEmulation(mEntry.getPath(), mParent.shouldResumeStateByDefault());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
mParent.openGamePopupMenu(v, mEntry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewAdapter extends RecyclerView.Adapter<GameListFragment.ViewHolder> {
|
||||
private MainActivity mParent;
|
||||
private LayoutInflater mInflater;
|
||||
private GameList mGameList;
|
||||
|
||||
public ViewAdapter(@NonNull MainActivity parent, @NonNull GameList gameList) {
|
||||
mParent = parent;
|
||||
mInflater = LayoutInflater.from(parent);
|
||||
mGameList = gameList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public GameListFragment.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new GameListFragment.ViewHolder(mParent, mInflater.inflate(R.layout.layout_game_list_entry, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull GameListFragment.ViewHolder holder, int position) {
|
||||
GameListEntry entry = mGameList.getEntry(position);
|
||||
holder.bindToEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mGameList.getEntryCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_game_list_entry;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
public class GamePropertiesActivity extends AppCompatActivity {
|
||||
PropertyListAdapter mPropertiesListAdapter;
|
||||
GameListEntry mGameListEntry;
|
||||
|
||||
public ListAdapter getPropertyListAdapter() {
|
||||
if (mPropertiesListAdapter != null)
|
||||
return mPropertiesListAdapter;
|
||||
|
||||
mPropertiesListAdapter = new PropertyListAdapter(this);
|
||||
mPropertiesListAdapter.addItem("title", "Title", mGameListEntry.getTitle());
|
||||
mPropertiesListAdapter.addItem("serial", "Serial", mGameListEntry.getCode());
|
||||
mPropertiesListAdapter.addItem("type", "Type", mGameListEntry.getType().toString());
|
||||
mPropertiesListAdapter.addItem("path", "Path", mGameListEntry.getPath());
|
||||
mPropertiesListAdapter.addItem("region", "Region", mGameListEntry.getRegion().toString());
|
||||
mPropertiesListAdapter.addItem("compatibility", "Compatibility Rating", mGameListEntry.getCompatibilityRating().toString());
|
||||
return mPropertiesListAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
|
||||
String path = getIntent().getStringExtra("path");
|
||||
if (path == null || path.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mGameListEntry = AndroidHostInterface.getInstance().getGameListEntry(path);
|
||||
if (mGameListEntry == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment(this))
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
setTitle(mGameListEntry.getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void createBooleanGameSetting(PreferenceScreen ps, String key, int titleId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
private void createTraitGameSetting(PreferenceScreen ps, String key, int titleId) {
|
||||
GameTraitPreference pref = new GameTraitPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
private void createListGameSetting(PreferenceScreen ps, String key, int titleId, int entryId, int entryValuesId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId, entryId, entryValuesId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
public static class GameSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public GameSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
activity.createListGameSetting(ps, "CPUOverclock", R.string.settings_cpu_overclocking, R.array.settings_advanced_cpu_overclock_entries, R.array.settings_advanced_cpu_overclock_values);
|
||||
activity.createListGameSetting(ps, "CDROMReadSpeedup", R.string.settings_cdrom_read_speedup, R.array.settings_cdrom_read_speedup_entries, R.array.settings_cdrom_read_speedup_values);
|
||||
activity.createListGameSetting(ps, "CDROMSeekSpeedup", R.string.settings_cdrom_seek_speedup, R.array.settings_cdrom_seek_speedup_entries, R.array.settings_cdrom_seek_speedup_values);
|
||||
|
||||
activity.createListGameSetting(ps, "GPURenderer", R.string.settings_gpu_renderer, R.array.gpu_renderer_entries, R.array.gpu_renderer_values);
|
||||
activity.createListGameSetting(ps, "DisplayAspectRatio", R.string.settings_aspect_ratio, R.array.settings_display_aspect_ratio_names, R.array.settings_display_aspect_ratio_values);
|
||||
activity.createListGameSetting(ps, "DisplayCropMode", R.string.settings_crop_mode, R.array.settings_display_crop_mode_entries, R.array.settings_display_crop_mode_values);
|
||||
activity.createListGameSetting(ps, "GPUDownsampleMode", R.string.settings_downsample_mode, R.array.settings_downsample_mode_entries, R.array.settings_downsample_mode_values);
|
||||
activity.createBooleanGameSetting(ps, "DisplayLinearUpscaling", R.string.settings_linear_upscaling);
|
||||
activity.createBooleanGameSetting(ps, "DisplayIntegerUpscaling", R.string.settings_integer_upscaling);
|
||||
activity.createBooleanGameSetting(ps, "DisplayForce4_3For24Bit", R.string.settings_force_4_3_for_24bit);
|
||||
|
||||
activity.createListGameSetting(ps, "GPUResolutionScale", R.string.settings_gpu_resolution_scale, R.array.settings_gpu_resolution_scale_entries, R.array.settings_gpu_resolution_scale_values);
|
||||
activity.createListGameSetting(ps, "GPUMSAA", R.string.settings_msaa, R.array.settings_gpu_msaa_entries, R.array.settings_gpu_msaa_values);
|
||||
activity.createBooleanGameSetting(ps, "GPUTrueColor", R.string.settings_true_color);
|
||||
activity.createBooleanGameSetting(ps, "GPUScaledDithering", R.string.settings_scaled_dithering);
|
||||
activity.createListGameSetting(ps, "GPUTextureFilter", R.string.settings_texture_filtering, R.array.settings_gpu_texture_filter_names, R.array.settings_gpu_texture_filter_values);
|
||||
activity.createBooleanGameSetting(ps, "GPUForceNTSCTimings", R.string.settings_force_ntsc_timings);
|
||||
activity.createBooleanGameSetting(ps, "GPUWidescreenHack", R.string.settings_widescreen_hack);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXP", R.string.settings_pgxp_geometry_correction);
|
||||
activity.createBooleanGameSetting(ps, "PGXPPreserveProjFP", R.string.settings_pgxp_preserve_projection_precision);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXPDepthBuffer", R.string.settings_pgxp_depth_buffer);
|
||||
activity.createTraitGameSetting(ps, "ForceSoftwareRenderer", R.string.settings_use_software_renderer);
|
||||
activity.createTraitGameSetting(ps, "ForceSoftwareRendererForReadbacks", R.string.settings_use_software_renderer_for_readbacks);
|
||||
activity.createTraitGameSetting(ps, "DisableWidescreen", R.string.settings_disable_widescreen);
|
||||
activity.createTraitGameSetting(ps, "ForcePGXPVertexCache", R.string.settings_pgxp_vertex_cache);
|
||||
activity.createTraitGameSetting(ps, "ForcePGXPCPUMode", R.string.settings_pgxp_cpu_mode);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ControllerSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public ControllerSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
private void createInputProfileSetting(PreferenceScreen ps) {
|
||||
final GameSettingPreference pref = new GameSettingPreference(ps.getContext(), activity.mGameListEntry.getPath(), "InputProfileName", R.string.settings_input_profile);
|
||||
|
||||
final String[] inputProfileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
pref.setEntries(inputProfileNames);
|
||||
pref.setEntryValues(inputProfileNames);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
|
||||
activity.createListGameSetting(ps, "Controller1Type", R.string.settings_controller_type, R.array.settings_controller_type_entries, R.array.settings_controller_type_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard1Type", R.string.settings_memory_card_1_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard2Type", R.string.settings_memory_card_2_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
createInputProfileSetting(ps);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private GamePropertiesActivity activity;
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
public SettingsCollectionFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(activity, this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
switch (position) {
|
||||
case 0:
|
||||
tab.setText(R.string.game_properties_tab_summary);
|
||||
break;
|
||||
case 1:
|
||||
tab.setText(R.string.game_properties_tab_game_settings);
|
||||
break;
|
||||
case 2:
|
||||
tab.setText(R.string.game_properties_tab_controller_settings);
|
||||
break;
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull GamePropertiesActivity activity, @NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0: { // Summary
|
||||
ListFragment lf = new ListFragment();
|
||||
lf.setListAdapter(activity.getPropertyListAdapter());
|
||||
return lf;
|
||||
}
|
||||
|
||||
case 1: { // Game Settings
|
||||
return new GameSettingsFragment(activity);
|
||||
}
|
||||
|
||||
case 2: { // Controller Settings
|
||||
return new ControllerSettingsFragment(activity);
|
||||
}
|
||||
|
||||
// TODO: Memory Card Editor
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
|
||||
public class GameSettingPreference extends ListPreference {
|
||||
private String mGamePath;
|
||||
|
||||
/**
|
||||
* Creates a boolean game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(R.array.settings_boolean_entries);
|
||||
setEntryValues(R.array.settings_boolean_values);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId, int entryArray, int entryValuesArray) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(entryArray);
|
||||
setEntryValues(entryValuesArray);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue() {
|
||||
final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey());
|
||||
if (value == null)
|
||||
super.setValue("null");
|
||||
else
|
||||
super.setValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
super.setValue(value);
|
||||
if (value.equals("null"))
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), null);
|
||||
else
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntries(CharSequence[] entries) {
|
||||
final int length = (entries != null) ? entries.length : 0;
|
||||
CharSequence[] newEntries = new CharSequence[length + 1];
|
||||
newEntries[0] = getContext().getString(R.string.game_properties_preference_use_global_setting);
|
||||
if (entries != null)
|
||||
System.arraycopy(entries, 0, newEntries, 1, entries.length);
|
||||
super.setEntries(newEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
final int length = (entryValues != null) ? entryValues.length : 0;
|
||||
CharSequence[] newEntryValues = new CharSequence[length + 1];
|
||||
newEntryValues[0] = "null";
|
||||
if (entryValues != null)
|
||||
System.arraycopy(entryValues, 0, newEntryValues, 1, length);
|
||||
super.setEntryValues(newEntryValues);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.SwitchPreference;
|
||||
|
||||
public class GameTraitPreference extends SwitchPreference {
|
||||
private String mGamePath;
|
||||
|
||||
/**
|
||||
* Creates a boolean game property preference.
|
||||
*/
|
||||
public GameTraitPreference(Context context, String gamePath, String settingKey, int titleId) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue() {
|
||||
final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey());
|
||||
super.setChecked(value != null && value.equals("true"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {
|
||||
super.setChecked(checked);
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), checked ? "true" : "false");
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class GenerateCoverTask extends AsyncTask<Void, Void, Bitmap> {
|
||||
private final Context mContext;
|
||||
private final WeakReference<ImageView> mView;
|
||||
private final String mTitle;
|
||||
|
||||
public GenerateCoverTask(Context context, ImageView view, String title) {
|
||||
mContext = context;
|
||||
mView = new WeakReference<>(view);
|
||||
mTitle = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... voids) {
|
||||
try {
|
||||
final Bitmap background = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.cover_placeholder);
|
||||
if (background == null)
|
||||
return null;
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(background.getWidth(), background.getHeight(), background.getConfig());
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
canvas.drawBitmap(background, 0.0f, 0.0f, paint);
|
||||
|
||||
paint.setColor(Color.rgb(255, 255, 255));
|
||||
paint.setTextSize(100);
|
||||
paint.setShadowLayer(1.0f, 0.0f, 1.0f, Color.DKGRAY);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
final StaticLayout staticLayout = new StaticLayout(mTitle, paint,
|
||||
canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
|
||||
canvas.save();
|
||||
canvas.translate(canvas.getWidth() / 2, (canvas.getHeight() / 2) - (staticLayout.getHeight() / 2));
|
||||
staticLayout.draw(canvas);
|
||||
canvas.restore();
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
ImageView iv = mView.get();
|
||||
if (iv != null)
|
||||
iv.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
// https://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GridAutofitLayoutManager extends GridLayoutManager
|
||||
{
|
||||
private int columnWidth;
|
||||
private boolean isColumnWidthChanged = true;
|
||||
private int lastWidth;
|
||||
private int lastHeight;
|
||||
|
||||
public GridAutofitLayoutManager(@NonNull final Context context, final int columnWidth) {
|
||||
/* Initially set spanCount to 1, will be changed automatically later. */
|
||||
super(context, 1);
|
||||
setColumnWidth(checkedColumnWidth(context, columnWidth));
|
||||
}
|
||||
|
||||
public GridAutofitLayoutManager(
|
||||
@NonNull final Context context,
|
||||
final int columnWidth,
|
||||
final int orientation,
|
||||
final boolean reverseLayout) {
|
||||
|
||||
/* Initially set spanCount to 1, will be changed automatically later. */
|
||||
super(context, 1, orientation, reverseLayout);
|
||||
setColumnWidth(checkedColumnWidth(context, columnWidth));
|
||||
}
|
||||
|
||||
private int checkedColumnWidth(@NonNull final Context context, int columnWidth) {
|
||||
if (columnWidth <= 0) {
|
||||
/* Set default columnWidth value (48dp here). It is better to move this constant
|
||||
to static constant on top, but we need context to convert it to dp, so can't really
|
||||
do so. */
|
||||
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
|
||||
context.getResources().getDisplayMetrics());
|
||||
}
|
||||
return columnWidth;
|
||||
}
|
||||
|
||||
public void setColumnWidth(final int newColumnWidth) {
|
||||
if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
|
||||
columnWidth = newColumnWidth;
|
||||
isColumnWidthChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) {
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
|
||||
final int totalSpace;
|
||||
if (getOrientation() == VERTICAL) {
|
||||
totalSpace = width - getPaddingRight() - getPaddingLeft();
|
||||
} else {
|
||||
totalSpace = height - getPaddingTop() - getPaddingBottom();
|
||||
}
|
||||
final int spanCount = Math.max(1, totalSpace / columnWidth);
|
||||
setSpanCount(spanCount);
|
||||
isColumnWidthChanged = false;
|
||||
}
|
||||
lastWidth = width;
|
||||
lastHeight = height;
|
||||
super.onLayoutChildren(recycler, state);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
public class HotkeyInfo {
|
||||
private String mCategory;
|
||||
private String mName;
|
||||
private String mDisplayName;
|
||||
|
||||
public HotkeyInfo(String category, String name, String displayName) {
|
||||
mCategory = category;
|
||||
mName = name;
|
||||
mDisplayName = displayName;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return mCategory;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
public String getBindingConfigKey() {
|
||||
return String.format("Hotkeys/%s", mName);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class ImageLoadTask extends AsyncTask<String, Void, Bitmap> {
|
||||
private WeakReference<ImageView> mView;
|
||||
|
||||
public ImageLoadTask(ImageView view) {
|
||||
mView = new WeakReference<>(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(String... strings) {
|
||||
try {
|
||||
return BitmapFactory.decodeFile(strings[0]);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
ImageView iv = mView.get();
|
||||
if (iv != null)
|
||||
iv.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
|
@ -1,556 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1;
|
||||
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2;
|
||||
private static final int REQUEST_IMPORT_BIOS_IMAGE = 3;
|
||||
private static final int REQUEST_START_FILE = 4;
|
||||
private static final int REQUEST_SETTINGS = 5;
|
||||
private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6;
|
||||
private static final int REQUEST_CHOOSE_COVER_IMAGE = 7;
|
||||
|
||||
private GameList mGameList;
|
||||
private ListView mGameListView;
|
||||
private GameListFragment mGameListFragment;
|
||||
private GameGridFragment mGameGridFragment;
|
||||
private Fragment mEmptyGameListFragment;
|
||||
private boolean mHasExternalStoragePermissions = false;
|
||||
private boolean mIsShowingGameGrid = false;
|
||||
private String mPathForChosenCoverImage = null;
|
||||
|
||||
public GameList getGameList() {
|
||||
return mGameList;
|
||||
}
|
||||
|
||||
public boolean shouldResumeStateByDefault() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (!prefs.getBoolean("Main/SaveStateOnExit", true))
|
||||
return false;
|
||||
|
||||
// don't resume with challenge mode on
|
||||
if (Achievement.willChallengeModeBeEnabled(this))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setLanguage() {
|
||||
String language = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Language", "none");
|
||||
if (language == null || language.equals("none")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = language.split("-");
|
||||
if (parts.length < 2)
|
||||
return;
|
||||
|
||||
Locale locale = new Locale(parts[0], parts[1]);
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Resources res = getResources();
|
||||
Configuration config = res.getConfiguration();
|
||||
config.setLocale(locale);
|
||||
res.updateConfiguration(config, res.getDisplayMetrics());
|
||||
}
|
||||
|
||||
private void setTheme() {
|
||||
String theme = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Theme", "follow_system");
|
||||
if (theme == null)
|
||||
return;
|
||||
|
||||
if (theme.equals("follow_system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
} else if (theme.equals("light")) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else if (theme.equals("dark")) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
setLanguage();
|
||||
setTheme();
|
||||
|
||||
mIsShowingGameGrid = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("Main/GameGridView", false);
|
||||
}
|
||||
|
||||
private void switchGameListView() {
|
||||
mIsShowingGameGrid = !mIsShowingGameGrid;
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putBoolean("Main/GameGridView", mIsShowingGameGrid)
|
||||
.commit();
|
||||
|
||||
updateGameListFragment(true);
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private void updateGameListFragment(boolean allowEmpty) {
|
||||
final Fragment listFragment = (allowEmpty && mGameList.getEntryCount() == 0) ?
|
||||
mEmptyGameListFragment :
|
||||
(mIsShowingGameGrid ? mGameGridFragment : mGameListFragment);
|
||||
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setReorderingAllowed(true).
|
||||
replace(R.id.content_fragment, listFragment)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
loadSettings();
|
||||
setTitle(null);
|
||||
|
||||
super.onCreate(null);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startEmulation(null, shouldResumeStateByDefault());
|
||||
}
|
||||
});
|
||||
|
||||
// Set up game list view.
|
||||
mGameList = new GameList(this);
|
||||
mGameList.addRefreshListener(() -> updateGameListFragment(true));
|
||||
mGameListFragment = new GameListFragment(this);
|
||||
mGameGridFragment = new GameGridFragment(this);
|
||||
mEmptyGameListFragment = new EmptyGameListFragment(this);
|
||||
updateGameListFragment(false);
|
||||
|
||||
mHasExternalStoragePermissions = checkForExternalStoragePermissions();
|
||||
if (mHasExternalStoragePermissions)
|
||||
completeStartup();
|
||||
}
|
||||
|
||||
private void completeStartup() {
|
||||
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
|
||||
Log.i("MainActivity", "Failed to create host interface");
|
||||
throw new RuntimeException("Failed to create host interface");
|
||||
}
|
||||
|
||||
AndroidHostInterface.getInstance().setContext(this);
|
||||
mGameList.refresh(false, false, this);
|
||||
UpdateNotes.displayUpdateNotes(this);
|
||||
}
|
||||
|
||||
public void startAddGameDirectory() {
|
||||
if (!checkForExternalStoragePermissions())
|
||||
return;
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
|
||||
final MenuItem switchViewItem = menu.findItem(R.id.action_switch_view);
|
||||
if (switchViewItem != null) {
|
||||
switchViewItem.setTitle(mIsShowingGameGrid ? R.string.action_show_game_list : R.string.action_show_game_grid);
|
||||
switchViewItem.setIcon(mIsShowingGameGrid ? R.drawable.ic_baseline_view_list_24 : R.drawable.ic_baseline_grid_view_24);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_start_bios) {
|
||||
startEmulation(null, false);
|
||||
} else if (id == R.id.action_start_file) {
|
||||
startStartFile();
|
||||
} else if (id == R.id.action_edit_game_directories) {
|
||||
Intent intent = new Intent(this, GameDirectoriesActivity.class);
|
||||
startActivityForResult(intent, REQUEST_EDIT_GAME_DIRECTORIES);
|
||||
return true;
|
||||
} else if (id == R.id.action_scan_for_new_games) {
|
||||
mGameList.refresh(false, false, this);
|
||||
} else if (id == R.id.action_rescan_all_games) {
|
||||
mGameList.refresh(true, true, this);
|
||||
} else if (id == R.id.action_import_bios) {
|
||||
importBIOSImage();
|
||||
} else if (id == R.id.action_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivityForResult(intent, REQUEST_SETTINGS);
|
||||
return true;
|
||||
} else if (id == R.id.action_controller_settings) {
|
||||
Intent intent = new Intent(this, ControllerSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.action_memory_card_editor) {
|
||||
Intent intent = new Intent(this, MemoryCardEditorActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.action_switch_view) {
|
||||
switchGameListView();
|
||||
return true;
|
||||
} else if (id == R.id.action_show_version) {
|
||||
showVersion();
|
||||
return true;
|
||||
} else if (id == R.id.action_github_respository) {
|
||||
openGithubRepository();
|
||||
return true;
|
||||
} else if (id == R.id.action_discord_server) {
|
||||
openDiscordServer();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||
if (resultCode != RESULT_OK || data.getData() == null)
|
||||
return;
|
||||
|
||||
// Use legacy storage on devices older than Android 11... apparently some of them
|
||||
// have broken storage access framework....
|
||||
if (!GameDirectoriesActivity.useStorageAccessFramework(this)) {
|
||||
final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this);
|
||||
if (path != null) {
|
||||
GameDirectoriesActivity.addSearchDirectory(this, path, true);
|
||||
mGameList.refresh(false, false, this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
getContentResolver().takePersistableUriPermission(data.getData(),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
GameDirectoriesActivity.addSearchDirectory(this, data.getDataString(), true);
|
||||
mGameList.refresh(false, false, this);
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_IMPORT_BIOS_IMAGE: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
onImportBIOSImageResult(data.getData());
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_START_FILE: {
|
||||
if (resultCode != RESULT_OK || data.getData() == null)
|
||||
return;
|
||||
|
||||
startEmulation(data.getDataString(), shouldResumeStateByDefault());
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_SETTINGS: {
|
||||
loadSettings();
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_EDIT_GAME_DIRECTORIES: {
|
||||
mGameList.refresh(false, false, this);
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_CHOOSE_COVER_IMAGE: {
|
||||
final String gamePath = mPathForChosenCoverImage;
|
||||
mPathForChosenCoverImage = null;
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
finishChooseCoverImage(gamePath, data.getData());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkForExternalStoragePermissions() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat
|
||||
.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
REQUEST_EXTERNAL_STORAGE_PERMISSIONS);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) {
|
||||
// check that all were successful
|
||||
for (int i = 0; i < grantResults.length; i++) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (!mHasExternalStoragePermissions) {
|
||||
mHasExternalStoragePermissions = true;
|
||||
completeStartup();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
R.string.main_activity_external_storage_permissions_error,
|
||||
Toast.LENGTH_LONG);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean openGameProperties(String path) {
|
||||
Intent intent = new Intent(this, GamePropertiesActivity.class);
|
||||
intent.putExtra("path", path);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void openGamePopupMenu(View anchorToView, GameListEntry entry) {
|
||||
androidx.appcompat.widget.PopupMenu menu = new androidx.appcompat.widget.PopupMenu(this, anchorToView, Gravity.RIGHT | Gravity.TOP);
|
||||
menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
|
||||
menu.setOnMenuItemClickListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.game_list_entry_menu_start_game) {
|
||||
startEmulation(entry.getPath(), false);
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_resume_game) {
|
||||
startEmulation(entry.getPath(), true);
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_properties) {
|
||||
openGameProperties(entry.getPath());
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_choose_cover_image) {
|
||||
startChooseCoverImage(entry.getPath());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// disable resume state when challenge mode is on
|
||||
if (Achievement.willChallengeModeBeEnabled(this)) {
|
||||
MenuItem item = menu.getMenu().findItem(R.id.game_list_entry_menu_resume_game);
|
||||
if (item != null)
|
||||
item.setEnabled(false);
|
||||
}
|
||||
|
||||
menu.show();
|
||||
}
|
||||
|
||||
public boolean startEmulation(String bootPath, boolean resumeState) {
|
||||
if (!doBIOSCheck())
|
||||
return false;
|
||||
|
||||
Intent intent = new Intent(this, EmulationActivity.class);
|
||||
intent.putExtra("bootPath", bootPath);
|
||||
intent.putExtra("resumeState", resumeState);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void startStartFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_START_FILE);
|
||||
}
|
||||
|
||||
private void startChooseCoverImage(String gamePath) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("image/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
mPathForChosenCoverImage = gamePath;
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.menu_game_list_entry_choose_cover_image)),
|
||||
REQUEST_CHOOSE_COVER_IMAGE);
|
||||
}
|
||||
|
||||
private void finishChooseCoverImage(String gamePath, Uri uri) {
|
||||
final GameListEntry gameListEntry = mGameList.getEntryForPath(gamePath);
|
||||
if (gameListEntry == null)
|
||||
return;
|
||||
|
||||
final Bitmap bitmap = FileHelper.loadBitmapFromUri(this, uri);
|
||||
if (bitmap == null) {
|
||||
Toast.makeText(this, "Failed to open/decode image.", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String coverFileName = String.format("%s/covers/%s.png",
|
||||
AndroidHostInterface.getUserDirectory(), gameListEntry.getTitle());
|
||||
try {
|
||||
final File file = new File(coverFileName);
|
||||
final OutputStream outputStream = new FileOutputStream(file);
|
||||
final boolean result = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||
outputStream.close();;
|
||||
if (!result) {
|
||||
file.delete();
|
||||
throw new Exception("Failed to compress bitmap.");
|
||||
}
|
||||
|
||||
gameListEntry.setCoverPath(coverFileName);
|
||||
mGameList.fireRefreshListeners();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, "Failed to save image.", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
bitmap.recycle();
|
||||
}
|
||||
|
||||
private boolean doBIOSCheck() {
|
||||
if (AndroidHostInterface.getInstance().hasAnyBIOSImages())
|
||||
return true;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.main_activity_missing_bios_image)
|
||||
.setMessage(R.string.main_activity_missing_bios_image_prompt)
|
||||
.setPositiveButton(R.string.main_activity_yes, (dialog, button) -> importBIOSImage())
|
||||
.setNegativeButton(R.string.main_activity_no, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void importBIOSImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_bios_image)), REQUEST_IMPORT_BIOS_IMAGE);
|
||||
}
|
||||
|
||||
private void onImportBIOSImageResult(Uri uri) {
|
||||
// This should really be 512K but just in case we wanted to support the other BIOSes in the future...
|
||||
final int MAX_BIOS_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
Toast.makeText(this, R.string.main_activity_failed_to_open_bios_image, Toast.LENGTH_LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
byte[] buffer = new byte[512 * 1024];
|
||||
int len;
|
||||
while ((len = stream.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, len);
|
||||
if (os.size() > MAX_BIOS_SIZE) {
|
||||
throw new IOException(getString(R.string.main_activity_bios_image_too_large));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.main_activity_failed_to_read_bios_image_prefix) + e.getMessage())
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
String importResult = AndroidHostInterface.getInstance().importBIOSImage(os.toByteArray());
|
||||
String message = (importResult == null) ? getString(R.string.main_activity_invalid_error) : ("BIOS '" + importResult + "' imported.");
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showVersion() {
|
||||
final String message = AndroidHostInterface.getFullScmVersion();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.main_activity_show_version_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.setNeutralButton(R.string.main_activity_copy, (dialog, button) -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
if (clipboard != null)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(getString(R.string.main_activity_show_version_title), message));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openGithubRepository() {
|
||||
final String url = "https://github.com/stenzek/duckstation";
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private void openDiscordServer() {
|
||||
final String url = "https://discord.gg/Buktv3t";
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
}
|
|
@ -1,524 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MemoryCardEditorActivity extends AppCompatActivity {
|
||||
public static final int REQUEST_IMPORT_CARD = 1;
|
||||
|
||||
private final ArrayList<MemoryCardImage> cards = new ArrayList<>();
|
||||
private CollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
private TabLayout tabLayout;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.activity_memory_card_editor);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
adapter = new CollectionAdapter(this);
|
||||
viewPager = findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
tabLayout = findViewById(R.id.tab_layout);
|
||||
tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager, adapter.getTabConfigurationStrategy());
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
findViewById(R.id.open_card).setOnClickListener((v) -> openCard());
|
||||
findViewById(R.id.close_card).setOnClickListener((v) -> closeCard());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_memory_card_editor, menu);
|
||||
|
||||
final boolean hasCurrentCard = (getCurrentCard() != null);
|
||||
menu.findItem(R.id.action_delete_card).setEnabled(hasCurrentCard);
|
||||
menu.findItem(R.id.action_format_card).setEnabled(hasCurrentCard);
|
||||
menu.findItem(R.id.action_import_card).setEnabled(hasCurrentCard);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
case R.id.action_import_card: {
|
||||
importCard();
|
||||
return true;
|
||||
}
|
||||
|
||||
case R.id.action_delete_card: {
|
||||
deleteCard();
|
||||
return true;
|
||||
}
|
||||
|
||||
case R.id.action_format_card: {
|
||||
formatCard();
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openCard() {
|
||||
final Uri[] uris = MemoryCardImage.getCardUris(this);
|
||||
if (uris == null) {
|
||||
displayMessage(getString(R.string.memory_card_editor_no_cards_found));
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] uriTitles = new String[uris.length];
|
||||
for (int i = 0; i < uris.length; i++)
|
||||
uriTitles[i] = MemoryCardImage.getTitleForUri(uris[i]);
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.memory_card_editor_select_card);
|
||||
builder.setItems(uriTitles, (dialog, which) -> {
|
||||
final Uri uri = uris[which];
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
if (cards.get(i).getUri().equals(uri)) {
|
||||
displayError(getString(R.string.memory_card_editor_card_already_open));
|
||||
tabLayout.getTabAt(i).select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final MemoryCardImage card = MemoryCardImage.open(MemoryCardEditorActivity.this, uri);
|
||||
if (card == null) {
|
||||
displayError(getString(R.string.memory_card_editor_failed_to_open_card));
|
||||
return;
|
||||
}
|
||||
|
||||
cards.add(card);
|
||||
refreshView(card);
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void closeCard() {
|
||||
final int index = tabLayout.getSelectedTabPosition();
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
cards.remove(index);
|
||||
refreshView(index);
|
||||
}
|
||||
|
||||
private void displayMessage(String message) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void displayError(String message) {
|
||||
final AlertDialog.Builder errorBuilder = new AlertDialog.Builder(this);
|
||||
errorBuilder.setTitle(R.string.memory_card_editor_error);
|
||||
errorBuilder.setMessage(message);
|
||||
errorBuilder.setPositiveButton(R.string.main_activity_ok, (dialog1, which1) -> dialog1.dismiss());
|
||||
errorBuilder.create().show();
|
||||
}
|
||||
|
||||
private void copySave(MemoryCardImage sourceCard, MemoryCardFileInfo sourceFile) {
|
||||
if (cards.size() < 2) {
|
||||
displayError(getString(R.string.memory_card_editor_must_have_at_least_two_cards_to_copy));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cards.indexOf(sourceCard) < 0) {
|
||||
// this shouldn't happen..
|
||||
return;
|
||||
}
|
||||
|
||||
final MemoryCardImage[] destinationCards = new MemoryCardImage[cards.size() - 1];
|
||||
final String[] cardTitles = new String[cards.size() - 1];
|
||||
for (int i = 0, d = 0; i < cards.size(); i++) {
|
||||
if (cards.get(i) == sourceCard)
|
||||
continue;
|
||||
|
||||
destinationCards[d] = cards.get(i);
|
||||
cardTitles[d] = cards.get(i).getTitle();
|
||||
d++;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.memory_card_editor_copy_save_to, sourceFile.getTitle()));
|
||||
builder.setItems(cardTitles, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
final MemoryCardImage destinationCard = destinationCards[which];
|
||||
|
||||
byte[] data = null;
|
||||
if (destinationCard.getFreeBlocks() < sourceFile.getNumBlocks()) {
|
||||
displayError(getString(R.string.memory_card_editor_copy_insufficient_blocks, sourceFile.getNumBlocks(),
|
||||
destinationCard.getFreeBlocks()));
|
||||
} else if (destinationCard.hasFile(sourceFile.getFilename())) {
|
||||
displayError(getString(R.string.memory_card_editor_copy_already_exists, sourceFile.getFilename()));
|
||||
} else if ((data = sourceCard.readFile(sourceFile.getFilename())) == null) {
|
||||
displayError(getString(R.string.memory_card_editor_copy_read_failed, sourceFile.getFilename()));
|
||||
} else if (!destinationCard.writeFile(sourceFile.getFilename(), data)) {
|
||||
displayMessage(getString(R.string.memory_card_editor_copy_write_failed, sourceFile.getFilename()));
|
||||
} else {
|
||||
displayMessage(getString(R.string.memory_card_editor_copy_success, sourceFile.getFilename(),
|
||||
destinationCard.getTitle()));
|
||||
refreshView(destinationCard);
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void deleteSave(MemoryCardImage card, MemoryCardFileInfo file) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.memory_card_editor_delete_confirm, file.getFilename()));
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
if (card.deleteFile(file.getFilename())) {
|
||||
displayMessage(getString(R.string.memory_card_editor_delete_success, file.getFilename()));
|
||||
refreshView(card);
|
||||
} else {
|
||||
displayError(getString(R.string.memory_card_editor_delete_failed, file.getFilename()));
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void refreshView(int newSelection) {
|
||||
final int oldPos = viewPager.getCurrentItem();
|
||||
tabLayoutMediator.detach();
|
||||
viewPager.setAdapter(null);
|
||||
viewPager.setAdapter(adapter);
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
if (cards.isEmpty())
|
||||
return;
|
||||
|
||||
if (newSelection < 0 || newSelection >= tabLayout.getTabCount()) {
|
||||
if (oldPos < cards.size())
|
||||
tabLayout.getTabAt(oldPos).select();
|
||||
else
|
||||
tabLayout.getTabAt(cards.size() - 1).select();
|
||||
} else {
|
||||
tabLayout.getTabAt(newSelection).select();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshView(MemoryCardImage newSelectedCard) {
|
||||
if (newSelectedCard == null)
|
||||
refreshView(-1);
|
||||
else
|
||||
refreshView(cards.indexOf(newSelectedCard));
|
||||
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private MemoryCardImage getCurrentCard() {
|
||||
final int index = tabLayout.getSelectedTabPosition();
|
||||
if (index < 0 || index >= cards.size())
|
||||
return null;
|
||||
|
||||
return cards.get(index);
|
||||
}
|
||||
|
||||
private void importCard() {
|
||||
if (getCurrentCard() == null) {
|
||||
displayMessage(getString(R.string.memory_card_editor_no_card_selected));
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_IMPORT_CARD);
|
||||
}
|
||||
|
||||
private void importCard(Uri uri) {
|
||||
final MemoryCardImage card = getCurrentCard();
|
||||
if (card == null)
|
||||
return;
|
||||
|
||||
final byte[] data = FileHelper.readBytesFromUri(this, uri, 16 * 1024 * 1024);
|
||||
if (data == null) {
|
||||
displayError(getString(R.string.memory_card_editor_import_card_read_failed, uri.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
String importFileName = FileHelper.getDocumentNameFromUri(this, uri);
|
||||
if (importFileName == null) {
|
||||
importFileName = uri.getPath();
|
||||
if (importFileName == null || importFileName.isEmpty())
|
||||
importFileName = uri.toString();
|
||||
}
|
||||
|
||||
final String captureImportFileName = importFileName;
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.memory_card_editor_import_card_confirm_message,
|
||||
importFileName, card.getTitle()));
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!card.importCard(captureImportFileName, data)) {
|
||||
displayError(getString(R.string.memory_card_editor_import_failed));
|
||||
return;
|
||||
}
|
||||
|
||||
refreshView(card);
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void formatCard() {
|
||||
final MemoryCardImage card = getCurrentCard();
|
||||
if (card == null) {
|
||||
displayMessage(getString(R.string.memory_card_editor_no_card_selected));
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.memory_card_editor_format_card_confirm_message, card.getTitle()));
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!card.format()) {
|
||||
displayError(getString(R.string.memory_card_editor_format_card_failed, card.getUri().toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
displayMessage(getString(R.string.memory_card_editor_format_card_success, card.getUri().toString()));
|
||||
refreshView(card);
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void deleteCard() {
|
||||
final MemoryCardImage card = getCurrentCard();
|
||||
if (card == null) {
|
||||
displayMessage(getString(R.string.memory_card_editor_no_card_selected));
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.memory_card_editor_delete_card_confirm_message, card.getTitle()));
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!card.delete()) {
|
||||
displayError(getString(R.string.memory_card_editor_delete_card_failed, card.getUri().toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
displayMessage(getString(R.string.memory_card_editor_delete_card_success, card.getUri().toString()));
|
||||
cards.remove(card);
|
||||
refreshView(-1);
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_IMPORT_CARD: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
importCard(data.getData());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SaveViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
private MemoryCardEditorActivity mParent;
|
||||
private View mItemView;
|
||||
private MemoryCardImage mCard;
|
||||
private MemoryCardFileInfo mFile;
|
||||
|
||||
public SaveViewHolder(MemoryCardEditorActivity parent, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
mParent = parent;
|
||||
mItemView = itemView;
|
||||
mItemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
public void bindToEntry(MemoryCardImage card, MemoryCardFileInfo file) {
|
||||
mCard = card;
|
||||
mFile = file;
|
||||
|
||||
((TextView) mItemView.findViewById(R.id.title)).setText(mFile.getTitle());
|
||||
((TextView) mItemView.findViewById(R.id.filename)).setText(mFile.getFilename());
|
||||
|
||||
final String blocksText = String.format("%d Blocks", mFile.getNumBlocks());
|
||||
final String sizeText = String.format("%.1f KB", (float)mFile.getSize() / 1024.0f);
|
||||
((TextView) mItemView.findViewById(R.id.block_size)).setText(blocksText);
|
||||
((TextView) mItemView.findViewById(R.id.file_size)).setText(sizeText);
|
||||
|
||||
if (mFile.getNumIconFrames() > 0) {
|
||||
final Bitmap bitmap = mFile.getIconFrameBitmap(0);
|
||||
if (bitmap != null) {
|
||||
((ImageView) mItemView.findViewById(R.id.icon)).setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(mItemView.getContext());
|
||||
builder.setTitle(mFile.getFilename());
|
||||
builder.setItems(R.array.memory_card_editor_save_menu, ((dialog, which) -> {
|
||||
switch (which) {
|
||||
// Copy Save
|
||||
case 0: {
|
||||
dialog.dismiss();
|
||||
mParent.copySave(mCard, mFile);
|
||||
}
|
||||
break;
|
||||
|
||||
// Delete Save
|
||||
case 1: {
|
||||
dialog.dismiss();
|
||||
mParent.deleteSave(mCard, mFile);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}));
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SaveViewAdapter extends RecyclerView.Adapter<SaveViewHolder> {
|
||||
private MemoryCardEditorActivity parent;
|
||||
private MemoryCardImage card;
|
||||
private MemoryCardFileInfo[] files;
|
||||
|
||||
public SaveViewAdapter(MemoryCardEditorActivity parent, MemoryCardImage card) {
|
||||
this.parent = parent;
|
||||
this.card = card;
|
||||
this.files = card.getFiles();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SaveViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
final View rootView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_memory_card_save, parent, false);
|
||||
return new SaveViewHolder(this.parent, rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SaveViewHolder holder, int position) {
|
||||
holder.bindToEntry(card, files[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return (files != null) ? files.length : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_memory_card_save;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MemoryCardFileFragment extends Fragment {
|
||||
private MemoryCardEditorActivity parent;
|
||||
private MemoryCardImage card;
|
||||
private SaveViewAdapter adapter;
|
||||
private RecyclerView recyclerView;
|
||||
|
||||
public MemoryCardFileFragment(MemoryCardEditorActivity parent, MemoryCardImage card) {
|
||||
this.parent = parent;
|
||||
this.card = card;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_memory_card_file, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SaveViewAdapter(parent, card);
|
||||
recyclerView = view.findViewById(R.id.recyclerView);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(),
|
||||
DividerItemDecoration.VERTICAL));
|
||||
}
|
||||
}
|
||||
|
||||
public static class CollectionAdapter extends FragmentStateAdapter {
|
||||
private MemoryCardEditorActivity parent;
|
||||
private final TabLayoutMediator.TabConfigurationStrategy tabConfigurationStrategy = (tab, position) -> {
|
||||
tab.setText(parent.cards.get(position).getTitle());
|
||||
};
|
||||
|
||||
public CollectionAdapter(MemoryCardEditorActivity parent) {
|
||||
super(parent);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public TabLayoutMediator.TabConfigurationStrategy getTabConfigurationStrategy() {
|
||||
return tabConfigurationStrategy;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
return new MemoryCardFileFragment(parent, parent.cards.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return parent.cards.size();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class MemoryCardFileInfo {
|
||||
public static final int ICON_WIDTH = 16;
|
||||
public static final int ICON_HEIGHT = 16;
|
||||
|
||||
private final String filename;
|
||||
private final String title;
|
||||
private final int size;
|
||||
private final int firstBlock;
|
||||
private final int numBlocks;
|
||||
private final byte[][] iconFrames;
|
||||
|
||||
public MemoryCardFileInfo(String filename, String title, int size, int firstBlock, int numBlocks, byte[][] iconFrames) {
|
||||
this.filename = filename;
|
||||
this.title = title;
|
||||
this.size = size;
|
||||
this.firstBlock = firstBlock;
|
||||
this.numBlocks = numBlocks;
|
||||
this.iconFrames = iconFrames;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getFirstBlock() {
|
||||
return firstBlock;
|
||||
}
|
||||
|
||||
public int getNumBlocks() {
|
||||
return numBlocks;
|
||||
}
|
||||
|
||||
public int getNumIconFrames() {
|
||||
return (iconFrames != null) ? iconFrames.length : 0;
|
||||
}
|
||||
|
||||
public byte[] getIconFrame(int index) {
|
||||
return iconFrames[index];
|
||||
}
|
||||
|
||||
public Bitmap getIconFrameBitmap(int index) {
|
||||
final byte[] data = getIconFrame(index);
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(ICON_WIDTH, ICON_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
|
||||
return bitmap;
|
||||
}
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
public class MemoryCardImage {
|
||||
public static final int DATA_SIZE = 128 * 1024;
|
||||
public static final String FILE_EXTENSION = ".mcd";
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
private final byte[] data;
|
||||
|
||||
private MemoryCardImage(Context context, Uri uri, byte[] data) {
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static String getTitleForUri(Uri uri) {
|
||||
String name = uri.getLastPathSegment();
|
||||
if (name != null) {
|
||||
final int lastSlash = name.lastIndexOf('/');
|
||||
if (lastSlash >= 0)
|
||||
name = name.substring(lastSlash + 1);
|
||||
|
||||
if (name.endsWith(FILE_EXTENSION))
|
||||
name = name.substring(0, name.length() - FILE_EXTENSION.length());
|
||||
} else {
|
||||
name = uri.toString();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public static MemoryCardImage open(Context context, Uri uri) {
|
||||
byte[] data = FileHelper.readBytesFromUri(context, uri, DATA_SIZE);
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
if (!isValid(data))
|
||||
return null;
|
||||
|
||||
return new MemoryCardImage(context, uri, data);
|
||||
}
|
||||
|
||||
public static MemoryCardImage create(Context context, Uri uri) {
|
||||
byte[] data = new byte[DATA_SIZE];
|
||||
format(data);
|
||||
|
||||
MemoryCardImage card = new MemoryCardImage(context, uri, data);
|
||||
if (!card.save())
|
||||
return null;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
public static Uri[] getCardUris(Context context) {
|
||||
final String directory = String.format("%s/memcards",
|
||||
AndroidHostInterface.getUserDirectory());
|
||||
final ArrayList<Uri> results = new ArrayList<>();
|
||||
|
||||
if (directory.charAt(0) == '/') {
|
||||
// native path
|
||||
final File directoryFile = new File(directory);
|
||||
final File[] files = directoryFile.listFiles();
|
||||
for (File file : files) {
|
||||
if (!file.isFile())
|
||||
continue;
|
||||
|
||||
if (!file.getName().endsWith(FILE_EXTENSION))
|
||||
continue;
|
||||
|
||||
results.add(Uri.fromFile(file));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
final Uri baseUri = null;
|
||||
final String[] scanProjection = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE};
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final String treeDocId = DocumentsContract.getTreeDocumentId(baseUri);
|
||||
final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(baseUri, treeDocId);
|
||||
final Cursor cursor = resolver.query(queryUri, scanProjection, null, null, null);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final String mimeType = cursor.getString(2);
|
||||
final String documentId = cursor.getString(0);
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(baseUri, documentId);
|
||||
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String uriString = uri.toString();
|
||||
if (!uriString.endsWith(FILE_EXTENSION))
|
||||
continue;
|
||||
|
||||
results.add(uri);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (results.isEmpty())
|
||||
return null;
|
||||
|
||||
Collections.sort(results, (a, b) -> a.compareTo(b));
|
||||
|
||||
final Uri[] resultArray = new Uri[results.size()];
|
||||
results.toArray(resultArray);
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
private static native boolean isValid(byte[] data);
|
||||
|
||||
private static native void format(byte[] data);
|
||||
|
||||
private static native int getFreeBlocks(byte[] data);
|
||||
|
||||
private static native MemoryCardFileInfo[] getFiles(byte[] data);
|
||||
|
||||
private static native boolean hasFile(byte[] data, String filename);
|
||||
|
||||
private static native byte[] readFile(byte[] data, String filename);
|
||||
|
||||
private static native boolean writeFile(byte[] data, String filename, byte[] fileData);
|
||||
|
||||
private static native boolean deleteFile(byte[] data, String filename);
|
||||
|
||||
private static native boolean importCard(byte[] data, String filename, byte[] importData);
|
||||
|
||||
public boolean save() {
|
||||
return FileHelper.writeBytesToUri(context, uri, data);
|
||||
}
|
||||
|
||||
public boolean delete() {
|
||||
return FileHelper.deleteFileAtUri(context, uri);
|
||||
}
|
||||
|
||||
public boolean format() {
|
||||
format(data);
|
||||
return save();
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return getTitleForUri(uri);
|
||||
}
|
||||
|
||||
public int getFreeBlocks() {
|
||||
return getFreeBlocks(data);
|
||||
}
|
||||
|
||||
public MemoryCardFileInfo[] getFiles() {
|
||||
return getFiles(data);
|
||||
}
|
||||
|
||||
public boolean hasFile(String filename) {
|
||||
return hasFile(data, filename);
|
||||
}
|
||||
|
||||
public byte[] readFile(String filename) {
|
||||
return readFile(data, filename);
|
||||
}
|
||||
|
||||
public boolean writeFile(String filename, byte[] fileData) {
|
||||
if (!writeFile(data, filename, fileData))
|
||||
return false;
|
||||
|
||||
return save();
|
||||
}
|
||||
|
||||
public boolean deleteFile(String filename) {
|
||||
if (!deleteFile(data, filename))
|
||||
return false;
|
||||
|
||||
return save();
|
||||
}
|
||||
|
||||
public boolean importCard(String filename, byte[] importData) {
|
||||
if (!importCard(data, filename, importData))
|
||||
return false;
|
||||
|
||||
return save();
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
public class PatchCode {
|
||||
private static final String UNGROUPED_NAME = "Ungrouped";
|
||||
|
||||
private int mIndex;
|
||||
private String mGroup;
|
||||
private String mDescription;
|
||||
private boolean mEnabled;
|
||||
|
||||
public PatchCode(int index, String group, String description, boolean enabled) {
|
||||
mIndex = index;
|
||||
mGroup = group;
|
||||
mDescription = description;
|
||||
mEnabled = enabled;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return mGroup;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
if (mGroup == null || mGroup.equals(UNGROUPED_NAME))
|
||||
return mDescription;
|
||||
else
|
||||
return String.format("(%s) %s", mGroup, mDescription);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class PreferenceHelpers {
|
||||
/**
|
||||
* Clears all preferences in the specified section (starting with sectionName/).
|
||||
* We really don't want to have to do this with JNI...
|
||||
*
|
||||
* @param prefs Preferences object.
|
||||
* @param sectionName Section to clear keys for.
|
||||
*/
|
||||
public static void clearSection(SharedPreferences prefs, String sectionName) {
|
||||
String testSectionName = sectionName + "/";
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
for (String keyName : prefs.getAll().keySet()) {
|
||||
if (keyName.startsWith(testSectionName)) {
|
||||
editor.remove(keyName);
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static Set<String> getStringSet(SharedPreferences prefs, String keyName) {
|
||||
Set<String> values = null;
|
||||
try {
|
||||
values = prefs.getStringSet(keyName, null);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
String singleValue = prefs.getString(keyName, null);
|
||||
if (singleValue != null) {
|
||||
values = new ArraySet<>();
|
||||
values.add(singleValue);
|
||||
}
|
||||
} catch (Exception e2) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static boolean addToStringList(SharedPreferences prefs, String keyName, String valueToAdd) {
|
||||
Set<String> values = getStringSet(prefs, keyName);
|
||||
if (values == null) {
|
||||
values = new ArraySet<>();
|
||||
} else {
|
||||
// We need to copy it otherwise the put doesn't save.
|
||||
Set<String> valuesCopy = new ArraySet<>();
|
||||
valuesCopy.addAll(values);
|
||||
values = valuesCopy;
|
||||
}
|
||||
|
||||
final boolean result = values.add(valueToAdd);
|
||||
prefs.edit().putStringSet(keyName, values).commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean removeFromStringList(SharedPreferences prefs, String keyName, String valueToRemove) {
|
||||
Set<String> values = getStringSet(prefs, keyName);
|
||||
if (values == null)
|
||||
return false;
|
||||
|
||||
// We need to copy it otherwise the put doesn't save.
|
||||
Set<String> valuesCopy = new ArraySet<>();
|
||||
valuesCopy.addAll(values);
|
||||
values = valuesCopy;
|
||||
|
||||
final boolean result = values.remove(valueToRemove);
|
||||
prefs.edit().putStringSet(keyName, values).commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setStringList(SharedPreferences prefs, String keyName, String[] values) {
|
||||
Set<String> valueSet = new ArraySet<String>();
|
||||
for (String value : values)
|
||||
valueSet.add(value);
|
||||
|
||||
prefs.edit().putStringSet(keyName, valueSet).commit();
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class PropertyListAdapter extends BaseAdapter {
|
||||
private class Item {
|
||||
public String key;
|
||||
public String title;
|
||||
public String value;
|
||||
|
||||
Item(String key, String title, String value) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private ArrayList<Item> mItems = new ArrayList<>();
|
||||
|
||||
public PropertyListAdapter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Item getItemByKey(String key) {
|
||||
for (Item it : mItems) {
|
||||
if (it.key.equals(key))
|
||||
return it;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int addItem(String key, String title, String value) {
|
||||
if (getItemByKey(key) != null)
|
||||
return -1;
|
||||
|
||||
Item it = new Item(key, title, value);
|
||||
int position = mItems.size();
|
||||
mItems.add(it);
|
||||
return position;
|
||||
}
|
||||
|
||||
public boolean removeItem(Item item) {
|
||||
return mItems.remove(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mItems.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.layout_game_property_entry, parent, false);
|
||||
}
|
||||
|
||||
TextView titleView = (TextView) convertView.findViewById(R.id.property_title);
|
||||
TextView valueView = (TextView) convertView.findViewById(R.id.property_value);
|
||||
Item prop = mItems.get(position);
|
||||
titleView.setText(prop.title);
|
||||
valueView.setText(prop.value);
|
||||
return convertView;
|
||||
}
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
public class RatioPreference extends Preference {
|
||||
private String mNumeratorKey;
|
||||
private String mDenominatorKey;
|
||||
|
||||
private int mNumeratorValue = 1;
|
||||
private int mDenominatorValue = 1;
|
||||
|
||||
private int mMinimumNumerator = 1;
|
||||
private int mMaximumNumerator = 50;
|
||||
private int mDefaultNumerator = 1;
|
||||
private int mMinimumDenominator = 1;
|
||||
private int mMaximumDenominator = 50;
|
||||
private int mDefaultDenominator = 1;
|
||||
|
||||
private void initAttributes(AttributeSet attrs) {
|
||||
for (int i = 0; i < attrs.getAttributeCount(); i++) {
|
||||
final String key = attrs.getAttributeName(i);
|
||||
if (key.equals("numeratorKey")) {
|
||||
mNumeratorKey = attrs.getAttributeValue(i);
|
||||
} else if (key.equals("minimumNumerator")) {
|
||||
mMinimumNumerator = attrs.getAttributeIntValue(i, 1);
|
||||
} else if (key.equals("maximumNumerator")) {
|
||||
mMaximumNumerator = attrs.getAttributeIntValue(i, 1);
|
||||
} else if (key.equals("defaultNumerator")) {
|
||||
mDefaultNumerator = attrs.getAttributeIntValue(i, 1);
|
||||
} else if(key.equals("denominatorKey")) {
|
||||
mDenominatorKey = attrs.getAttributeValue(i);
|
||||
} else if (key.equals("minimumDenominator")) {
|
||||
mMinimumDenominator = attrs.getAttributeIntValue(i, 1);
|
||||
} else if (key.equals("maximumDenominator")) {
|
||||
mMaximumDenominator = attrs.getAttributeIntValue(i, 1);
|
||||
} else if (key.equals("defaultDenominator")) {
|
||||
mDefaultDenominator = attrs.getAttributeIntValue(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RatioPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setWidgetLayoutResource(R.layout.layout_ratio_preference);
|
||||
initAttributes(attrs);
|
||||
}
|
||||
|
||||
public RatioPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.layout_ratio_preference);
|
||||
initAttributes(attrs);
|
||||
}
|
||||
|
||||
public RatioPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setWidgetLayoutResource(R.layout.layout_ratio_preference);
|
||||
initAttributes(attrs);
|
||||
}
|
||||
|
||||
public RatioPreference(Context context) {
|
||||
super(context);
|
||||
setWidgetLayoutResource(R.layout.layout_ratio_preference);
|
||||
}
|
||||
|
||||
private void persistValues() {
|
||||
final PreferenceDataStore dataStore = getPreferenceDataStore();
|
||||
if (dataStore != null) {
|
||||
if (mNumeratorKey != null)
|
||||
dataStore.putInt(mNumeratorKey, mNumeratorValue);
|
||||
if (mDenominatorKey != null)
|
||||
dataStore.putInt(mDenominatorKey, mDenominatorValue);
|
||||
} else {
|
||||
SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
|
||||
if (mNumeratorKey != null)
|
||||
editor.putInt(mNumeratorKey, mNumeratorValue);
|
||||
if (mDenominatorKey != null)
|
||||
editor.putInt(mDenominatorKey, mDenominatorValue);
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
|
||||
super.onAttachedToHierarchy(preferenceManager);
|
||||
setInitialValue();
|
||||
}
|
||||
|
||||
private void setInitialValue() {
|
||||
final PreferenceDataStore dataStore = getPreferenceDataStore();
|
||||
if (dataStore != null) {
|
||||
if (mNumeratorKey != null)
|
||||
mNumeratorValue = dataStore.getInt(mNumeratorKey, mDefaultNumerator);
|
||||
if (mDenominatorKey != null)
|
||||
mDenominatorValue = dataStore.getInt(mDenominatorKey, mDefaultDenominator);
|
||||
} else {
|
||||
final SharedPreferences pm = getPreferenceManager().getSharedPreferences();
|
||||
if (mNumeratorKey != null)
|
||||
mNumeratorValue = pm.getInt(mNumeratorKey, mDefaultNumerator);
|
||||
if (mDenominatorKey != null)
|
||||
mDenominatorValue = pm.getInt(mDenominatorKey, mDefaultDenominator);
|
||||
}
|
||||
}
|
||||
|
||||
private static BaseAdapter generateDropdownItems(int min, int max) {
|
||||
return new BaseAdapter() {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (max - min) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return Integer.toString(min + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextView view;
|
||||
if (convertView != null) {
|
||||
view = (TextView) convertView;
|
||||
} else {
|
||||
view = new TextView(parent.getContext());
|
||||
|
||||
Resources r = parent.getResources();
|
||||
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, r.getDisplayMetrics());
|
||||
view.setPadding((int) px, (int) px, (int) px, (int) px);
|
||||
}
|
||||
|
||||
view.setText(Integer.toString(min + position));
|
||||
return view;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
holder.itemView.setClickable(false);
|
||||
|
||||
Spinner numeratorSpinner = (Spinner) holder.findViewById(R.id.numerator);
|
||||
numeratorSpinner.setAdapter(generateDropdownItems(mMinimumNumerator, mMaximumNumerator));
|
||||
numeratorSpinner.setSelection(mNumeratorValue - mMinimumNumerator);
|
||||
numeratorSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
final int newValue = position + mMinimumNumerator;
|
||||
if (newValue != mNumeratorValue) {
|
||||
mNumeratorValue = newValue;
|
||||
persistValues();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
Spinner denominatorSpinner = (Spinner) holder.findViewById(R.id.denominator);
|
||||
denominatorSpinner.setAdapter(generateDropdownItems(mMinimumDenominator, mMaximumDenominator));
|
||||
denominatorSpinner.setSelection(mDenominatorValue - mMinimumDenominator);
|
||||
denominatorSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
final int newValue = position + mMinimumDenominator;
|
||||
if (newValue != mDenominatorValue) {
|
||||
mDenominatorValue = newValue;
|
||||
persistValues();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SaveStateInfo {
|
||||
private String mPath;
|
||||
private String mGameTitle;
|
||||
private String mGameCode;
|
||||
private String mMediaPath;
|
||||
private String mTimestamp;
|
||||
private int mSlot;
|
||||
private boolean mGlobal;
|
||||
private Bitmap mScreenshot;
|
||||
|
||||
public SaveStateInfo(String path, String gameTitle, String gameCode, String mediaPath, String timestamp, int slot, boolean global,
|
||||
int screenshotWidth, int screenshotHeight, byte[] screenshotData) {
|
||||
mPath = path;
|
||||
mGameTitle = gameTitle;
|
||||
mGameCode = gameCode;
|
||||
mMediaPath = mediaPath;
|
||||
mTimestamp = timestamp;
|
||||
mSlot = slot;
|
||||
mGlobal = global;
|
||||
|
||||
if (screenshotData != null) {
|
||||
try {
|
||||
mScreenshot = Bitmap.createBitmap(screenshotWidth, screenshotHeight, Bitmap.Config.ARGB_8888);
|
||||
mScreenshot.copyPixelsFromBuffer(ByteBuffer.wrap(screenshotData));
|
||||
} catch (Exception e) {
|
||||
mScreenshot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return mPath != null;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getGameTitle() {
|
||||
return mGameTitle;
|
||||
}
|
||||
|
||||
public String getGameCode() {
|
||||
return mGameCode;
|
||||
}
|
||||
|
||||
public String getMediaPath() {
|
||||
return mMediaPath;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
public int getSlot() {
|
||||
return mSlot;
|
||||
}
|
||||
|
||||
public boolean isGlobal() {
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
public Bitmap getScreenshot() {
|
||||
return mScreenshot;
|
||||
}
|
||||
|
||||
private void fillView(Context context, View view) {
|
||||
ImageView imageView = (ImageView) view.findViewById(R.id.image);
|
||||
TextView summaryView = (TextView) view.findViewById(R.id.summary);
|
||||
TextView gameView = (TextView) view.findViewById(R.id.game);
|
||||
TextView pathView = (TextView) view.findViewById(R.id.path);
|
||||
TextView timestampView = (TextView) view.findViewById(R.id.timestamp);
|
||||
|
||||
if (mScreenshot != null)
|
||||
imageView.setImageBitmap(mScreenshot);
|
||||
else
|
||||
imageView.setImageDrawable(context.getDrawable(R.drawable.ic_baseline_not_interested_60));
|
||||
|
||||
String summaryText;
|
||||
if (mGlobal)
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_global_save_n), mSlot));
|
||||
else if (mSlot == 0)
|
||||
summaryView.setText(R.string.save_state_info_quick_save);
|
||||
else
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_game_save_n), mSlot));
|
||||
|
||||
if (exists()) {
|
||||
gameView.setText(String.format("%s - %s", mGameCode, mGameTitle));
|
||||
|
||||
int lastSlashPosition = mMediaPath.lastIndexOf('/');
|
||||
if (lastSlashPosition >= 0)
|
||||
pathView.setText(mMediaPath.substring(lastSlashPosition + 1));
|
||||
else
|
||||
pathView.setText(mMediaPath);
|
||||
|
||||
timestampView.setText(mTimestamp);
|
||||
} else {
|
||||
gameView.setText(R.string.save_state_info_slot_is_empty);
|
||||
pathView.setText("");
|
||||
timestampView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ListAdapter extends BaseAdapter {
|
||||
private final Context mContext;
|
||||
private final SaveStateInfo[] mInfos;
|
||||
|
||||
public ListAdapter(Context context, SaveStateInfo[] infos) {
|
||||
mContext = context;
|
||||
mInfos = infos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mInfos.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mInfos[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext).inflate(R.layout.save_state_view_entry, parent, false);
|
||||
}
|
||||
|
||||
mInfos[position].fillView(mContext, convertView);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment())
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
public class SettingsCollectionFragment extends Fragment {
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings_collection, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager,
|
||||
(tab, position) -> tab.setText(getResources().getStringArray(R.array.settings_tabs)[position])
|
||||
).attach();
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private final int resourceId;
|
||||
|
||||
public SettingsFragment(int resourceId) {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(resourceId, rootKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
public SettingsCollectionAdapter(@NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0: // General
|
||||
return new SettingsFragment(R.xml.general_preferences);
|
||||
|
||||
case 1: // Display
|
||||
return new SettingsFragment(R.xml.display_preferences);
|
||||
|
||||
case 2: // Audio
|
||||
return new SettingsFragment(R.xml.audio_preferences);
|
||||
|
||||
case 3: // Enhancements
|
||||
return new SettingsFragment(R.xml.enhancements_preferences);
|
||||
|
||||
case 4: // Achievements
|
||||
return new AchievementSettingsFragment();
|
||||
|
||||
case 5: // Advanced
|
||||
return new SettingsFragment(R.xml.advanced_preferences);
|
||||
|
||||
default:
|
||||
return new Fragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public final class TouchscreenControllerAxisView extends View {
|
||||
private Drawable mBaseDrawable;
|
||||
private Drawable mStickUnpressedDrawable;
|
||||
private Drawable mStickPressedDrawable;
|
||||
private boolean mPressed = false;
|
||||
private int mPointerId = 0;
|
||||
private float mXValue = 0.0f;
|
||||
private float mYValue = 0.0f;
|
||||
private int mDrawXPos = 0;
|
||||
private int mDrawYPos = 0;
|
||||
|
||||
private String mConfigName;
|
||||
private boolean mDefaultVisibility = true;
|
||||
|
||||
private int mControllerIndex = -1;
|
||||
private int mXAxisCode = -1;
|
||||
private int mYAxisCode = -1;
|
||||
private int mLeftButtonCode = -1;
|
||||
private int mRightButtonCode = -1;
|
||||
private int mUpButtonCode = -1;
|
||||
private int mDownButtonCode = -1;
|
||||
|
||||
public TouchscreenControllerAxisView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerAxisView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerAxisView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mBaseDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_base);
|
||||
mBaseDrawable.setCallback(this);
|
||||
mStickUnpressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_unpressed);
|
||||
mStickUnpressedDrawable.setCallback(this);
|
||||
mStickPressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_pressed);
|
||||
mStickPressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
public String getConfigName() {
|
||||
return mConfigName;
|
||||
}
|
||||
public void setConfigName(String configName) {
|
||||
mConfigName = configName;
|
||||
}
|
||||
|
||||
public boolean getDefaultVisibility() { return mDefaultVisibility; }
|
||||
public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; }
|
||||
|
||||
public void setControllerAxis(int controllerIndex, int xCode, int yCode) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mXAxisCode = xCode;
|
||||
mYAxisCode = yCode;
|
||||
mLeftButtonCode = -1;
|
||||
mRightButtonCode = -1;
|
||||
mUpButtonCode = -1;
|
||||
mDownButtonCode = -1;
|
||||
}
|
||||
|
||||
public void setControllerButtons(int controllerIndex, int leftCode, int rightCode, int upCode, int downCode) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mXAxisCode = -1;
|
||||
mYAxisCode = -1;
|
||||
mLeftButtonCode = leftCode;
|
||||
mRightButtonCode = rightCode;
|
||||
mUpButtonCode = upCode;
|
||||
mDownButtonCode = downCode;
|
||||
}
|
||||
|
||||
public void setUnpressed() {
|
||||
if (!mPressed && mXValue == 0.0f && mYValue == 0.0f)
|
||||
return;
|
||||
|
||||
mPressed = false;
|
||||
mXValue = 0.0f;
|
||||
mYValue = 0.0f;
|
||||
mDrawXPos = 0;
|
||||
mDrawYPos = 0;
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
}
|
||||
|
||||
public void setPressed(int pointerId, float pointerX, float pointerY) {
|
||||
final float dx = (pointerX / getScaleX()) - (float) (getWidth() / 2);
|
||||
final float dy = (pointerY / getScaleY()) - (float) (getHeight() / 2);
|
||||
// Log.i("SetPressed", String.format("px=%f,py=%f dx=%f,dy=%f", pointerX, pointerY, dx, dy));
|
||||
|
||||
final float pointerDistance = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
final float angle = (float) Math.atan2((double) dy, (double) dx);
|
||||
|
||||
final float maxDistance = (float) Math.min((getWidth() - getPaddingLeft() - getPaddingRight()) / 2, (getHeight() - getPaddingTop() - getPaddingBottom()) / 2);
|
||||
final float length = Math.min(pointerDistance / maxDistance, 1.0f);
|
||||
// Log.i("SetPressed", String.format("pointerDist=%f,angle=%f,w=%d,h=%d,maxDist=%f,length=%f", pointerDistance, angle, getWidth(), getHeight(), maxDistance, length));
|
||||
|
||||
final float xValue = (float) Math.cos((double) angle) * length;
|
||||
final float yValue = (float) Math.sin((double) angle) * length;
|
||||
mDrawXPos = (int) (xValue * maxDistance);
|
||||
mDrawYPos = (int) (yValue * maxDistance);
|
||||
|
||||
boolean doUpdate = (pointerId != mPointerId || !mPressed || (xValue != mXValue || yValue != mYValue));
|
||||
mPointerId = pointerId;
|
||||
mPressed = true;
|
||||
mXValue = xValue;
|
||||
mYValue = yValue;
|
||||
// Log.i("SetPressed", String.format("xval=%f,yval=%f,drawX=%d,drawY=%d", mXValue, mYValue, mDrawXPos, mDrawYPos));
|
||||
|
||||
if (doUpdate) {
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateControllerState() {
|
||||
final float BUTTON_THRESHOLD = 0.33f;
|
||||
|
||||
AndroidHostInterface hostInterface = AndroidHostInterface.getInstance();
|
||||
if (mXAxisCode >= 0)
|
||||
hostInterface.setControllerAxisState(mControllerIndex, mXAxisCode, mXValue);
|
||||
if (mYAxisCode >= 0)
|
||||
hostInterface.setControllerAxisState(mControllerIndex, mYAxisCode, mYValue);
|
||||
|
||||
if (mLeftButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mLeftButtonCode, (mXValue <= -BUTTON_THRESHOLD));
|
||||
if (mRightButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mRightButtonCode, (mXValue >= BUTTON_THRESHOLD));
|
||||
if (mUpButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mUpButtonCode, (mYValue <= -BUTTON_THRESHOLD));
|
||||
if (mDownButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mDownButtonCode, (mYValue >= BUTTON_THRESHOLD));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int paddingLeft = getPaddingLeft();
|
||||
final int paddingTop = getPaddingTop();
|
||||
final int paddingRight = getPaddingRight();
|
||||
final int paddingBottom = getPaddingBottom();
|
||||
final int contentWidth = getWidth() - paddingLeft - paddingRight;
|
||||
final int contentHeight = getHeight() - paddingTop - paddingBottom;
|
||||
|
||||
mBaseDrawable.setBounds(paddingLeft, paddingTop,
|
||||
paddingLeft + contentWidth, paddingTop + contentHeight);
|
||||
mBaseDrawable.draw(canvas);
|
||||
|
||||
final int stickWidth = contentWidth / 3;
|
||||
final int stickHeight = contentHeight / 3;
|
||||
final int halfStickWidth = stickWidth / 2;
|
||||
final int halfStickHeight = stickHeight / 2;
|
||||
final int centerX = getWidth() / 2;
|
||||
final int centerY = getHeight() / 2;
|
||||
final int drawX = centerX + mDrawXPos;
|
||||
final int drawY = centerY + mDrawYPos;
|
||||
|
||||
Drawable stickDrawable = mPressed ? mStickPressedDrawable : mStickUnpressedDrawable;
|
||||
stickDrawable.setBounds(drawX - halfStickWidth, drawY - halfStickHeight, drawX + halfStickWidth, drawY + halfStickHeight);
|
||||
stickDrawable.draw(canvas);
|
||||
}
|
||||
|
||||
public boolean isPressed() {
|
||||
return mPressed;
|
||||
}
|
||||
|
||||
public boolean hasPointerId() {
|
||||
return mPointerId >= 0;
|
||||
}
|
||||
|
||||
public int getPointerId() {
|
||||
return mPointerId;
|
||||
}
|
||||
|
||||
public void setPointerId(int mPointerId) {
|
||||
this.mPointerId = mPointerId;
|
||||
}
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* TODO: document your custom view class.
|
||||
*/
|
||||
public final class TouchscreenControllerButtonView extends View {
|
||||
public enum Hotkey
|
||||
{
|
||||
NONE,
|
||||
FAST_FORWARD,
|
||||
ANALOG_TOGGLE,
|
||||
OPEN_PAUSE_MENU,
|
||||
QUICK_LOAD,
|
||||
QUICK_SAVE
|
||||
}
|
||||
|
||||
private Drawable mUnpressedDrawable;
|
||||
private Drawable mPressedDrawable;
|
||||
private boolean mPressed = false;
|
||||
private boolean mHapticFeedback = false;
|
||||
private int mControllerIndex = -1;
|
||||
private int mButtonCode = -1;
|
||||
private int mAutoFireSlot = -1;
|
||||
private Hotkey mHotkey = Hotkey.NONE;
|
||||
private String mConfigName;
|
||||
private boolean mDefaultVisibility = true;
|
||||
private boolean mIsGlidable = true;
|
||||
|
||||
public TouchscreenControllerButtonView(Context context) {
|
||||
super(context);
|
||||
init(context, null, 0);
|
||||
}
|
||||
|
||||
public TouchscreenControllerButtonView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TouchscreenControllerButtonView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs, int defStyle) {
|
||||
// Load attributes
|
||||
final TypedArray a = getContext().obtainStyledAttributes(
|
||||
attrs, R.styleable.TouchscreenControllerButtonView, defStyle, 0);
|
||||
|
||||
if (a.hasValue(R.styleable.TouchscreenControllerButtonView_unpressedDrawable)) {
|
||||
mUnpressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_unpressedDrawable);
|
||||
mUnpressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
if (a.hasValue(R.styleable.TouchscreenControllerButtonView_pressedDrawable)) {
|
||||
mPressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_pressedDrawable);
|
||||
mPressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int leftBounds = 0;
|
||||
int rightBounds = leftBounds + getWidth();
|
||||
int topBounds = 0;
|
||||
int bottomBounds = topBounds + getHeight();
|
||||
|
||||
if (mPressed) {
|
||||
final int expandSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
10.0f, getResources().getDisplayMetrics());
|
||||
leftBounds -= expandSize;
|
||||
rightBounds += expandSize;
|
||||
topBounds -= expandSize;
|
||||
bottomBounds += expandSize;
|
||||
}
|
||||
|
||||
// Draw the example drawable on top of the text.
|
||||
Drawable drawable = mPressed ? mPressedDrawable : mUnpressedDrawable;
|
||||
if (drawable != null) {
|
||||
drawable.setBounds(leftBounds, topBounds, rightBounds, bottomBounds);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPressed() {
|
||||
return mPressed;
|
||||
}
|
||||
|
||||
public void setPressed(boolean pressed) {
|
||||
if (pressed == mPressed)
|
||||
return;
|
||||
|
||||
mPressed = pressed;
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
|
||||
if (mHapticFeedback) {
|
||||
performHapticFeedback(pressed ? HapticFeedbackConstants.VIRTUAL_KEY : HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setButtonCode(int controllerIndex, int code) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mButtonCode = code;
|
||||
}
|
||||
|
||||
public void setAutoFireSlot(int controllerIndex, int slot) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mAutoFireSlot = slot;
|
||||
}
|
||||
|
||||
public void setHotkey(Hotkey hotkey) {
|
||||
mHotkey = hotkey;
|
||||
}
|
||||
|
||||
public String getConfigName() {
|
||||
return mConfigName;
|
||||
}
|
||||
public void setConfigName(String name) {
|
||||
mConfigName = name;
|
||||
}
|
||||
|
||||
public boolean getIsGlidable() { return mIsGlidable; }
|
||||
public void setIsGlidable(boolean isGlidable) { mIsGlidable = isGlidable; }
|
||||
|
||||
public boolean getDefaultVisibility() { return mDefaultVisibility; }
|
||||
public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; }
|
||||
|
||||
public void setHapticFeedback(boolean enabled) {
|
||||
mHapticFeedback = enabled;
|
||||
}
|
||||
|
||||
private void updateControllerState() {
|
||||
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||
if (mButtonCode >= 0)
|
||||
hi.setControllerButtonState(mControllerIndex, mButtonCode, mPressed);
|
||||
if (mAutoFireSlot >= 0)
|
||||
hi.setControllerAutoFireState(mControllerIndex, mAutoFireSlot, mPressed);
|
||||
|
||||
switch (mHotkey)
|
||||
{
|
||||
case FAST_FORWARD: {
|
||||
hi.setFastForwardEnabled(mPressed);
|
||||
}
|
||||
break;
|
||||
|
||||
case ANALOG_TOGGLE: {
|
||||
if (!mPressed)
|
||||
hi.toggleControllerAnalogMode();
|
||||
}
|
||||
break;
|
||||
|
||||
case OPEN_PAUSE_MENU: {
|
||||
if (!mPressed)
|
||||
hi.getEmulationActivity().openPauseMenu();
|
||||
}
|
||||
break;
|
||||
|
||||
case QUICK_LOAD: {
|
||||
if (!mPressed)
|
||||
hi.loadState(false, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case QUICK_SAVE: {
|
||||
if (!mPressed)
|
||||
hi.saveState(false, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getPressedDrawable() {
|
||||
return mPressedDrawable;
|
||||
}
|
||||
|
||||
public void setPressedDrawable(Drawable pressedDrawable) {
|
||||
mPressedDrawable = pressedDrawable;
|
||||
}
|
||||
|
||||
public Drawable getUnpressedDrawable() {
|
||||
return mUnpressedDrawable;
|
||||
}
|
||||
|
||||
public void setUnpressedDrawable(Drawable unpressedDrawable) {
|
||||
mUnpressedDrawable = unpressedDrawable;
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
public final class TouchscreenControllerDPadView extends View {
|
||||
private static final int NUM_DIRECTIONS = 4;
|
||||
private static final int NUM_POSITIONS = 8;
|
||||
private static final int DIRECTION_UP = 0;
|
||||
private static final int DIRECTION_RIGHT = 1;
|
||||
private static final int DIRECTION_DOWN = 2;
|
||||
private static final int DIRECTION_LEFT = 3;
|
||||
|
||||
private final Drawable[] mUnpressedDrawables = new Drawable[NUM_DIRECTIONS];
|
||||
private final Drawable[] mPressedDrawables = new Drawable[NUM_DIRECTIONS];
|
||||
private final int[] mDirectionCodes = new int[] { -1, -1, -1, -1 };
|
||||
private final boolean[] mDirectionStates = new boolean[NUM_DIRECTIONS];
|
||||
|
||||
private boolean mPressed = false;
|
||||
private int mPointerId = 0;
|
||||
private int mPointerX = 0;
|
||||
private int mPointerY = 0;
|
||||
|
||||
private String mConfigName;
|
||||
private boolean mDefaultVisibility = true;
|
||||
|
||||
private int mControllerIndex = -1;
|
||||
|
||||
private static final int[][] DRAWABLES = {
|
||||
{R.drawable.ic_controller_up_button,R.drawable.ic_controller_up_button_pressed},
|
||||
{R.drawable.ic_controller_right_button,R.drawable.ic_controller_right_button_pressed},
|
||||
{R.drawable.ic_controller_down_button,R.drawable.ic_controller_down_button_pressed},
|
||||
{R.drawable.ic_controller_left_button,R.drawable.ic_controller_left_button_pressed},
|
||||
};
|
||||
|
||||
|
||||
public TouchscreenControllerDPadView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerDPadView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerDPadView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
for (int i = 0; i < NUM_DIRECTIONS; i++) {
|
||||
mUnpressedDrawables[i] = getContext().getDrawable(DRAWABLES[i][0]);
|
||||
mPressedDrawables[i] = getContext().getDrawable(DRAWABLES[i][1]);
|
||||
}
|
||||
}
|
||||
|
||||
public String getConfigName() {
|
||||
return mConfigName;
|
||||
}
|
||||
public void setConfigName(String configName) {
|
||||
mConfigName = configName;
|
||||
}
|
||||
|
||||
public boolean getDefaultVisibility() { return mDefaultVisibility; }
|
||||
public void setDefaultVisibility(boolean visibility) { mDefaultVisibility = visibility; }
|
||||
|
||||
public void setControllerButtons(int controllerIndex, int leftCode, int rightCode, int upCode, int downCode) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mDirectionCodes[DIRECTION_LEFT] = leftCode;
|
||||
mDirectionCodes[DIRECTION_RIGHT] = rightCode;
|
||||
mDirectionCodes[DIRECTION_UP] = upCode;
|
||||
mDirectionCodes[DIRECTION_DOWN] = downCode;
|
||||
}
|
||||
|
||||
public void setUnpressed() {
|
||||
if (!mPressed && mPointerX == 0 && mPointerY == 0)
|
||||
return;
|
||||
|
||||
mPressed = false;
|
||||
mPointerX = 0;
|
||||
mPointerY = 0;
|
||||
updateControllerState();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setPressed(int pointerId, int pointerX, int pointerY) {
|
||||
final int posX = (int)(pointerX / getScaleX());
|
||||
final int posY = (int)(pointerY / getScaleY());
|
||||
|
||||
boolean doUpdate = (pointerId != mPointerId || !mPressed || (posX != mPointerX || posY != mPointerY));
|
||||
mPointerId = pointerId;
|
||||
mPointerX = posX;
|
||||
mPointerY = posY;
|
||||
mPressed = true;
|
||||
|
||||
if (doUpdate) {
|
||||
updateControllerState();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateControllerState() {
|
||||
final int subX = mPointerX / (getWidth() / 3);
|
||||
final int subY = mPointerY / (getHeight() / 3);
|
||||
|
||||
mDirectionStates[DIRECTION_UP] = (mPressed && subY == 0);
|
||||
mDirectionStates[DIRECTION_RIGHT] = (mPressed && subX == 2);
|
||||
mDirectionStates[DIRECTION_DOWN] = (mPressed && subY == 2);
|
||||
mDirectionStates[DIRECTION_LEFT] = (mPressed && subX == 0);
|
||||
|
||||
AndroidHostInterface hostInterface = AndroidHostInterface.getInstance();
|
||||
for (int i = 0; i < NUM_DIRECTIONS; i++) {
|
||||
if (mDirectionCodes[i] >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mDirectionCodes[i], mDirectionStates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawDirection(int direction, int subX, int subY, Canvas canvas, int buttonWidth, int buttonHeight) {
|
||||
int leftBounds = subX * buttonWidth;
|
||||
int rightBounds = leftBounds + buttonWidth;
|
||||
int topBounds = subY * buttonHeight;
|
||||
int bottomBounds = topBounds + buttonHeight;
|
||||
|
||||
if (mDirectionStates[direction]) {
|
||||
final int expandSize = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
10.0f, getResources().getDisplayMetrics());
|
||||
leftBounds -= expandSize;
|
||||
rightBounds += expandSize;
|
||||
topBounds -= expandSize;
|
||||
bottomBounds += expandSize;
|
||||
}
|
||||
|
||||
final Drawable drawable = mDirectionStates[direction] ? mPressedDrawables[direction] : mUnpressedDrawables[direction];
|
||||
drawable.setBounds(leftBounds, topBounds, rightBounds, bottomBounds);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
|
||||
// Divide it into thirds - draw between.
|
||||
final int buttonWidth = width / 3;
|
||||
final int buttonHeight = height / 3;
|
||||
|
||||
drawDirection(DIRECTION_UP, 1, 0, canvas, buttonWidth, buttonHeight);
|
||||
drawDirection(DIRECTION_RIGHT, 2, 1, canvas, buttonWidth, buttonHeight);
|
||||
drawDirection(DIRECTION_DOWN, 1, 2, canvas, buttonWidth, buttonHeight);
|
||||
drawDirection(DIRECTION_LEFT, 0, 1, canvas, buttonWidth, buttonHeight);
|
||||
}
|
||||
|
||||
public boolean isPressed() {
|
||||
return mPressed;
|
||||
}
|
||||
|
||||
public boolean hasPointerId() {
|
||||
return mPointerId >= 0;
|
||||
}
|
||||
|
||||
public int getPointerId() {
|
||||
return mPointerId;
|
||||
}
|
||||
|
||||
public void setPointerId(int mPointerId) {
|
||||
this.mPointerId = mPointerId;
|
||||
}
|
||||
}
|
|
@ -1,858 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* TODO: document your custom view class.
|
||||
*/
|
||||
public class TouchscreenControllerView extends FrameLayout {
|
||||
public static final int DEFAULT_OPACITY = 100;
|
||||
|
||||
public static final float MIN_VIEW_SCALE = 0.25f;
|
||||
public static final float MAX_VIEW_SCALE = 10.0f;
|
||||
|
||||
public enum EditMode {
|
||||
NONE,
|
||||
POSITION,
|
||||
SCALE
|
||||
}
|
||||
|
||||
private int mControllerIndex;
|
||||
private String mControllerType;
|
||||
private String mViewType;
|
||||
private View mMainView;
|
||||
private ArrayList<TouchscreenControllerButtonView> mButtonViews = new ArrayList<>();
|
||||
private ArrayList<TouchscreenControllerAxisView> mAxisViews = new ArrayList<>();
|
||||
private TouchscreenControllerDPadView mDPadView = null;
|
||||
private int mPointerButtonCode = -1;
|
||||
private int mPointerPointerId = -1;
|
||||
private boolean mHapticFeedback;
|
||||
private String mLayoutOrientation;
|
||||
private EditMode mEditMode = EditMode.NONE;
|
||||
private View mMovingView = null;
|
||||
private String mMovingName = null;
|
||||
private float mMovingLastX = 0.0f;
|
||||
private float mMovingLastY = 0.0f;
|
||||
private float mMovingLastScale = 0.0f;
|
||||
private ConstraintLayout mEditLayout = null;
|
||||
private int mOpacity = 100;
|
||||
private Map<Integer, View> mGlidePairs = new HashMap<>();
|
||||
|
||||
public TouchscreenControllerView(Context context) {
|
||||
super(context);
|
||||
setFocusable(false);
|
||||
setFocusableInTouchMode(false);
|
||||
}
|
||||
|
||||
public TouchscreenControllerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TouchscreenControllerView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private String getConfigKeyForXTranslation(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sXTranslation", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private String getConfigKeyForYTranslation(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sYTranslation", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private String getConfigKeyForScale(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sScale", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private String getConfigKeyForVisibility(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sVisible", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private void saveSettingsForButton(String name, View view) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putFloat(getConfigKeyForXTranslation(name), view.getTranslationX());
|
||||
editor.putFloat(getConfigKeyForYTranslation(name), view.getTranslationY());
|
||||
editor.putFloat(getConfigKeyForScale(name), view.getScaleX());
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void saveVisibilityForButton(String name, boolean visible) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(getConfigKeyForVisibility(name), visible);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void clearTranslationForAllButtons() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
editor.remove(getConfigKeyForXTranslation(buttonView.getConfigName()));
|
||||
editor.remove(getConfigKeyForYTranslation(buttonView.getConfigName()));
|
||||
editor.remove(getConfigKeyForScale(buttonView.getConfigName()));
|
||||
buttonView.setTranslationX(0.0f);
|
||||
buttonView.setTranslationY(0.0f);
|
||||
buttonView.setScaleX(1.0f);
|
||||
buttonView.setScaleY(1.0f);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
editor.remove(getConfigKeyForXTranslation(axisView.getConfigName()));
|
||||
editor.remove(getConfigKeyForYTranslation(axisView.getConfigName()));
|
||||
editor.remove(getConfigKeyForScale(axisView.getConfigName()));
|
||||
axisView.setTranslationX(0.0f);
|
||||
axisView.setTranslationY(0.0f);
|
||||
axisView.setScaleX(1.0f);
|
||||
axisView.setScaleY(1.0f);
|
||||
}
|
||||
|
||||
if (mDPadView != null) {
|
||||
editor.remove(getConfigKeyForXTranslation(mDPadView.getConfigName()));
|
||||
editor.remove(getConfigKeyForYTranslation(mDPadView.getConfigName()));
|
||||
editor.remove(getConfigKeyForScale(mDPadView.getConfigName()));
|
||||
mDPadView.setTranslationX(0.0f);
|
||||
mDPadView.setTranslationY(0.0f);
|
||||
mDPadView.setScaleX(1.0f);
|
||||
mDPadView.setScaleY(1.0f);
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void reloadButtonSettings() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
try {
|
||||
buttonView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(buttonView.getConfigName()), 0.0f));
|
||||
buttonView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(buttonView.getConfigName()), 0.0f));
|
||||
buttonView.setScaleX(prefs.getFloat(getConfigKeyForScale(buttonView.getConfigName()), 1.0f));
|
||||
buttonView.setScaleY(prefs.getFloat(getConfigKeyForScale(buttonView.getConfigName()), 1.0f));
|
||||
//Log.i("TouchscreenController", String.format("Translation for %s %f %f", buttonView.getConfigName(),
|
||||
// buttonView.getTranslationX(), buttonView.getTranslationY()));
|
||||
|
||||
final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(buttonView.getConfigName()), buttonView.getDefaultVisibility());
|
||||
buttonView.setVisibility(visible ? VISIBLE : INVISIBLE);
|
||||
} catch (ClassCastException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
try {
|
||||
axisView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(axisView.getConfigName()), 0.0f));
|
||||
axisView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(axisView.getConfigName()), 0.0f));
|
||||
axisView.setScaleX(prefs.getFloat(getConfigKeyForScale(axisView.getConfigName()), 1.0f));
|
||||
axisView.setScaleY(prefs.getFloat(getConfigKeyForScale(axisView.getConfigName()), 1.0f));
|
||||
|
||||
final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(axisView.getConfigName()), axisView.getDefaultVisibility());
|
||||
axisView.setVisibility(visible ? VISIBLE : INVISIBLE);
|
||||
} catch (ClassCastException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (mDPadView != null) {
|
||||
try {
|
||||
mDPadView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(mDPadView.getConfigName()), 0.0f));
|
||||
mDPadView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(mDPadView.getConfigName()), 0.0f));
|
||||
mDPadView.setScaleX(prefs.getFloat(getConfigKeyForScale(mDPadView.getConfigName()), 1.0f));
|
||||
mDPadView.setScaleY(prefs.getFloat(getConfigKeyForScale(mDPadView.getConfigName()), 1.0f));
|
||||
|
||||
final boolean visible = prefs.getBoolean(getConfigKeyForVisibility(mDPadView.getConfigName()), mDPadView.getDefaultVisibility());
|
||||
mDPadView.setVisibility(visible ? VISIBLE : INVISIBLE);
|
||||
} catch (ClassCastException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setOpacity(int opacity) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt("TouchscreenController/Opacity", opacity);
|
||||
editor.commit();
|
||||
|
||||
updateOpacity();
|
||||
}
|
||||
|
||||
private void updateOpacity() {
|
||||
mOpacity = PreferenceManager.getDefaultSharedPreferences(getContext()).getInt("TouchscreenController/Opacity", DEFAULT_OPACITY);
|
||||
|
||||
float alpha = (float)mOpacity / 100.0f;
|
||||
alpha = (alpha < 0.0f) ? 0.0f : ((alpha > 1.0f) ? 1.0f : alpha);
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.setAlpha(alpha);
|
||||
}
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.setAlpha(alpha);
|
||||
}
|
||||
if (mDPadView != null)
|
||||
mDPadView.setAlpha(alpha);
|
||||
}
|
||||
|
||||
private String getOrientationString() {
|
||||
switch (getContext().getResources().getConfiguration().orientation) {
|
||||
case Configuration.ORIENTATION_PORTRAIT:
|
||||
return "Portrait";
|
||||
case Configuration.ORIENTATION_LANDSCAPE:
|
||||
default:
|
||||
return "Landscape";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the orientation of the layout has changed, and if so, reloads button translations.
|
||||
*/
|
||||
public void updateOrientation() {
|
||||
String newOrientation = getOrientationString();
|
||||
if (mLayoutOrientation != null && mLayoutOrientation.equals(newOrientation))
|
||||
return;
|
||||
|
||||
Log.i("TouchscreenController", "New orientation: " + newOrientation);
|
||||
mLayoutOrientation = newOrientation;
|
||||
reloadButtonSettings();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void init(int controllerIndex, String controllerType, String viewType, boolean hapticFeedback, boolean gliding) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mControllerType = controllerType;
|
||||
mViewType = viewType;
|
||||
mHapticFeedback = hapticFeedback;
|
||||
mLayoutOrientation = getOrientationString();
|
||||
|
||||
if (mEditMode != EditMode.NONE)
|
||||
endLayoutEditing();
|
||||
|
||||
mButtonViews.clear();
|
||||
mAxisViews.clear();
|
||||
removeAllViews();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
String pointerButtonName = null;
|
||||
switch (viewType) {
|
||||
case "digital":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_digital, this, true);
|
||||
break;
|
||||
|
||||
case "analog_stick":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_stick, this, true);
|
||||
break;
|
||||
|
||||
case "analog_sticks":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_sticks, this, true);
|
||||
break;
|
||||
|
||||
case "lightgun":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_lightgun, this, true);
|
||||
pointerButtonName = "Trigger";
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
mMainView = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (mMainView == null)
|
||||
return;
|
||||
|
||||
mMainView.setOnTouchListener((view1, event) -> {
|
||||
if (mEditMode != EditMode.NONE)
|
||||
return handleEditingTouchEvent(event);
|
||||
else
|
||||
return handleTouchEvent(event);
|
||||
});
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
linkDPadToButtons(mMainView, R.id.controller_dpad, "DPad", "", true);
|
||||
linkButton(mMainView, R.id.controller_button_l1, "L1Button", "L1", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_l2, "L2Button", "L2", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_select, "SelectButton", "Select", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_start, "StartButton", "Start", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_triangle, "TriangleButton", "Triangle", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_circle, "CircleButton", "Circle", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_cross, "CrossButton", "Cross", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_square, "SquareButton", "Square", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_r1, "R1Button", "R1", true, gliding);
|
||||
linkButton(mMainView, R.id.controller_button_r2, "R2Button", "R2", true, gliding);
|
||||
|
||||
if (!linkAxis(mMainView, R.id.controller_axis_left, "LeftAxis", "Left", true))
|
||||
linkAxisToButtons(mMainView, R.id.controller_axis_left, "LeftAxis", "");
|
||||
|
||||
linkAxis(mMainView, R.id.controller_axis_right, "RightAxis", "Right", true);
|
||||
|
||||
// GunCon
|
||||
linkButton(mMainView, R.id.controller_button_a, "AButton", "A", true, true);
|
||||
linkButton(mMainView, R.id.controller_button_b, "BButton", "B", true, true);
|
||||
if (pointerButtonName != null)
|
||||
linkPointer(pointerButtonName);
|
||||
|
||||
// Turbo/autofire buttons
|
||||
linkAutoFireButton(mMainView, R.id.controller_button_autofire_1, "AutoFire1", 0, false);
|
||||
linkAutoFireButton(mMainView, R.id.controller_button_autofire_2, "AutoFire2", 1, false);
|
||||
linkAutoFireButton(mMainView, R.id.controller_button_autofire_3, "AutoFire3", 2, false);
|
||||
linkAutoFireButton(mMainView, R.id.controller_button_autofire_4, "AutoFire4", 3, false);
|
||||
|
||||
// Hotkeys
|
||||
linkHotkeyButton(mMainView, R.id.controller_button_fast_forward, "FastForward",
|
||||
TouchscreenControllerButtonView.Hotkey.FAST_FORWARD, false);
|
||||
linkHotkeyButton(mMainView, R.id.controller_button_analog, "AnalogToggle",
|
||||
TouchscreenControllerButtonView.Hotkey.ANALOG_TOGGLE, false);
|
||||
linkHotkeyButton(mMainView, R.id.controller_button_pause, "OpenPauseMenu",
|
||||
TouchscreenControllerButtonView.Hotkey.OPEN_PAUSE_MENU, true);
|
||||
linkHotkeyButton(mMainView, R.id.controller_button_quick_load, "QuickLoad",
|
||||
TouchscreenControllerButtonView.Hotkey.QUICK_LOAD, false);
|
||||
linkHotkeyButton(mMainView, R.id.controller_button_quick_save, "QuickSave",
|
||||
TouchscreenControllerButtonView.Hotkey.QUICK_SAVE, false);
|
||||
|
||||
reloadButtonSettings();
|
||||
updateOpacity();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void linkButton(View view, int id, String configName, String buttonName, boolean defaultVisibility, boolean isGlidable) {
|
||||
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id);
|
||||
if (buttonView == null)
|
||||
return;
|
||||
|
||||
buttonView.setConfigName(configName);
|
||||
buttonView.setDefaultVisibility(defaultVisibility);
|
||||
buttonView.setIsGlidable(isGlidable);
|
||||
mButtonViews.add(buttonView);
|
||||
|
||||
int code = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonName);
|
||||
Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code));
|
||||
|
||||
if (code >= 0) {
|
||||
buttonView.setButtonCode(mControllerIndex, code);
|
||||
buttonView.setHapticFeedback(mHapticFeedback);
|
||||
} else {
|
||||
Log.e("TouchscreenController", String.format("Unknown button name '%s' " +
|
||||
"for '%s'", buttonName, mControllerType));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean linkAxis(View view, int id, String configName, String axisName, boolean defaultVisibility) {
|
||||
TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id);
|
||||
if (axisView == null)
|
||||
return false;
|
||||
|
||||
axisView.setConfigName(configName);
|
||||
axisView.setDefaultVisibility(defaultVisibility);
|
||||
mAxisViews.add(axisView);
|
||||
|
||||
int xCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "X");
|
||||
int yCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "Y");
|
||||
Log.i("TouchscreenController", String.format("%s -> %d/%d", axisName, xCode, yCode));
|
||||
if (xCode < 0 && yCode < 0)
|
||||
return false;
|
||||
|
||||
axisView.setControllerAxis(mControllerIndex, xCode, yCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean linkAxisToButtons(View view, int id, String configName, String buttonPrefix) {
|
||||
TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id);
|
||||
if (axisView == null)
|
||||
return false;
|
||||
|
||||
int leftCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Left");
|
||||
int rightCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Right");
|
||||
int upCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Up");
|
||||
int downCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Down");
|
||||
Log.i("TouchscreenController", String.format("%s(ButtonAxis) -> %d,%d,%d,%d", buttonPrefix, leftCode, rightCode, upCode, downCode));
|
||||
if (leftCode < 0 && rightCode < 0 && upCode < 0 && downCode < 0)
|
||||
return false;
|
||||
|
||||
axisView.setControllerButtons(mControllerIndex, leftCode, rightCode, upCode, downCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean linkDPadToButtons(View view, int id, String configName, String buttonPrefix, boolean defaultVisibility) {
|
||||
TouchscreenControllerDPadView dpadView = (TouchscreenControllerDPadView) view.findViewById(id);
|
||||
if (dpadView == null)
|
||||
return false;
|
||||
|
||||
dpadView.setConfigName(configName);
|
||||
dpadView.setDefaultVisibility(defaultVisibility);
|
||||
mDPadView = dpadView;
|
||||
|
||||
int leftCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Left");
|
||||
int rightCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Right");
|
||||
int upCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Up");
|
||||
int downCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Down");
|
||||
Log.i("TouchscreenController", String.format("%s(DPad) -> %d,%d,%d,%d", buttonPrefix, leftCode, rightCode, upCode, downCode));
|
||||
if (leftCode < 0 && rightCode < 0 && upCode < 0 && downCode < 0)
|
||||
return false;
|
||||
|
||||
dpadView.setControllerButtons(mControllerIndex, leftCode, rightCode, upCode, downCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void linkHotkeyButton(View view, int id, String configName, TouchscreenControllerButtonView.Hotkey hotkey, boolean defaultVisibility) {
|
||||
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id);
|
||||
if (buttonView == null)
|
||||
return;
|
||||
|
||||
buttonView.setConfigName(configName);
|
||||
buttonView.setDefaultVisibility(defaultVisibility);
|
||||
buttonView.setHotkey(hotkey);
|
||||
buttonView.setIsGlidable(false);
|
||||
mButtonViews.add(buttonView);
|
||||
}
|
||||
|
||||
private void linkAutoFireButton(View view, int id, String configName, int slot, boolean defaultVisibility) {
|
||||
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id);
|
||||
if (buttonView == null)
|
||||
return;
|
||||
|
||||
buttonView.setConfigName(configName);
|
||||
buttonView.setDefaultVisibility(defaultVisibility);
|
||||
buttonView.setAutoFireSlot(mControllerIndex, slot);
|
||||
buttonView.setIsGlidable(true);
|
||||
mButtonViews.add(buttonView);
|
||||
}
|
||||
|
||||
private boolean linkPointer(String buttonName) {
|
||||
mPointerButtonCode = AndroidHostInterface.getInstance().getControllerButtonCode(mControllerType, buttonName);
|
||||
Log.i("TouchscreenController", String.format("Pointer -> %s,%d", buttonName, mPointerButtonCode));
|
||||
return (mPointerButtonCode >= 0);
|
||||
}
|
||||
|
||||
private int dpToPixels(float dp) {
|
||||
return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()));
|
||||
}
|
||||
|
||||
public void startLayoutEditing(EditMode mode) {
|
||||
if (mEditLayout == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
mEditLayout = (ConstraintLayout) inflater.inflate(R.layout.layout_touchscreen_controller_edit, this, false);
|
||||
((Button) mEditLayout.findViewById(R.id.options)).setOnClickListener((view) -> showEditorMenu());
|
||||
addView(mEditLayout);
|
||||
}
|
||||
|
||||
mEditMode = mode;
|
||||
}
|
||||
|
||||
public void endLayoutEditing() {
|
||||
if (mEditLayout != null) {
|
||||
((ViewGroup) mMainView).removeView(mEditLayout);
|
||||
mEditLayout = null;
|
||||
}
|
||||
|
||||
mEditMode = EditMode.NONE;
|
||||
mMovingView = null;
|
||||
mMovingName = null;
|
||||
mMovingLastX = 0.0f;
|
||||
mMovingLastY = 0.0f;
|
||||
|
||||
// unpause if we're paused (from the setting)
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadPaused())
|
||||
AndroidHostInterface.getInstance().pauseEmulationThread(false);
|
||||
}
|
||||
|
||||
private float snapToValue(float pos, float value) {
|
||||
return Math.round(pos / value) * value;
|
||||
}
|
||||
|
||||
private float snapToGrid(float pos) {
|
||||
final float value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20.0f, getResources().getDisplayMetrics());
|
||||
return snapToValue(pos, value);
|
||||
}
|
||||
|
||||
private boolean handleEditingTouchEvent(MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (mMovingView != null) {
|
||||
// save position
|
||||
saveSettingsForButton(mMovingName, mMovingView);
|
||||
mMovingView = null;
|
||||
mMovingName = null;
|
||||
mMovingLastX = 0.0f;
|
||||
mMovingLastY = 0.0f;
|
||||
mMovingLastScale = 0.0f;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
if (mMovingView != null) {
|
||||
// already moving a button
|
||||
return true;
|
||||
}
|
||||
|
||||
Rect rect = new Rect();
|
||||
final float x = event.getX();
|
||||
final float y = event.getY();
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.getHitRect(rect);
|
||||
if (rect.contains((int) x, (int) y)) {
|
||||
mMovingView = buttonView;
|
||||
mMovingName = buttonView.getConfigName();
|
||||
mMovingLastX = snapToGrid(x);
|
||||
mMovingLastY = snapToGrid(y);
|
||||
mMovingLastScale = buttonView.getScaleX();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.getHitRect(rect);
|
||||
if (rect.contains((int) x, (int) y)) {
|
||||
mMovingView = axisView;
|
||||
mMovingName = axisView.getConfigName();
|
||||
mMovingLastX = snapToGrid(x);
|
||||
mMovingLastY = snapToGrid(y);
|
||||
mMovingLastScale = axisView.getScaleX();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mDPadView != null) {
|
||||
mDPadView.getHitRect(rect);
|
||||
if (rect.contains((int) x, (int) y)) {
|
||||
mMovingView = mDPadView;
|
||||
mMovingName = mDPadView.getConfigName();
|
||||
mMovingLastX = snapToGrid(x);
|
||||
mMovingLastY = snapToGrid(y);
|
||||
mMovingLastScale = mDPadView.getScaleX();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// nothing..
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mMovingView == null)
|
||||
return true;
|
||||
|
||||
final float x = snapToGrid(event.getX());
|
||||
final float y = snapToGrid(event.getY());
|
||||
if (mEditMode == EditMode.POSITION) {
|
||||
final float dx = x - mMovingLastX;
|
||||
final float dy = y - mMovingLastY;
|
||||
mMovingLastX = x;
|
||||
mMovingLastY = y;
|
||||
|
||||
final float posX = mMovingView.getX() + dx;
|
||||
final float posY = mMovingView.getY() + dy;
|
||||
//Log.d("Position", String.format("%f %f -> (%f %f) %f %f",
|
||||
// mMovingView.getX(), mMovingView.getY(), dx, dy, posX, posY));
|
||||
mMovingView.setX(posX);
|
||||
mMovingView.setY(posY);
|
||||
} else {
|
||||
final float lastDx = mMovingLastX - mMovingView.getX();
|
||||
final float lastDy = mMovingLastY - mMovingView.getY();
|
||||
final float dx = x - mMovingView.getX();
|
||||
final float dy = y - mMovingView.getY();
|
||||
final float lastDistance = Math.max(Math.abs(lastDx), Math.abs(lastDy));
|
||||
final float distance = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
final float scaler = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50.0f, getResources().getDisplayMetrics());
|
||||
final float scaleDiff = snapToValue((distance - lastDistance) / scaler, 0.1f);
|
||||
final float scale = Math.max(Math.min(mMovingLastScale + mMovingLastScale * scaleDiff, MAX_VIEW_SCALE), MIN_VIEW_SCALE);
|
||||
mMovingView.setScaleX(scale);
|
||||
mMovingView.setScaleY(scale);
|
||||
}
|
||||
|
||||
mMovingView.invalidate();
|
||||
mMainView.requestLayout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean updateTouchButtonsFromEvent(MotionEvent event) {
|
||||
if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning())
|
||||
return false;
|
||||
|
||||
Rect rect = new Rect();
|
||||
final int actionMasked = event.getActionMasked();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
final int liftedPointerIndex = (actionMasked == MotionEvent.ACTION_POINTER_UP) ? event.getActionIndex() : -1;
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
if (buttonView.getVisibility() != VISIBLE)
|
||||
continue;
|
||||
|
||||
buttonView.getHitRect(rect);
|
||||
boolean pressed = false;
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
if (i == liftedPointerIndex)
|
||||
continue;
|
||||
|
||||
final int x = (int) event.getX(i);
|
||||
final int y = (int) event.getY(i);
|
||||
if (rect.contains(x, y)) {
|
||||
buttonView.setPressed(true);
|
||||
final int pointerId = event.getPointerId(i);
|
||||
if (!mGlidePairs.containsKey(pointerId) && !mGlidePairs.containsValue(buttonView)) {
|
||||
if (buttonView.getIsGlidable())
|
||||
mGlidePairs.put(pointerId, buttonView);
|
||||
else { mGlidePairs.put(pointerId, null); }
|
||||
}
|
||||
pressed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pressed && !mGlidePairs.containsValue(buttonView))
|
||||
buttonView.setPressed(pressed);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
if (axisView.getVisibility() != VISIBLE)
|
||||
continue;
|
||||
|
||||
axisView.getHitRect(rect);
|
||||
boolean pressed = false;
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
if (i == liftedPointerIndex)
|
||||
continue;
|
||||
|
||||
final int pointerId = event.getPointerId(i);
|
||||
final int x = (int) event.getX(i);
|
||||
final int y = (int) event.getY(i);
|
||||
|
||||
if ((rect.contains(x, y) && !axisView.isPressed()) ||
|
||||
(axisView.isPressed() && axisView.getPointerId() == pointerId)) {
|
||||
axisView.setPressed(pointerId, x - rect.left, y - rect.top);
|
||||
pressed = true;
|
||||
mGlidePairs.put(pointerId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pressed)
|
||||
axisView.setUnpressed();
|
||||
}
|
||||
|
||||
if (mDPadView != null && mDPadView.getVisibility() == VISIBLE) {
|
||||
mDPadView.getHitRect(rect);
|
||||
|
||||
boolean pressed = false;
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
if (i == liftedPointerIndex)
|
||||
continue;
|
||||
|
||||
final int x = (int) event.getX(i);
|
||||
final int y = (int) event.getY(i);
|
||||
if (rect.contains(x, y)) {
|
||||
mDPadView.setPressed(event.getPointerId(i), x - rect.left, y - rect.top);
|
||||
pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pressed)
|
||||
mDPadView.setUnpressed();
|
||||
}
|
||||
|
||||
if (mPointerButtonCode >= 0) {
|
||||
final int pointerIndex = event.getActionIndex();
|
||||
final int pointerId = event.getPointerId(pointerIndex);
|
||||
if (mPointerPointerId < 0 && (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
|
||||
if (!mGlidePairs.containsKey(pointerId)) {
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex,
|
||||
mPointerButtonCode, true);
|
||||
mPointerPointerId = pointerId;
|
||||
}
|
||||
} else if (actionMasked == MotionEvent.ACTION_POINTER_UP) {
|
||||
if (pointerId == mPointerPointerId) {
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex,
|
||||
mPointerButtonCode, false);
|
||||
mPointerPointerId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
AndroidHostInterface.getInstance().setMousePosition(
|
||||
(int) event.getX(pointerIndex),
|
||||
(int) event.getY(pointerIndex));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleTouchEvent(MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning())
|
||||
return false;
|
||||
|
||||
mGlidePairs.clear();
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.setPressed(false);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.setUnpressed();
|
||||
}
|
||||
|
||||
if (mDPadView != null)
|
||||
mDPadView.setUnpressed();
|
||||
|
||||
if (mPointerPointerId >= 0) {
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(
|
||||
mControllerIndex, mPointerButtonCode, false);
|
||||
mPointerPointerId = -1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
final int pointerId = event.getPointerId(event.getActionIndex());
|
||||
if (mGlidePairs.containsKey(pointerId))
|
||||
mGlidePairs.remove(pointerId);
|
||||
|
||||
return updateTouchButtonsFromEvent(event);
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
return updateTouchButtonsFromEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public AlertDialog.Builder createAddRemoveButtonDialog(Context context) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final CharSequence[] items = new CharSequence[mButtonViews.size() + mAxisViews.size()];
|
||||
final boolean[] itemsChecked = new boolean[mButtonViews.size() + mAxisViews.size()];
|
||||
int itemCount = 0;
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
items[itemCount] = buttonView.getConfigName();
|
||||
itemsChecked[itemCount] = buttonView.getVisibility() == VISIBLE;
|
||||
itemCount++;
|
||||
}
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
items[itemCount] = axisView.getConfigName();
|
||||
itemsChecked[itemCount] = axisView.getVisibility() == VISIBLE;
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
builder.setTitle(R.string.dialog_touchscreen_controller_buttons);
|
||||
builder.setMultiChoiceItems(items, itemsChecked, (dialog, which, isChecked) -> {
|
||||
if (which < mButtonViews.size()) {
|
||||
TouchscreenControllerButtonView buttonView = mButtonViews.get(which);
|
||||
buttonView.setVisibility(isChecked ? VISIBLE : INVISIBLE);
|
||||
saveVisibilityForButton(buttonView.getConfigName(), isChecked);
|
||||
} else {
|
||||
TouchscreenControllerAxisView axisView = mAxisViews.get(which - mButtonViews.size());
|
||||
axisView.setVisibility(isChecked ? VISIBLE : INVISIBLE);
|
||||
saveVisibilityForButton(axisView.getConfigName(), isChecked);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.dialog_done, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public AlertDialog.Builder createOpacityDialog(Context context) {
|
||||
final SeekBar seekBar = new SeekBar(context);
|
||||
seekBar.setMax(100);
|
||||
seekBar.setProgress(mOpacity);
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
setOpacity(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.dialog_touchscreen_controller_opacity);
|
||||
builder.setView(seekBar);
|
||||
builder.setNegativeButton(R.string.dialog_done, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void showEditorMenu() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setItems(R.array.touchscreen_layout_menu, (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0: // Change Opacity
|
||||
{
|
||||
AlertDialog.Builder subBuilder = createOpacityDialog(getContext());
|
||||
subBuilder.create().show();
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // Add/Remove Buttons
|
||||
{
|
||||
AlertDialog.Builder subBuilder = createAddRemoveButtonDialog(getContext());
|
||||
subBuilder.create().show();
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Edit Positions
|
||||
{
|
||||
mEditMode = EditMode.POSITION;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // Edit Scale
|
||||
{
|
||||
mEditMode = EditMode.SCALE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 4: // Reset Layout
|
||||
{
|
||||
clearTranslationForAllButtons();
|
||||
}
|
||||
break;
|
||||
|
||||
case 5: // Exit Editor
|
||||
{
|
||||
endLayoutEditing();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Helper class for exposing HTTP downloads to native code without pulling in an external
|
||||
* dependency for doing so.
|
||||
*/
|
||||
public class URLDownloader {
|
||||
private int statusCode = -1;
|
||||
private byte[] data = null;
|
||||
private final String userAgent;
|
||||
|
||||
public URLDownloader(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
private HttpURLConnection getConnection(String url) {
|
||||
try {
|
||||
final URL parsedUrl = new URL(url);
|
||||
HttpURLConnection connection = (HttpURLConnection) parsedUrl.openConnection();
|
||||
if (connection == null)
|
||||
throw new RuntimeException(String.format("openConnection(%s) returned null", url));
|
||||
|
||||
if (userAgent != null)
|
||||
connection.addRequestProperty("User-Agent", userAgent);
|
||||
return connection;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
private boolean download(HttpURLConnection connection) {
|
||||
try {
|
||||
statusCode = connection.getResponseCode();
|
||||
if (statusCode != HttpURLConnection.HTTP_OK)
|
||||
return false;
|
||||
|
||||
final InputStream inStream = new BufferedInputStream(connection.getInputStream());
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final int CHUNK_SIZE = 128 * 1024;
|
||||
final byte[] chunk = new byte[CHUNK_SIZE];
|
||||
int len;
|
||||
while ((len = inStream.read(chunk)) > 0) {
|
||||
outputStream.write(chunk, 0, len);
|
||||
}
|
||||
|
||||
data = outputStream.toByteArray();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean get(String url) {
|
||||
final HttpURLConnection connection = getConnection(url);
|
||||
if (connection == null)
|
||||
return false;
|
||||
|
||||
return download(connection);
|
||||
}
|
||||
|
||||
public boolean post(String url, byte[] postData) {
|
||||
final HttpURLConnection connection = getConnection(url);
|
||||
if (connection == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
connection.setDoOutput(true);
|
||||
connection.setChunkedStreamingMode(0);
|
||||
|
||||
OutputStream postStream = new BufferedOutputStream(connection.getOutputStream());
|
||||
postStream.write(postData);
|
||||
return download(connection);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
public class UpdateNotes {
|
||||
private static final int VERSION_CONTROLLER_UPDATE = 1;
|
||||
private static final int CURRENT_VERSION = VERSION_CONTROLLER_UPDATE;
|
||||
|
||||
private static final String CONFIG_KEY = "Main/UpdateNotesVersion";
|
||||
|
||||
private static int getVersion(MainActivity parent) {
|
||||
try {
|
||||
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(parent);
|
||||
return sp.getInt(CONFIG_KEY, 0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return CURRENT_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setVersion(MainActivity parent, int version) {
|
||||
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(parent);
|
||||
sp.edit().putInt(CONFIG_KEY, version).commit();
|
||||
}
|
||||
|
||||
public static boolean displayUpdateNotes(MainActivity parent) {
|
||||
final int version = getVersion(parent);
|
||||
|
||||
if (version < VERSION_CONTROLLER_UPDATE ) {
|
||||
displayControllerUpdateNotes(parent);
|
||||
setVersion(parent, VERSION_CONTROLLER_UPDATE);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void displayControllerUpdateNotes(MainActivity parent) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setTitle(R.string.update_notes_title);
|
||||
builder.setMessage(R.string.update_notes_message_version_controller_update);
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
Intent intent = new Intent(parent, ControllerSettingsActivity.class);
|
||||
parent.startActivity(intent);
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
Binary file not shown.
Before Width: | Height: | Size: 209 KiB |
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
|
@ -1,71 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_us2.xml -->
|
||||
<vector android:height="15dp"
|
||||
android:viewportHeight="15"
|
||||
android:viewportWidth="21"
|
||||
android:width="21dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFFFFF"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFF0F0F0"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FF043CAE"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FF00339A"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M10.5,3L9.7929,3.2071L10,2.5L9.7929,1.7929L10.5,2L11.2071,1.7929L11,2.5L11.2071,3.2071L10.5,3ZM10.5,13L9.7929,13.2071L10,12.5L9.7929,11.7929L10.5,12L11.2071,11.7929L11,12.5L11.2071,13.2071L10.5,13ZM15.5,8L14.7929,8.2071L15,7.5L14.7929,6.7929L15.5,7L16.2071,6.7929L16,7.5L16.2071,8.2071L15.5,8ZM5.5,8L4.7929,8.2071L5,7.5L4.7929,6.7929L5.5,7L6.2071,6.7929L6,7.5L6.2071,8.2071L5.5,8ZM14.8301,5.5L14.123,5.7071L14.3301,5L14.123,4.2929L14.8301,4.5L15.5372,4.2929L15.3301,5L15.5372,5.7071L14.8301,5.5ZM6.1699,10.5L5.4628,10.7071L5.6699,10L5.4628,9.2929L6.1699,9.5L6.877,9.2929L6.6699,10L6.877,10.7071L6.1699,10.5ZM13,3.6699L12.2929,3.877L12.5,3.1699L12.2929,2.4628L13,2.6699L13.7071,2.4628L13.5,3.1699L13.7071,3.877L13,3.6699ZM8,12.3301L7.2929,12.5372L7.5,11.8301L7.2929,11.123L8,11.3301L8.7071,11.123L8.5,11.8301L8.7071,12.5372L8,12.3301ZM14.8301,10.5L14.123,10.7071L14.3301,10L14.123,9.2929L14.8301,9.5L15.5372,9.2929L15.3301,10L15.5372,10.7071L14.8301,10.5ZM6.1699,5.5L5.4628,5.7071L5.6699,5L5.4628,4.2929L6.1699,4.5L6.877,4.2929L6.6699,5L6.877,5.7071L6.1699,5.5ZM13,12.3301L12.2929,12.5372L12.5,11.8301L12.2929,11.123L13,11.3301L13.7071,11.123L13.5,11.8301L13.7071,12.5372L13,12.3301ZM8,3.6699L7.2929,3.877L7.5,3.1699L7.2929,2.4628L8,2.6699L8.7071,2.4628L8.5,3.1699L8.7071,3.877L8,3.6699Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="13.2071"
|
||||
android:startX="10.5"
|
||||
android:startY="1.7929"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFD429"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFFFCC00"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
|
@ -1,50 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_hp.xml -->
|
||||
<vector android:height="15dp"
|
||||
android:viewportHeight="15"
|
||||
android:viewportWidth="21"
|
||||
android:width="21dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFFFFF"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFF0F0F0"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M10.5,7.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="12"
|
||||
android:startX="10.5"
|
||||
android:startY="3"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFD81441"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFBB0831"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
|
@ -1,539 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_us2.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="21dp"
|
||||
android:height="21dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="13">
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 0h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 1h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 2h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 3h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 4h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 5h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 6h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 7h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 8h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 9h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 10h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 11h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 12h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#192f5d"
|
||||
android:pathData="M0 0h5.2v7h-5.2Z" />
|
||||
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="1.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="2.9">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="4.3">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="5.6">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Odd stars -->
|
||||
|
||||
<group
|
||||
android:translateY="0.7"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="2.1"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="3.6"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="5.0"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
|
@ -1,16 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2l-5.5,9h11z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.5,17.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,13.5h8v8H3z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3 -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.5,4.5c-1.95,0 -4.05,0.4 -5.5,1.5c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.65 0.73,0.45 0.75,0.45C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5c1.35,-0.85 3.8,-1.5 5.5,-1.5c1.65,0 3.35,0.3 4.75,1.05C22.66,21.26 23,20.86 23,20.6V6C21.51,4.88 19.37,4.5 17.5,4.5zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5c-1.7,0 -4.15,0.65 -5.5,1.5V8c1.35,-0.85 3.8,-1.5 5.5,-1.5c1.2,0 2.4,0.15 3.5,0.5V18.5z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue