Remove Android app

This repository exists solely for the desktop version now.
This commit is contained in:
Connor McLaughlin 2021-06-08 16:34:27 +10:00
parent 81da9be2d1
commit 6e49adb508
261 changed files with 0 additions and 22601 deletions

14
android/.gitignore vendored
View File

@ -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

View File

@ -1 +0,0 @@
DuckStation

View File

@ -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>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1 +0,0 @@
/build

View File

@ -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;
}

View File

@ -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

View File

@ -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());
}
}

View File

@ -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()

View File

@ -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;
}

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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

View File

@ -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());
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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{};
};

View File

@ -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() {}

View File

@ -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;
};

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -1,8 +0,0 @@
package com.github.stenzek.duckstation;
public enum ConsoleRegion {
AutoDetect,
NTSC_J,
NTSC_U,
PAL
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -1,8 +0,0 @@
package com.github.stenzek.duckstation;
public enum DiscRegion {
NTSC_J,
NTSC_U,
PAL,
Other
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
});
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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