Android: Add AndroidHTTPDownloader class

This commit is contained in:
Connor McLaughlin 2021-03-13 21:06:31 +10:00
parent 566ecaf209
commit 6bced299f4
11 changed files with 360 additions and 35 deletions

View file

@ -3,6 +3,8 @@ set(SRCS
android_controller_interface.h
android_host_interface.cpp
android_host_interface.h
android_http_downloader.cpp
android_http_downloader.h
android_progress_callback.cpp
android_progress_callback.h
android_settings_interface.cpp

View file

@ -1,6 +1,6 @@
#pragma once
#include "frontend-common/controller_interface.h"
#include "core/types.h"
#include "frontend-common/controller_interface.h"
#include <array>
#include <functional>
#include <mutex>

View file

@ -55,6 +55,11 @@ static jclass s_SaveStateInfo_class;
static jmethodID s_SaveStateInfo_constructor;
namespace AndroidHelpers {
JavaVM* GetJavaVM()
{
return s_jvm;
}
// helper for retrieving the current per-thread jni environment
JNIEnv* GetJNIEnv()
{

View file

@ -109,6 +109,7 @@ private:
namespace AndroidHelpers {
JavaVM* GetJavaVM();
JNIEnv* GetJNIEnv();
AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
std::string JStringToString(JNIEnv* env, jstring str);

View file

@ -0,0 +1,162 @@
#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);
}
else
{
Log_ErrorPrintf("AttachCurrentThread() failed");
}
cancel_lock.lock();
req->state = Request::State::Complete;
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
HTTPDownloader::Request* AndroidHTTPDownloader::InternalCreateRequest()
{
Request* req = new Request();
return req;
}
void AndroidHTTPDownloader::InternalPollRequests()
{
// noop - uses thread pool
}
bool AndroidHTTPDownloader::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetValue();
m_thread_pool->Schedule(std::bind(&AndroidHTTPDownloader::ProcessRequest, this, req));
return true;
}
void AndroidHTTPDownloader::CloseRequest(HTTPDownloader::Request* request)
{
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
Request* req = static_cast<Request*>(request);
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
} // namespace FrontendCommon

View file

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

View file

@ -1,11 +1,10 @@
#include "android_progress_callback.h"
#include "android_host_interface.h"
#include "common/log.h"
#include "common/assert.h"
#include "common/log.h"
Log_SetChannel(AndroidProgressCallback);
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object)
: m_java_object(java_object)
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object) : m_java_object(java_object)
{
jclass cls = env->GetObjectClass(java_object);
m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
@ -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_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V");
m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z");
Assert(m_set_status_text_method && m_set_progress_range_method && m_set_progress_value_method && m_modal_error_method && m_modal_information_method && m_modal_confirmation_method);
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;

View file

@ -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);
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_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);
m_helper_clear_section = env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V");
m_helper_add_to_string_list = env->GetStaticMethodID(m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
m_helper_remove_from_string_list = env->GetStaticMethodID(m_helper_class, "removeFromStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
m_helper_set_string_list = env->GetStaticMethodID(m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V");
Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list && m_helper_set_string_list);
m_helper_clear_section =
env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V");
m_helper_add_to_string_list = env->GetStaticMethodID(
m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
m_helper_remove_from_string_list =
env->GetStaticMethodID(m_helper_class, "removeFromStringList",
"(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
m_helper_set_string_list = env->GetStaticMethodID(
m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V");
Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list &&
m_helper_set_string_list);
}
AndroidSettingsInterface::~AndroidSettingsInterface()
@ -212,7 +221,7 @@ jobject AndroidSettingsInterface::GetPreferencesEditor(JNIEnv* env)
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())
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> 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);
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> 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);
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> 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);
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> 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);
CheckForException(env, "SetStringValue");
@ -316,10 +329,11 @@ std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* sec
env->ExceptionClear();
// this might just be a string, not a string set
LocalRefHolder<jstring> string_object(
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
LocalRefHolder<jstring> string_object(env, reinterpret_cast<jstring>(env->CallObjectMethod(
m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
if (!env->ExceptionCheck()) {
if (!env->ExceptionCheck())
{
std::vector<std::string> ret;
if (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();
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++)
{
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)));
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");
}
@ -389,7 +405,8 @@ bool AndroidSettingsInterface::RemoveFromStringList(const char* section, const c
JNIEnv* env = AndroidHelpers::GetJNIEnv();
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_remove_from_string_list, m_java_shared_preferences, key_string.Get(), item_string.Get());
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_remove_from_string_list,
m_java_shared_preferences, key_string.Get(), item_string.Get());
CheckForException(env, "RemoveFromStringList");
return result;
}
@ -401,7 +418,8 @@ bool AndroidSettingsInterface::AddToStringList(const char* section, const char*
JNIEnv* env = AndroidHelpers::GetJNIEnv();
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_add_to_string_list, m_java_shared_preferences, key_string.Get(), item_string.Get());
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_add_to_string_list,
m_java_shared_preferences, key_string.Get(), item_string.Get());
CheckForException(env, "AddToStringList");
return result;
}

View file

@ -129,8 +129,8 @@ bool OpenSLESAudioStream::OpenDevice()
for (u32 i = 0; i < NUM_BUFFERS; i++)
m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels);
Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers",
m_output_sample_rate, m_channels, m_buffer_size, NUM_BUFFERS);
Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers", m_output_sample_rate,
m_channels, m_buffer_size, NUM_BUFFERS);
return true;
}
@ -139,7 +139,8 @@ void OpenSLESAudioStream::PauseDevice(bool paused)
if (m_paused == paused)
return;
SLresult res = (*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
SLresult res =
(*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
if (res != SL_RESULT_SUCCESS)
Log_ErrorPrintf("SetPlayState failed: %d", res);

View file

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

View file

@ -100,7 +100,7 @@ if(ENABLE_CHEEVOS)
http_downloader_winhttp.cpp
http_downloader_winhttp.h
)
else()
elseif(NOT ANDROID)
target_sources(frontend-common PRIVATE
http_downloader_curl.cpp
http_downloader_curl.h