mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-10-23 07:35:57 +00:00
Merge pull request #1797 from stenzek/android-cheevos
Android: Implement RetroAchievements
This commit is contained in:
commit
cf7b167359
|
@ -69,10 +69,6 @@ if(ANDROID)
|
||||||
message("Building for Android, disabling Discord Presence support")
|
message("Building for Android, disabling Discord Presence support")
|
||||||
set(ENABLE_DISCORD_PRESENCE OFF)
|
set(ENABLE_DISCORD_PRESENCE OFF)
|
||||||
endif()
|
endif()
|
||||||
if(ENABLE_CHEEVOS)
|
|
||||||
message("Building for Android. disabling RetroAchievements support")
|
|
||||||
set(ENABLE_CHEEVOS OFF)
|
|
||||||
endif()
|
|
||||||
if(USE_SDL2)
|
if(USE_SDL2)
|
||||||
message("Building for Android, disabling SDL2 support")
|
message("Building for Android, disabling SDL2 support")
|
||||||
set(USE_SDL2 OFF)
|
set(USE_SDL2 OFF)
|
||||||
|
@ -83,6 +79,9 @@ if(ANDROID)
|
||||||
if(USE_WAYLAND)
|
if(USE_WAYLAND)
|
||||||
set(USE_WAYLAND OFF)
|
set(USE_WAYLAND OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Cheevos are always on.
|
||||||
|
set(ENABLE_CHEEVOS ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,7 +142,7 @@ if(USE_EVDEV)
|
||||||
endif()
|
endif()
|
||||||
if(ENABLE_CHEEVOS)
|
if(ENABLE_CHEEVOS)
|
||||||
message(STATUS "RetroAchievements support enabled")
|
message(STATUS "RetroAchievements support enabled")
|
||||||
if(NOT WIN32)
|
if(NOT WIN32 AND NOT ANDROID)
|
||||||
find_package(CURL REQUIRED)
|
find_package(CURL REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -3,6 +3,8 @@ set(SRCS
|
||||||
android_controller_interface.h
|
android_controller_interface.h
|
||||||
android_host_interface.cpp
|
android_host_interface.cpp
|
||||||
android_host_interface.h
|
android_host_interface.h
|
||||||
|
android_http_downloader.cpp
|
||||||
|
android_http_downloader.h
|
||||||
android_progress_callback.cpp
|
android_progress_callback.cpp
|
||||||
android_progress_callback.h
|
android_progress_callback.h
|
||||||
android_settings_interface.cpp
|
android_settings_interface.cpp
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "frontend-common/controller_interface.h"
|
|
||||||
#include "core/types.h"
|
#include "core/types.h"
|
||||||
|
#include "frontend-common/controller_interface.h"
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
#include "core/gpu.h"
|
#include "core/gpu.h"
|
||||||
#include "core/host_display.h"
|
#include "core/host_display.h"
|
||||||
#include "core/system.h"
|
#include "core/system.h"
|
||||||
|
#include "frontend-common/cheevos.h"
|
||||||
#include "frontend-common/game_list.h"
|
#include "frontend-common/game_list.h"
|
||||||
|
#include "frontend-common/imgui_fullscreen.h"
|
||||||
#include "frontend-common/imgui_styles.h"
|
#include "frontend-common/imgui_styles.h"
|
||||||
#include "frontend-common/opengl_host_display.h"
|
#include "frontend-common/opengl_host_display.h"
|
||||||
#include "frontend-common/vulkan_host_display.h"
|
#include "frontend-common/vulkan_host_display.h"
|
||||||
|
@ -53,8 +55,15 @@ static jclass s_GameListEntry_class;
|
||||||
static jmethodID s_GameListEntry_constructor;
|
static jmethodID s_GameListEntry_constructor;
|
||||||
static jclass s_SaveStateInfo_class;
|
static jclass s_SaveStateInfo_class;
|
||||||
static jmethodID s_SaveStateInfo_constructor;
|
static jmethodID s_SaveStateInfo_constructor;
|
||||||
|
static jclass s_Achievement_class;
|
||||||
|
static jmethodID s_Achievement_constructor;
|
||||||
|
|
||||||
namespace AndroidHelpers {
|
namespace AndroidHelpers {
|
||||||
|
JavaVM* GetJavaVM()
|
||||||
|
{
|
||||||
|
return s_jvm;
|
||||||
|
}
|
||||||
|
|
||||||
// helper for retrieving the current per-thread jni environment
|
// helper for retrieving the current per-thread jni environment
|
||||||
JNIEnv* GetJNIEnv()
|
JNIEnv* GetJNIEnv()
|
||||||
{
|
{
|
||||||
|
@ -440,6 +449,10 @@ void AndroidHostInterface::EmulationThreadLoop(JNIEnv* env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't do a full PollAndUpdate() here
|
||||||
|
if (Cheevos::IsActive())
|
||||||
|
Cheevos::Update();
|
||||||
|
|
||||||
// simulate the system if not paused
|
// simulate the system if not paused
|
||||||
if (System::IsRunning())
|
if (System::IsRunning())
|
||||||
{
|
{
|
||||||
|
@ -593,8 +606,11 @@ void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, in
|
||||||
Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height);
|
Log_InfoPrintf("SurfaceChanged %p %d %d %d", surface, format, width, height);
|
||||||
if (m_surface == surface)
|
if (m_surface == surface)
|
||||||
{
|
{
|
||||||
if (m_display)
|
if (m_display && (width != m_display->GetWindowWidth() || height != m_display->GetWindowHeight()))
|
||||||
|
{
|
||||||
m_display->ResizeRenderWindow(width, height);
|
m_display->ResizeRenderWindow(width, height);
|
||||||
|
OnHostDisplayResized(width, height, m_display->GetWindowScale());
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -611,6 +627,8 @@ void AndroidHostInterface::SurfaceChanged(ANativeWindow* surface, int format, in
|
||||||
wi.surface_scale = m_display->GetWindowScale();
|
wi.surface_scale = m_display->GetWindowScale();
|
||||||
|
|
||||||
m_display->ChangeRenderWindow(wi);
|
m_display->ChangeRenderWindow(wi);
|
||||||
|
if (surface)
|
||||||
|
OnHostDisplayResized(width, height, m_display->GetWindowScale());
|
||||||
|
|
||||||
if (surface && System::GetState() == System::State::Paused)
|
if (surface && System::GetState() == System::State::Paused)
|
||||||
PauseSystem(false);
|
PauseSystem(false);
|
||||||
|
@ -815,7 +833,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
|
|
||||||
// Create global reference so it doesn't get cleaned up.
|
// Create global reference so it doesn't get cleaned up.
|
||||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
jclass string_class, host_interface_class, patch_code_class, game_list_entry_class, save_state_info_class;
|
jclass string_class, host_interface_class, patch_code_class, game_list_entry_class, save_state_info_class,
|
||||||
|
achievement_class;
|
||||||
if ((string_class = env->FindClass("java/lang/String")) == nullptr ||
|
if ((string_class = env->FindClass("java/lang/String")) == nullptr ||
|
||||||
(s_String_class = static_cast<jclass>(env->NewGlobalRef(string_class))) == nullptr ||
|
(s_String_class = static_cast<jclass>(env->NewGlobalRef(string_class))) == nullptr ||
|
||||||
(host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr ||
|
(host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr ||
|
||||||
|
@ -825,7 +844,9 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
(game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr ||
|
(game_list_entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry")) == nullptr ||
|
||||||
(s_GameListEntry_class = static_cast<jclass>(env->NewGlobalRef(game_list_entry_class))) == nullptr ||
|
(s_GameListEntry_class = static_cast<jclass>(env->NewGlobalRef(game_list_entry_class))) == nullptr ||
|
||||||
(save_state_info_class = env->FindClass("com/github/stenzek/duckstation/SaveStateInfo")) == nullptr ||
|
(save_state_info_class = env->FindClass("com/github/stenzek/duckstation/SaveStateInfo")) == nullptr ||
|
||||||
(s_SaveStateInfo_class = static_cast<jclass>(env->NewGlobalRef(save_state_info_class))) == nullptr)
|
(s_SaveStateInfo_class = static_cast<jclass>(env->NewGlobalRef(save_state_info_class))) == nullptr ||
|
||||||
|
(achievement_class = env->FindClass("com/github/stenzek/duckstation/Achievement")) == nullptr ||
|
||||||
|
(s_Achievement_class = static_cast<jclass>(env->NewGlobalRef(achievement_class))) == nullptr)
|
||||||
{
|
{
|
||||||
Log_ErrorPrint("AndroidHostInterface class lookup failed");
|
Log_ErrorPrint("AndroidHostInterface class lookup failed");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -835,6 +856,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
env->DeleteLocalRef(host_interface_class);
|
env->DeleteLocalRef(host_interface_class);
|
||||||
env->DeleteLocalRef(patch_code_class);
|
env->DeleteLocalRef(patch_code_class);
|
||||||
env->DeleteLocalRef(game_list_entry_class);
|
env->DeleteLocalRef(game_list_entry_class);
|
||||||
|
env->DeleteLocalRef(achievement_class);
|
||||||
|
|
||||||
jclass emulation_activity_class;
|
jclass emulation_activity_class;
|
||||||
if ((s_AndroidHostInterface_constructor =
|
if ((s_AndroidHostInterface_constructor =
|
||||||
|
@ -871,7 +893,10 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
(s_SaveStateInfo_constructor = env->GetMethodID(
|
(s_SaveStateInfo_constructor = env->GetMethodID(
|
||||||
s_SaveStateInfo_class, "<init>",
|
s_SaveStateInfo_class, "<init>",
|
||||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZII[B)V")) ==
|
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZII[B)V")) ==
|
||||||
nullptr)
|
nullptr ||
|
||||||
|
(s_Achievement_constructor =
|
||||||
|
env->GetMethodID(s_Achievement_class, "<init>",
|
||||||
|
"(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZ)V")) == nullptr)
|
||||||
{
|
{
|
||||||
Log_ErrorPrint("AndroidHostInterface lookups failed");
|
Log_ErrorPrint("AndroidHostInterface lookups failed");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -1647,4 +1672,107 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_toggleControllerAnalogMode, jo
|
||||||
ctrl->SetButtonState(code.value(), true);
|
ctrl->SetButtonState(code.value(), true);
|
||||||
ctrl->SetButtonState(code.value(), false);
|
ctrl->SetButtonState(code.value(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setFullscreenUINotificationVerticalPosition, jobject obj,
|
||||||
|
jfloat position, jfloat direction)
|
||||||
|
{
|
||||||
|
AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj);
|
||||||
|
hi->RunOnEmulationThread(
|
||||||
|
[position, direction]() { ImGuiFullscreen::SetNotificationVerticalPosition(position, direction); });
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isCheevosActive, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::IsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isCheevosChallengeModeActive, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::IsChallengeModeActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jobjectArray, AndroidHostInterface_getCheevoList, jobject obj)
|
||||||
|
{
|
||||||
|
if (!Cheevos::IsActive())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
std::vector<jobject> cheevos;
|
||||||
|
Cheevos::EnumerateAchievements([env, &cheevos](const Cheevos::Achievement& cheevo) {
|
||||||
|
jstring title = env->NewStringUTF(cheevo.title.c_str());
|
||||||
|
jstring description = env->NewStringUTF(cheevo.description.c_str());
|
||||||
|
jstring locked_badge_path =
|
||||||
|
cheevo.locked_badge_path.empty() ? nullptr : env->NewStringUTF(cheevo.locked_badge_path.c_str());
|
||||||
|
jstring unlocked_badge_path =
|
||||||
|
cheevo.unlocked_badge_path.empty() ? nullptr : env->NewStringUTF(cheevo.unlocked_badge_path.c_str());
|
||||||
|
|
||||||
|
jobject object = env->NewObject(s_Achievement_class, s_Achievement_constructor, static_cast<jint>(cheevo.id), title,
|
||||||
|
description, locked_badge_path, unlocked_badge_path,
|
||||||
|
static_cast<jint>(cheevo.points), static_cast<jboolean>(cheevo.locked));
|
||||||
|
cheevos.push_back(object);
|
||||||
|
|
||||||
|
if (unlocked_badge_path)
|
||||||
|
env->DeleteLocalRef(unlocked_badge_path);
|
||||||
|
if (locked_badge_path)
|
||||||
|
env->DeleteLocalRef(locked_badge_path);
|
||||||
|
env->DeleteLocalRef(description);
|
||||||
|
env->DeleteLocalRef(title);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cheevos.empty())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
jobjectArray ret = env->NewObjectArray(static_cast<jsize>(cheevos.size()), s_Achievement_class, nullptr);
|
||||||
|
for (size_t i = 0; i < cheevos.size(); i++)
|
||||||
|
{
|
||||||
|
env->SetObjectArrayElement(ret, static_cast<jsize>(i), cheevos[i]);
|
||||||
|
env->DeleteLocalRef(cheevos[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoCount, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::GetAchievementCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getUnlockedCheevoCount, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::GetUnlockedAchiementCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoPointsForGame, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::GetCurrentPointsForGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getCheevoMaximumPointsForGame, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::GetMaximumPointsForGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getCheevoGameTitle, jobject obj)
|
||||||
|
{
|
||||||
|
const std::string& title = Cheevos::GetGameTitle();
|
||||||
|
return title.empty() ? nullptr : env->NewStringUTF(title.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jstring, AndroidHostInterface_getCheevoGameIconPath, jobject obj)
|
||||||
|
{
|
||||||
|
const std::string& path = Cheevos::GetGameIcon();
|
||||||
|
return path.empty() ? nullptr : env->NewStringUTF(path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_cheevosLogin, jobject obj, jstring username, jstring password)
|
||||||
|
{
|
||||||
|
const std::string username_str(AndroidHelpers::JStringToString(env, username));
|
||||||
|
const std::string password_str(AndroidHelpers::JStringToString(env, password));
|
||||||
|
return Cheevos::Login(username_str.c_str(), password_str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_cheevosLogout, jobject obj)
|
||||||
|
{
|
||||||
|
return Cheevos::Logout();
|
||||||
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ private:
|
||||||
|
|
||||||
namespace AndroidHelpers {
|
namespace AndroidHelpers {
|
||||||
|
|
||||||
|
JavaVM* GetJavaVM();
|
||||||
JNIEnv* GetJNIEnv();
|
JNIEnv* GetJNIEnv();
|
||||||
AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
|
AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
|
||||||
std::string JStringToString(JNIEnv* env, jstring str);
|
std::string JStringToString(JNIEnv* env, jstring str);
|
||||||
|
|
163
android/app/src/cpp/android_http_downloader.cpp
Normal file
163
android/app/src/cpp/android_http_downloader.cpp
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
#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()
|
||||||
|
{
|
||||||
|
std::unique_ptr<AndroidHTTPDownloader> instance(std::make_unique<AndroidHTTPDownloader>());
|
||||||
|
if (!instance->Initialize())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AndroidHTTPDownloader::Initialize()
|
||||||
|
{
|
||||||
|
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>", "()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_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)
|
||||||
|
{
|
||||||
|
jobject obj = env->NewObject(m_URLDownloader_class, m_URLDownloader_constructor);
|
||||||
|
jstring url_string = env->NewStringUTF(req->url.c_str());
|
||||||
|
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);
|
||||||
|
|
||||||
|
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
|
44
android/app/src/cpp/android_http_downloader.h
Normal file
44
android/app/src/cpp/android_http_downloader.h
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#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();
|
||||||
|
|
||||||
|
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::unique_ptr<cb::ThreadPool> m_thread_pool;
|
||||||
|
std::mutex m_cancel_mutex;
|
||||||
|
|
||||||
|
jclass m_URLDownloader_class = nullptr;
|
||||||
|
jmethodID m_URLDownloader_constructor = nullptr;
|
||||||
|
jmethodID m_URLDownloader_get = nullptr;
|
||||||
|
jmethodID m_URLDownloader_post = nullptr;
|
||||||
|
jmethodID m_URLDownloader_getStatusCode = nullptr;
|
||||||
|
jmethodID m_URLDownloader_getData = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace FrontendCommon
|
|
@ -1,11 +1,10 @@
|
||||||
#include "android_progress_callback.h"
|
#include "android_progress_callback.h"
|
||||||
#include "android_host_interface.h"
|
#include "android_host_interface.h"
|
||||||
#include "common/log.h"
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include "common/log.h"
|
||||||
Log_SetChannel(AndroidProgressCallback);
|
Log_SetChannel(AndroidProgressCallback);
|
||||||
|
|
||||||
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object)
|
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object) : m_java_object(java_object)
|
||||||
: m_java_object(java_object)
|
|
||||||
{
|
{
|
||||||
jclass cls = env->GetObjectClass(java_object);
|
jclass cls = env->GetObjectClass(java_object);
|
||||||
m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
|
m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
|
||||||
|
@ -15,7 +14,8 @@ AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_objec
|
||||||
m_modal_error_method = env->GetMethodID(cls, "modalError", "(Ljava/lang/String;)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_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V");
|
||||||
m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z");
|
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);
|
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;
|
AndroidProgressCallback::~AndroidProgressCallback() = default;
|
||||||
|
|
|
@ -56,16 +56,25 @@ AndroidSettingsInterface::AndroidSettingsInterface(jobject java_context)
|
||||||
Assert(m_get_boolean && m_get_int && m_get_float && m_get_string && m_get_string_set && m_set_to_array);
|
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 = 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_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_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;");
|
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);
|
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_clear_section =
|
||||||
m_helper_add_to_string_list = env->GetStaticMethodID(m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V");
|
||||||
m_helper_remove_from_string_list = env->GetStaticMethodID(m_helper_class, "removeFromStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
m_helper_add_to_string_list = env->GetStaticMethodID(
|
||||||
m_helper_set_string_list = env->GetStaticMethodID(m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V");
|
m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
||||||
Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list && m_helper_set_string_list);
|
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()
|
AndroidSettingsInterface::~AndroidSettingsInterface()
|
||||||
|
@ -212,7 +221,7 @@ jobject AndroidSettingsInterface::GetPreferencesEditor(JNIEnv* env)
|
||||||
return env->CallObjectMethod(m_java_shared_preferences, m_edit);
|
return env->CallObjectMethod(m_java_shared_preferences, m_edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AndroidSettingsInterface::CheckForException(JNIEnv *env, const char *task)
|
void AndroidSettingsInterface::CheckForException(JNIEnv* env, const char* task)
|
||||||
{
|
{
|
||||||
if (!env->ExceptionCheck())
|
if (!env->ExceptionCheck())
|
||||||
return;
|
return;
|
||||||
|
@ -230,7 +239,8 @@ void AndroidSettingsInterface::SetIntValue(const char* section, const char* key,
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%d", value)));
|
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()));
|
LocalRefHolder<jobject> dummy(env,
|
||||||
|
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||||
env->CallBooleanMethod(editor, m_edit_commit);
|
env->CallBooleanMethod(editor, m_edit_commit);
|
||||||
|
|
||||||
CheckForException(env, "SetIntValue");
|
CheckForException(env, "SetIntValue");
|
||||||
|
@ -245,7 +255,8 @@ void AndroidSettingsInterface::SetFloatValue(const char* section, const char* ke
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%f", value)));
|
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()));
|
LocalRefHolder<jobject> dummy(env,
|
||||||
|
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||||
env->CallBooleanMethod(editor, m_edit_commit);
|
env->CallBooleanMethod(editor, m_edit_commit);
|
||||||
|
|
||||||
CheckForException(env, "SetFloatValue");
|
CheckForException(env, "SetFloatValue");
|
||||||
|
@ -260,7 +271,8 @@ void AndroidSettingsInterface::SetBoolValue(const char* section, const char* key
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value ? "true" : "false"));
|
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()));
|
LocalRefHolder<jobject> dummy(env,
|
||||||
|
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||||
env->CallBooleanMethod(editor, m_edit_commit);
|
env->CallBooleanMethod(editor, m_edit_commit);
|
||||||
|
|
||||||
CheckForException(env, "SetBoolValue");
|
CheckForException(env, "SetBoolValue");
|
||||||
|
@ -275,7 +287,8 @@ void AndroidSettingsInterface::SetStringValue(const char* section, const char* k
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value));
|
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()));
|
LocalRefHolder<jobject> dummy(env,
|
||||||
|
env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||||
env->CallBooleanMethod(editor, m_edit_commit);
|
env->CallBooleanMethod(editor, m_edit_commit);
|
||||||
|
|
||||||
CheckForException(env, "SetStringValue");
|
CheckForException(env, "SetStringValue");
|
||||||
|
@ -316,10 +329,11 @@ std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* sec
|
||||||
env->ExceptionClear();
|
env->ExceptionClear();
|
||||||
|
|
||||||
// this might just be a string, not a string set
|
// this might just be a string, not a string set
|
||||||
LocalRefHolder<jstring> string_object(
|
LocalRefHolder<jstring> string_object(env, reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
|
m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
|
||||||
|
|
||||||
if (!env->ExceptionCheck()) {
|
if (!env->ExceptionCheck())
|
||||||
|
{
|
||||||
std::vector<std::string> ret;
|
std::vector<std::string> ret;
|
||||||
if (string_object)
|
if (string_object)
|
||||||
ret.push_back(AndroidHelpers::JStringToString(env, string_object));
|
ret.push_back(AndroidHelpers::JStringToString(env, string_object));
|
||||||
|
@ -369,7 +383,8 @@ void AndroidSettingsInterface::SetStringList(const char* section, const char* ke
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
LocalRefHolder<jobjectArray> items_array(env, env->NewObjectArray(static_cast<jsize>(items.size()), AndroidHelpers::GetStringClass(), nullptr));
|
LocalRefHolder<jobjectArray> items_array(
|
||||||
|
env, env->NewObjectArray(static_cast<jsize>(items.size()), AndroidHelpers::GetStringClass(), nullptr));
|
||||||
for (size_t i = 0; i < items.size(); i++)
|
for (size_t i = 0; i < items.size(); i++)
|
||||||
{
|
{
|
||||||
LocalRefHolder<jstring> item_jstr(env, env->NewStringUTF(items[i].c_str()));
|
LocalRefHolder<jstring> item_jstr(env, env->NewStringUTF(items[i].c_str()));
|
||||||
|
@ -377,7 +392,8 @@ void AndroidSettingsInterface::SetStringList(const char* section, const char* ke
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
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());
|
env->CallStaticVoidMethod(m_helper_class, m_helper_set_string_list, m_java_shared_preferences, key_string.Get(),
|
||||||
|
items_array.Get());
|
||||||
|
|
||||||
CheckForException(env, "SetStringList");
|
CheckForException(env, "SetStringList");
|
||||||
}
|
}
|
||||||
|
@ -389,7 +405,8 @@ bool AndroidSettingsInterface::RemoveFromStringList(const char* section, const c
|
||||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
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());
|
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");
|
CheckForException(env, "RemoveFromStringList");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -401,7 +418,8 @@ bool AndroidSettingsInterface::AddToStringList(const char* section, const char*
|
||||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
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());
|
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");
|
CheckForException(env, "AddToStringList");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,8 +129,8 @@ bool OpenSLESAudioStream::OpenDevice()
|
||||||
for (u32 i = 0; i < NUM_BUFFERS; i++)
|
for (u32 i = 0; i < NUM_BUFFERS; i++)
|
||||||
m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels);
|
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",
|
Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers", m_output_sample_rate,
|
||||||
m_output_sample_rate, m_channels, m_buffer_size, NUM_BUFFERS);
|
m_channels, m_buffer_size, NUM_BUFFERS);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,8 @@ void OpenSLESAudioStream::PauseDevice(bool paused)
|
||||||
if (m_paused == paused)
|
if (m_paused == paused)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
SLresult res = (*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
|
SLresult res =
|
||||||
|
(*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
|
||||||
if (res != SL_RESULT_SUCCESS)
|
if (res != SL_RESULT_SUCCESS)
|
||||||
Log_ErrorPrintf("SetPlayState failed: %d", res);
|
Log_ErrorPrintf("SetPlayState failed: %d", res);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -13,7 +14,9 @@
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".GameDirectoriesActivity"
|
android:name=".GameDirectoriesActivity"
|
||||||
android:label="@string/title_activity_game_directories"
|
android:label="@string/title_activity_game_directories"
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -144,6 +144,20 @@ public class AndroidHostInterface {
|
||||||
|
|
||||||
public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty);
|
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 {
|
static {
|
||||||
System.loadLibrary("duckstation-native");
|
System.loadLibrary("duckstation-native");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.stenzek.duckstation;
|
package com.github.stenzek.duckstation;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
|
@ -26,6 +27,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -392,6 +394,21 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
AndroidHostInterface.getInstance().pauseEmulationThread(false);
|
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 showMenu() {
|
private void showMenu() {
|
||||||
if (getBooleanSetting("Main/PauseOnMenu", false) &&
|
if (getBooleanSetting("Main/PauseOnMenu", false) &&
|
||||||
!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
|
!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
|
||||||
|
@ -401,6 +418,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
if (mGameTitle != null && !mGameTitle.isEmpty())
|
if (mGameTitle != null && !mGameTitle.isEmpty())
|
||||||
builder.setTitle(mGameTitle);
|
builder.setTitle(mGameTitle);
|
||||||
|
|
||||||
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
|
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0: // Load State
|
case 0: // Load State
|
||||||
|
@ -422,13 +440,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 3: // More Options
|
case 3: // Achievements
|
||||||
|
{
|
||||||
|
showAchievementsPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: // More Options
|
||||||
{
|
{
|
||||||
showMoreMenu();
|
showMoreMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 4: // Quit
|
case 5: // Quit
|
||||||
{
|
{
|
||||||
mStopRequested = true;
|
mStopRequested = true;
|
||||||
finish();
|
finish();
|
||||||
|
@ -437,7 +461,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||||
builder.create().show();
|
|
||||||
|
final AlertDialog dialog = builder.create();
|
||||||
|
dialog.setOnShowListener(dialogInterface -> {
|
||||||
|
// Disable cheevos if not loaded.
|
||||||
|
if (AndroidHostInterface.getInstance().getCheevoCount() == 0)
|
||||||
|
disableDialogMenuItem(dialog, 3);
|
||||||
|
|
||||||
|
// Disable load state for challenge mode.
|
||||||
|
if (AndroidHostInterface.getInstance().isCheevosChallengeModeActive())
|
||||||
|
disableDialogMenuItem(dialog, 0);
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSaveStateMenu(boolean saving) {
|
private void showSaveStateMenu(boolean saving) {
|
||||||
|
@ -512,7 +547,14 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||||
builder.create().show();
|
|
||||||
|
final AlertDialog dialog = builder.create();
|
||||||
|
dialog.setOnShowListener(dialogInterface -> {
|
||||||
|
// Disable patch codes when challenge mode is active.
|
||||||
|
if (AndroidHostInterface.getInstance().isCheevosChallengeModeActive())
|
||||||
|
disableDialogMenuItem(dialog, 1);
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showTouchscreenControllerMenu() {
|
private void showTouchscreenControllerMenu() {
|
||||||
|
@ -660,6 +702,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
builder.create().show();
|
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
|
* Touchscreen controller overlay
|
||||||
*/
|
*/
|
||||||
|
@ -697,6 +751,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||||
else
|
else
|
||||||
mVibratorService = null;
|
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 InputManager.InputDeviceListener mInputDeviceListener;
|
||||||
|
|
|
@ -98,26 +98,7 @@ public class GameGridFragment extends Fragment implements GameList.OnRefreshList
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View v) {
|
public boolean onLongClick(View v) {
|
||||||
PopupMenu menu = new PopupMenu(mParent, v, Gravity.RIGHT | Gravity.TOP);
|
mParent.openGamePopupMenu(v, mEntry);
|
||||||
menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
|
|
||||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == R.id.game_list_entry_menu_start_game) {
|
|
||||||
mParent.startEmulation(mEntry.getPath(), false);
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.game_list_entry_menu_resume_game) {
|
|
||||||
mParent.startEmulation(mEntry.getPath(), true);
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.game_list_entry_menu_properties) {
|
|
||||||
mParent.openGameProperties(mEntry.getPath());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
menu.show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,26 +167,7 @@ public class GameListFragment extends Fragment implements GameList.OnRefreshList
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View v) {
|
public boolean onLongClick(View v) {
|
||||||
androidx.appcompat.widget.PopupMenu menu = new androidx.appcompat.widget.PopupMenu(mParent, v, Gravity.RIGHT | Gravity.TOP);
|
mParent.openGamePopupMenu(v, mEntry);
|
||||||
menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
|
|
||||||
menu.setOnMenuItemClickListener(new androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == R.id.game_list_entry_menu_start_game) {
|
|
||||||
mParent.startEmulation(mEntry.getPath(), false);
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.game_list_entry_menu_resume_game) {
|
|
||||||
mParent.startEmulation(mEntry.getPath(), true);
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.game_list_entry_menu_properties) {
|
|
||||||
mParent.openGameProperties(mEntry.getPath());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
menu.show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -65,7 +66,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
public boolean shouldResumeStateByDefault() {
|
public boolean shouldResumeStateByDefault() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
return prefs.getBoolean("Main/SaveStateOnExit", true);
|
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() {
|
private void setLanguage() {
|
||||||
|
@ -341,6 +349,34 @@ public class MainActivity extends AppCompatActivity {
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
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) {
|
public boolean startEmulation(String bootPath, boolean resumeState) {
|
||||||
if (!doBIOSCheck())
|
if (!doBIOSCheck())
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -51,7 +51,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
private int resourceId;
|
private final int resourceId;
|
||||||
|
|
||||||
public SettingsFragment(int resourceId) {
|
public SettingsFragment(int resourceId) {
|
||||||
this.resourceId = resourceId;
|
this.resourceId = resourceId;
|
||||||
|
@ -110,7 +110,10 @@ public class SettingsActivity extends AppCompatActivity {
|
||||||
case 4: // Controllers
|
case 4: // Controllers
|
||||||
return new SettingsFragment(R.xml.controllers_preferences);
|
return new SettingsFragment(R.xml.controllers_preferences);
|
||||||
|
|
||||||
case 5: // Advanced
|
case 5: // Achievements
|
||||||
|
return new AchievementSettingsFragment();
|
||||||
|
|
||||||
|
case 6: // Advanced
|
||||||
return new SettingsFragment(R.xml.advanced_preferences);
|
return new SettingsFragment(R.xml.advanced_preferences);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -120,7 +123,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return 6;
|
return 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
public URLDownloader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
android/app/src/main/res/drawable/ic_baseline_lock_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_baseline_lock_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,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,-2zM18,20L6,20L6,10h12v10z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/navigation_header_container"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentStart="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:foregroundGravity="center_vertical"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
tools:srcCompat="@drawable/ic_media_cdrom" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:text="Game Title"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_toRightOf="@+id/icon"
|
||||||
|
android:layout_alignParentEnd="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:text="You have unlocked %d of %d achievements, earning %d of %d possible points."
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:layout_below="@+id/title"
|
||||||
|
android:layout_toRightOf="@+id/icon"
|
||||||
|
android:layout_alignParentEnd="true" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_below="@+id/summary"
|
||||||
|
android:layout_toRightOf="@+id/icon"
|
||||||
|
android:layout_alignParentEnd="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:layout_below="@+id/navigation_header_container"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentEnd="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
133
android/app/src/main/res/layout/fragment_achievements_login.xml
Normal file
133
android/app/src/main/res/layout/fragment_achievements_login.xml
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="280dp"
|
||||||
|
>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:text="@string/achievement_settings_login_title"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/achievement_settings_login_help"
|
||||||
|
/>
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:inputType="textVisiblePassword"
|
||||||
|
android:nextFocusDown="@+id/password"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:hint="@string/achievement_settings_login_username_hint"
|
||||||
|
/>
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:hint="@string/achievement_settings_login_password_hint"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:visibility="visible" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/buttonPanel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:divider="?android:attr/dividerHorizontal"
|
||||||
|
android:dividerPadding="0dip"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:showDividers="beginning"
|
||||||
|
android:padding="5dp"
|
||||||
|
>
|
||||||
|
<LinearLayout
|
||||||
|
style="?android:attr/buttonBarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layoutDirection="locale"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="UselessParent"
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancel"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:focusable="true"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:text="@string/achievement_settings_login_cancel_button"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/login"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:focusable="true"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:text="@string/achievement_settings_login_login_button"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminateOnly="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
/>
|
||||||
|
</FrameLayout>
|
79
android/app/src/main/res/layout/layout_achievement_entry.xml
Normal file
79
android/app/src/main/res/layout/layout_achievement_entry.xml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:foregroundGravity="center_vertical"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@drawable/ic_media_cdrom" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="80dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:text="Achievement Title"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="80dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:paddingBottom="8px"
|
||||||
|
android:text="Achievement Description"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/title" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locked_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:paddingBottom="8px"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_lock_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/points"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:text="5 Points"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:srcCompat="@drawable/ic_star_5"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/locked_icon" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -82,6 +82,7 @@
|
||||||
<item>Cargar Estado</item>
|
<item>Cargar Estado</item>
|
||||||
<item>Guardar Estado</item>
|
<item>Guardar Estado</item>
|
||||||
<item>Activar Avance Rápido</item>
|
<item>Activar Avance Rápido</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Más Opciones</item>
|
<item>Más Opciones</item>
|
||||||
<item>Salir</item>
|
<item>Salir</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Mejoras</item>
|
<item>Mejoras</item>
|
||||||
<item>Controles</item>
|
<item>Controles</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Avanzado</item>
|
<item>Avanzado</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
<item>Carica Stato</item>
|
<item>Carica Stato</item>
|
||||||
<item>Salva Stato</item>
|
<item>Salva Stato</item>
|
||||||
<item>Abilita/Disabilita Avanti Veloce</item>
|
<item>Abilita/Disabilita Avanti Veloce</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Altre Opzioni</item>
|
<item>Altre Opzioni</item>
|
||||||
<item>Esci</item>
|
<item>Esci</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Miglioramenti</item>
|
<item>Miglioramenti</item>
|
||||||
<item>Controller</item>
|
<item>Controller</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Avanzate</item>
|
<item>Avanzate</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
<item>Staat Laden</item>
|
<item>Staat Laden</item>
|
||||||
<item>Staat Opslaan</item>
|
<item>Staat Opslaan</item>
|
||||||
<item>Doorspoelen aan/uitzetten</item>
|
<item>Doorspoelen aan/uitzetten</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Meer Opties</item>
|
<item>Meer Opties</item>
|
||||||
<item>Afsluiten</item>
|
<item>Afsluiten</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Verbeteringen</item>
|
<item>Verbeteringen</item>
|
||||||
<item>Controllers</item>
|
<item>Controllers</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Geavanceerd</item>
|
<item>Geavanceerd</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
<item>Carregar Estado</item>
|
<item>Carregar Estado</item>
|
||||||
<item>Salvar Estado</item>
|
<item>Salvar Estado</item>
|
||||||
<item>Avanço (Fixo)</item>
|
<item>Avanço (Fixo)</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Mais Opções</item>
|
<item>Mais Opções</item>
|
||||||
<item>Sair</item>
|
<item>Sair</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
<item>Áudio</item>
|
<item>Áudio</item>
|
||||||
<item>Melhorias</item>
|
<item>Melhorias</item>
|
||||||
<item>Controles</item>
|
<item>Controles</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Avançado</item>
|
<item>Avançado</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
<item>Загрузить состояние</item>
|
<item>Загрузить состояние</item>
|
||||||
<item>Сохранить состояние</item>
|
<item>Сохранить состояние</item>
|
||||||
<item>Включить ускорение</item>
|
<item>Включить ускорение</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Другие опции</item>
|
<item>Другие опции</item>
|
||||||
<item>Выход</item>
|
<item>Выход</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -93,11 +94,11 @@
|
||||||
<item>Настройки эмулятора</item>
|
<item>Настройки эмулятора</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="emulation_touchscreen_menu">
|
<string-array name="emulation_touchscreen_menu">
|
||||||
<item>Сменить вид</item>
|
<item>Сменить вид</item>
|
||||||
<item>Настроить видимость</item>
|
<item>Настроить видимость</item>
|
||||||
<item>Добавить/убрать кнопки</item>
|
<item>Добавить/убрать кнопки</item>
|
||||||
<item>Изменить макет</item>
|
<item>Изменить макет</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_cdrom_read_speedup_entries">
|
<string-array name="settings_cdrom_read_speedup_entries">
|
||||||
<item>Нет (двойная скорость)</item>
|
<item>Нет (двойная скорость)</item>
|
||||||
<item>2x (скорость 4x)</item>
|
<item>2x (скорость 4x)</item>
|
||||||
|
@ -145,6 +146,7 @@
|
||||||
<item>Звук</item>
|
<item>Звук</item>
|
||||||
<item>Улучшения</item>
|
<item>Улучшения</item>
|
||||||
<item>Контроллеры</item>
|
<item>Контроллеры</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Расширенные</item>
|
<item>Расширенные</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -161,6 +161,7 @@
|
||||||
<item>Load State</item>
|
<item>Load State</item>
|
||||||
<item>Save State</item>
|
<item>Save State</item>
|
||||||
<item>Toggle Fast Forward</item>
|
<item>Toggle Fast Forward</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>More Options</item>
|
<item>More Options</item>
|
||||||
<item>Quit</item>
|
<item>Quit</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -265,6 +266,7 @@
|
||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
<item>Enhancements</item>
|
<item>Enhancements</item>
|
||||||
<item>Controllers</item>
|
<item>Controllers</item>
|
||||||
|
<item>Achievements</item>
|
||||||
<item>Advanced</item>
|
<item>Advanced</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="settings_gpu_msaa_entries">
|
<string-array name="settings_gpu_msaa_entries">
|
||||||
|
|
|
@ -233,4 +233,37 @@
|
||||||
<string name="settings_summary_touch_gliding">Allows you to press multiple controller face buttons by dragging your finger along the screen.</string>
|
<string name="settings_summary_touch_gliding">Allows you to press multiple controller face buttons by dragging your finger along the screen.</string>
|
||||||
<string name="menu_game_list_entry_game_properties">Game Properties</string>
|
<string name="menu_game_list_entry_game_properties">Game Properties</string>
|
||||||
<string name="emulation_activity_change_disc_select_new_file">Select New File...</string>
|
<string name="emulation_activity_change_disc_select_new_file">Select New File...</string>
|
||||||
|
<string name="settings_achievements_enable">Enable RetroAchievements</string>
|
||||||
|
<string name="settings_summary_achievements_enable">When enabled and logged in, DuckStation will scan for achievements on startup.</string>
|
||||||
|
<string name="settings_achievements_challenge_mode">Enable Hardcore Mode</string>
|
||||||
|
<string name="settings_summary_achievements_challenge_mode">Challenge mode. Disables save states, patch code, and slowdown functions, but you receive double the achievement points. Cannot be toggled while ingame.</string>
|
||||||
|
<string name="settings_achievements_rich_presence">Enable Rich Presence</string>
|
||||||
|
<string name="settings_summary_achievements_rich_presence">Rich presence information will be collected and sent to the server where supported.</string>
|
||||||
|
<string name="settings_achievements_username">User Name</string>
|
||||||
|
<string name="settings_achievements_token_generation_time">Token Generation Time</string>
|
||||||
|
<string name="settings_achievements_login">Login</string>
|
||||||
|
<string name="settings_summary_achievements_login">Logs in to your account to record achievements.</string>
|
||||||
|
<string name="settings_achievements_register">Register</string>
|
||||||
|
<string name="settings_summary_achievements_register">Opens a link to create a new account.</string>
|
||||||
|
<string name="settings_achievements_logout">Logout</string>
|
||||||
|
<string name="settings_summary_achievements_logout">Logs out of your account. No new achievements will be recorded.</string>
|
||||||
|
<string name="settings_achievements_view_profile">View Profile</string>
|
||||||
|
<string name="settings_summary_achievements_view_profile">Opens a link to your profile.</string>
|
||||||
|
<string name="settings_achievements_test_mode">Enable Test Mode</string>
|
||||||
|
<string name="settings_summary_achievements_test_mode">When enabled, DuckStation will assume all achievements are locked and not send any unlock notifications to the server.</string>
|
||||||
|
<string name="settings_achievements_use_first_disc_from_playlist">Use First Disc From Playlist</string>
|
||||||
|
<string name="settings_summary_achievements_use_first_disc_from_playlist">When enabled, the first disc in a playlist will be used for achievements, regardless of which disc is active.</string>
|
||||||
|
<string name="achievement_settings_login_title">RetroAchievements Login</string>
|
||||||
|
<string name="achievement_settings_login_help">Please enter user name and password for retroachievements.org below. Your password will not be saved in DuckStation, an access token will be generated and used instead.</string>
|
||||||
|
<string name="achievement_settings_login_username_hint">Username</string>
|
||||||
|
<string name="achievement_settings_login_password_hint">Password</string>
|
||||||
|
<string name="achievement_settings_login_login_button">Login</string>
|
||||||
|
<string name="achievement_settings_login_cancel_button">Cancel</string>
|
||||||
|
<string name="achievement_settings_login_failed">Login failed. Please check your username and password, and try again.</string>
|
||||||
|
<string name="achievement_points_format_string">%d points</string>
|
||||||
|
<string name="achievement_summary_format_string">You have unlocked %1$d of %2$d achievements, earning %3$d of %4$d possible points.</string>
|
||||||
|
<string name="achievement_title_challenge_mode_format_string">%s (Hardcore Mode)</string>
|
||||||
|
<string name="settings_achievements_disclaimer">DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress.</string>
|
||||||
|
<string name="settings_achievements_confirm_logout_title">Confirm Logout</string>
|
||||||
|
<string name="settings_achievements_confirm_logout_message">After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
59
android/app/src/main/res/xml/achievement_preferences.xml
Normal file
59
android/app/src/main/res/xml/achievement_preferences.xml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="Global Settings" app:iconSpaceReserved="false">
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="Cheevos/Enabled"
|
||||||
|
app:title="@string/settings_achievements_enable"
|
||||||
|
app:summary="@string/settings_summary_achievements_enable"
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="Cheevos/ChallengeMode"
|
||||||
|
app:title="@string/settings_achievements_challenge_mode"
|
||||||
|
app:summary="@string/settings_summary_achievements_challenge_mode"
|
||||||
|
app:dependency="Cheevos/Enabled"
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="Cheevos/RichPresence"
|
||||||
|
app:title="@string/settings_achievements_rich_presence"
|
||||||
|
app:summary="@string/settings_summary_achievements_rich_presence"
|
||||||
|
app:dependency="Cheevos/Enabled"
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="Account" app:iconSpaceReserved="false">
|
||||||
|
<Preference
|
||||||
|
app:key="Cheevos/Username"
|
||||||
|
app:title="@string/settings_achievements_username"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<Preference
|
||||||
|
app:key="Cheevos/LoginTokenTime"
|
||||||
|
app:title="@string/settings_achievements_token_generation_time"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<PreferenceScreen
|
||||||
|
app:key="Cheevos/Login"
|
||||||
|
app:title="@string/settings_achievements_login"
|
||||||
|
app:summary="@string/settings_summary_achievements_login"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<PreferenceScreen
|
||||||
|
app:key="Cheevos/Register"
|
||||||
|
app:title="@string/settings_achievements_register"
|
||||||
|
app:summary="@string/settings_summary_achievements_register"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<PreferenceScreen
|
||||||
|
app:key="Cheevos/Logout"
|
||||||
|
app:title="@string/settings_achievements_logout"
|
||||||
|
app:summary="@string/settings_summary_achievements_logout"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<PreferenceScreen
|
||||||
|
app:key="Cheevos/ViewProfile"
|
||||||
|
app:title="@string/settings_achievements_view_profile"
|
||||||
|
app:summary="@string/settings_summary_achievements_view_profile"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
<PreferenceCategory app:title="@string/settings_achievements_disclaimer" app:iconSpaceReserved="false">
|
||||||
|
</PreferenceCategory>
|
||||||
|
</PreferenceScreen>
|
|
@ -217,4 +217,19 @@
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="Achievement Settings" app:iconSpaceReserved="false">
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="Cheevos/TestMode"
|
||||||
|
app:title="@string/settings_achievements_test_mode"
|
||||||
|
app:summary="@string/settings_summary_achievements_test_mode"
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="Cheevos/UseFirstDiscFromPlaylist"
|
||||||
|
app:title="@string/settings_achievements_use_first_disc_from_playlist"
|
||||||
|
app:summary="@string/settings_summary_achievements_use_first_disc_from_playlist"
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
6
android/app/src/main/res/xml/network_security_config.xml
Normal file
6
android/app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">i.retroachievements.org</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
|
@ -100,14 +100,14 @@ if(ENABLE_CHEEVOS)
|
||||||
http_downloader_winhttp.cpp
|
http_downloader_winhttp.cpp
|
||||||
http_downloader_winhttp.h
|
http_downloader_winhttp.h
|
||||||
)
|
)
|
||||||
else()
|
elseif(NOT ANDROID)
|
||||||
target_sources(frontend-common PRIVATE
|
target_sources(frontend-common PRIVATE
|
||||||
http_downloader_curl.cpp
|
http_downloader_curl.cpp
|
||||||
http_downloader_curl.h
|
http_downloader_curl.h
|
||||||
)
|
)
|
||||||
target_link_libraries(frontend-common PRIVATE
|
target_link_libraries(frontend-common PRIVATE
|
||||||
CURL::libcurl
|
CURL::libcurl
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_sources(frontend-common PRIVATE
|
target_sources(frontend-common PRIVATE
|
||||||
|
|
Loading…
Reference in a new issue