Android: Add MemoryCardImage class

This commit is contained in:
Connor McLaughlin 2021-03-21 00:40:03 +10:00
parent 4dec0dee2f
commit 6d4a3bb5a5
4 changed files with 535 additions and 6 deletions

View file

@ -14,6 +14,7 @@
#include "core/controller.h"
#include "core/gpu.h"
#include "core/host_display.h"
#include "core/memory_card_image.h"
#include "core/system.h"
#include "frontend-common/cheevos.h"
#include "frontend-common/game_list.h"
@ -60,6 +61,8 @@ static jclass s_SaveStateInfo_class;
static jmethodID s_SaveStateInfo_constructor;
static jclass s_Achievement_class;
static jmethodID s_Achievement_constructor;
static jclass s_MemoryCardFileInfo_class;
static jmethodID s_MemoryCardFileInfo_constructor;
namespace AndroidHelpers {
JavaVM* GetJavaVM()
@ -137,6 +140,58 @@ std::unique_ptr<GrowableMemoryByteStream> ReadInputStreamToMemory(JNIEnv* env, j
env->DeleteLocalRef(cls);
return bs;
}
std::vector<u8> ByteArrayToVector(JNIEnv* env, jbyteArray obj)
{
std::vector<u8> ret;
const jsize size = obj ? env->GetArrayLength(obj) : 0;
if (size > 0)
{
jbyte* data = env->GetByteArrayElements(obj, nullptr);
ret.resize(static_cast<size_t>(size));
std::memcpy(ret.data(), data, ret.size());
env->ReleaseByteArrayElements(obj, data, 0);
}
return ret;
}
jbyteArray NewByteArray(JNIEnv* env, const void* data, size_t size)
{
if (!data || size == 0)
return nullptr;
jbyteArray obj = env->NewByteArray(static_cast<jsize>(size));
jbyte* obj_data = env->GetByteArrayElements(obj, nullptr);
std::memcpy(obj_data, data, static_cast<size_t>(static_cast<jsize>(size)));
env->ReleaseByteArrayElements(obj, obj_data, 0);
return obj;
}
jbyteArray VectorToByteArray(JNIEnv* env, const std::vector<u8>& data)
{
if (data.empty())
return nullptr;
return NewByteArray(env, data.data(), data.size());
}
jobjectArray CreateObjectArray(JNIEnv* env, jclass object_class, const jobject* objects, size_t num_objects,
bool release_refs/* = false*/)
{
if (!objects || num_objects == 0)
return nullptr;
jobjectArray arr = env->NewObjectArray(static_cast<jsize>(num_objects), object_class, nullptr);
for (jsize i = 0; i < static_cast<jsize>(num_objects); i++)
{
env->SetObjectArrayElement(arr, i, objects[i]);
if (release_refs && objects[i])
env->DeleteLocalRef(objects[i]);
}
return arr;
}
} // namespace AndroidHelpers
AndroidHostInterface::AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory)
@ -897,7 +952,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
// Create global reference so it doesn't get cleaned up.
JNIEnv* env = AndroidHelpers::GetJNIEnv();
jclass string_class, host_interface_class, patch_code_class, game_list_entry_class, save_state_info_class,
achievement_class;
achievement_class, memory_card_file_info_class;
if ((string_class = env->FindClass("java/lang/String")) == nullptr ||
(s_String_class = static_cast<jclass>(env->NewGlobalRef(string_class))) == nullptr ||
(host_interface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr ||
@ -909,7 +964,9 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
(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 ||
(achievement_class = env->FindClass("com/github/stenzek/duckstation/Achievement")) == nullptr ||
(s_Achievement_class = static_cast<jclass>(env->NewGlobalRef(achievement_class))) == nullptr)
(s_Achievement_class = static_cast<jclass>(env->NewGlobalRef(achievement_class))) == nullptr ||
(memory_card_file_info_class = env->FindClass("com/github/stenzek/duckstation/MemoryCardFileInfo")) == nullptr ||
(s_MemoryCardFileInfo_class = static_cast<jclass>(env->NewGlobalRef(memory_card_file_info_class))) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface class lookup failed");
return -1;
@ -920,6 +977,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->DeleteLocalRef(patch_code_class);
env->DeleteLocalRef(game_list_entry_class);
env->DeleteLocalRef(achievement_class);
env->DeleteLocalRef(memory_card_file_info_class);
jclass emulation_activity_class;
if ((s_AndroidHostInterface_constructor =
@ -963,9 +1021,11 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_SaveStateInfo_class, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZII[B)V")) ==
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)
(s_Achievement_constructor = env->GetMethodID(
s_Achievement_class, "<init>",
"(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZ)V")) == nullptr ||
(s_MemoryCardFileInfo_constructor = env->GetMethodID(s_MemoryCardFileInfo_class, "<init>",
"(Ljava/lang/String;Ljava/lang/String;III[[B)V")) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface lookups failed");
return -1;
@ -1878,3 +1938,201 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_cheevosLogout, jobject obj)
{
return Cheevos::Logout();
}
static_assert(sizeof(MemoryCardImage::DataArray) == MemoryCardImage::DATA_SIZE);
static MemoryCardImage::DataArray* GetMemoryCardData(JNIEnv* env, jbyteArray obj)
{
if (!obj || env->GetArrayLength(obj) != MemoryCardImage::DATA_SIZE)
return nullptr;
return reinterpret_cast<MemoryCardImage::DataArray*>(env->GetByteArrayElements(obj, nullptr));
}
static void ReleaseMemoryCardData(JNIEnv* env, jbyteArray obj, MemoryCardImage::DataArray* data)
{
env->ReleaseByteArrayElements(obj, reinterpret_cast<jbyte*>(data), 0);
}
DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_isValid, jclass clazz, jbyteArray obj)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return false;
const bool res = MemoryCardImage::IsValid(*data);
ReleaseMemoryCardData(env, obj, data);
return res;
}
DEFINE_JNI_ARGS_METHOD(void, MemoryCardImage_format, jclass clazz, jbyteArray obj)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return;
MemoryCardImage::Format(data);
ReleaseMemoryCardData(env, obj, data);
}
DEFINE_JNI_ARGS_METHOD(jint, MemoryCardImage_getFreeBlocks, jclass clazz, jbyteArray obj)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return 0;
const u32 free_blocks = MemoryCardImage::GetFreeBlockCount(*data);
ReleaseMemoryCardData(env, obj, data);
return free_blocks;
}
DEFINE_JNI_ARGS_METHOD(jobjectArray, MemoryCardImage_getFiles, jclass clazz, jbyteArray obj)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return nullptr;
const std::vector<MemoryCardImage::FileInfo> files(MemoryCardImage::EnumerateFiles(*data));
std::vector<jobject> file_objects;
file_objects.reserve(files.size());
jclass byteArrayClass = env->FindClass("[B");
for (const MemoryCardImage::FileInfo& file : files)
{
jobject filename = env->NewStringUTF(file.filename.c_str());
jobject title = env->NewStringUTF(file.title.c_str());
jobjectArray frames = nullptr;
if (!file.icon_frames.empty())
{
frames = env->NewObjectArray(static_cast<jint>(file.icon_frames.size()), byteArrayClass, nullptr);
for (jsize i = 0; i < static_cast<jsize>(file.icon_frames.size()); i++)
{
static constexpr jsize frame_size = MemoryCardImage::ICON_WIDTH * MemoryCardImage::ICON_HEIGHT * sizeof(u32);
jbyteArray frame_data = env->NewByteArray(frame_size);
jbyte* frame_data_ptr = env->GetByteArrayElements(frame_data, nullptr);
std::memcpy(frame_data_ptr, file.icon_frames[i].pixels, frame_size);
env->ReleaseByteArrayElements(frame_data, frame_data_ptr, 0);
env->SetObjectArrayElement(frames, i, frame_data);
env->DeleteLocalRef(frame_data);
}
}
file_objects.push_back(env->NewObject(s_MemoryCardFileInfo_class, s_MemoryCardFileInfo_constructor, filename, title,
static_cast<jint>(file.size), static_cast<jint>(file.first_block),
static_cast<int>(file.num_blocks), frames));
env->DeleteLocalRef(frames);
env->DeleteLocalRef(title);
env->DeleteLocalRef(filename);
}
jobjectArray file_object_array =
AndroidHelpers::CreateObjectArray(env, s_MemoryCardFileInfo_class, file_objects.data(), file_objects.size(), true);
ReleaseMemoryCardData(env, obj, data);
return file_object_array;
}
DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_hasFile, jclass clazz, jbyteArray obj, jstring filename)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return false;
const std::string filename_str(AndroidHelpers::JStringToString(env, filename));
bool result = false;
if (!filename_str.empty())
{
const std::vector<MemoryCardImage::FileInfo> files(MemoryCardImage::EnumerateFiles(*data));
result = std::any_of(files.begin(), files.end(),
[&filename_str](const MemoryCardImage::FileInfo& fi) { return fi.filename == filename_str; });
}
ReleaseMemoryCardData(env, obj, data);
return result;
}
DEFINE_JNI_ARGS_METHOD(jbyteArray, MemoryCardImage_readFile, jclass clazz, jbyteArray obj, jstring filename)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return nullptr;
const std::string filename_str(AndroidHelpers::JStringToString(env, filename));
jbyteArray ret = nullptr;
if (!filename_str.empty())
{
const std::vector<MemoryCardImage::FileInfo> files(MemoryCardImage::EnumerateFiles(*data));
auto iter = std::find_if(files.begin(), files.end(), [&filename_str](const MemoryCardImage::FileInfo& fi) {
return fi.filename == filename_str;
});
if (iter != files.end())
{
std::vector<u8> file_data;
if (MemoryCardImage::ReadFile(*data, *iter, &file_data))
ret = AndroidHelpers::VectorToByteArray(env, file_data);
}
}
ReleaseMemoryCardData(env, obj, data);
return ret;
}
DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_writeFile, jclass clazz, jbyteArray obj, jstring filename,
jbyteArray file_data)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return false;
const std::string filename_str(AndroidHelpers::JStringToString(env, filename));
const std::vector<u8> file_data_vec(AndroidHelpers::ByteArrayToVector(env, file_data));
bool ret = false;
if (!filename_str.empty())
ret = MemoryCardImage::WriteFile(data, filename_str, file_data_vec);
ReleaseMemoryCardData(env, obj, data);
return ret;
}
DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_deleteFile, jclass clazz, jbyteArray obj, jstring filename)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return false;
const std::string filename_str(AndroidHelpers::JStringToString(env, filename));
bool ret = false;
if (!filename_str.empty())
{
const std::vector<MemoryCardImage::FileInfo> files(MemoryCardImage::EnumerateFiles(*data));
auto iter = std::find_if(files.begin(), files.end(), [&filename_str](const MemoryCardImage::FileInfo& fi) {
return fi.filename == filename_str;
});
if (iter != files.end())
ret = MemoryCardImage::DeleteFile(data, *iter);
}
ReleaseMemoryCardData(env, obj, data);
return ret;
}
DEFINE_JNI_ARGS_METHOD(jboolean, MemoryCardImage_importCard, jclass clazz, jbyteArray obj, jstring filename,
jbyteArray import_data)
{
MemoryCardImage::DataArray* data = GetMemoryCardData(env, obj);
if (!data)
return false;
const std::string filename_str(AndroidHelpers::JStringToString(env, filename));
std::vector<u8> import_data_vec(AndroidHelpers::ByteArrayToVector(env, import_data));
bool ret = false;
if (!filename_str.empty() && !import_data_vec.empty())
ret = MemoryCardImage::ImportCard(data, filename_str.c_str(), std::move(import_data_vec));
ReleaseMemoryCardData(env, obj, data);
return ret;
}

View file

@ -120,7 +120,11 @@ AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
std::string JStringToString(JNIEnv* env, jstring str);
std::unique_ptr<GrowableMemoryByteStream> ReadInputStreamToMemory(JNIEnv* env, jobject obj, u32 chunk_size = 65536);
jclass GetStringClass();
std::vector<u8> ByteArrayToVector(JNIEnv* env, jbyteArray obj);
jbyteArray NewByteArray(JNIEnv* env, const void* data, size_t size);
jbyteArray VectorToByteArray(JNIEnv* env, const std::vector<u8>& data);
jobjectArray CreateObjectArray(JNIEnv* env, jclass object_class, const jobject* objects, size_t num_objects,
bool release_refs = false);
} // namespace AndroidHelpers
template<typename T>

View file

@ -0,0 +1,64 @@
package com.github.stenzek.duckstation;
import android.graphics.Bitmap;
import java.nio.ByteBuffer;
public class MemoryCardFileInfo {
public static final int ICON_WIDTH = 16;
public static final int ICON_HEIGHT = 16;
private final String filename;
private final String title;
private final int size;
private final int firstBlock;
private final int numBlocks;
private final byte[][] iconFrames;
public MemoryCardFileInfo(String filename, String title, int size, int firstBlock, int numBlocks, byte[][] iconFrames) {
this.filename = filename;
this.title = title;
this.size = size;
this.firstBlock = firstBlock;
this.numBlocks = numBlocks;
this.iconFrames = iconFrames;
}
public String getFilename() {
return filename;
}
public String getTitle() {
return title;
}
public int getSize() {
return size;
}
public int getFirstBlock() {
return firstBlock;
}
public int getNumBlocks() {
return numBlocks;
}
public int getNumIconFrames() {
return (iconFrames != null) ? iconFrames.length : 0;
}
public byte[] getIconFrame(int index) {
return iconFrames[index];
}
public Bitmap getIconFrameBitmap(int index) {
final byte[] data = getIconFrame(index);
if (data == null)
return null;
final Bitmap bitmap = Bitmap.createBitmap(ICON_WIDTH, ICON_HEIGHT, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
return bitmap;
}
}

View file

@ -0,0 +1,203 @@
package com.github.stenzek.duckstation;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
public class MemoryCardImage {
public static final int DATA_SIZE = 128 * 1024;
public static final String FILE_EXTENSION = ".mcd";
private final Context context;
private final Uri uri;
private final byte[] data;
private MemoryCardImage(Context context, Uri uri, byte[] data) {
this.context = context;
this.uri = uri;
this.data = data;
}
public static String getTitleForUri(Uri uri) {
String name = uri.getLastPathSegment();
if (name != null) {
final int lastSlash = name.lastIndexOf('/');
if (lastSlash >= 0)
name = name.substring(lastSlash + 1);
if (name.endsWith(FILE_EXTENSION))
name = name.substring(0, name.length() - FILE_EXTENSION.length());
} else {
name = uri.toString();
}
return name;
}
public static MemoryCardImage open(Context context, Uri uri) {
byte[] data = FileUtil.readBytesFromUri(context, uri, DATA_SIZE);
if (data == null)
return null;
if (!isValid(data))
return null;
return new MemoryCardImage(context, uri, data);
}
public static MemoryCardImage create(Context context, Uri uri) {
byte[] data = new byte[DATA_SIZE];
format(data);
MemoryCardImage card = new MemoryCardImage(context, uri, data);
if (!card.save())
return null;
return card;
}
public static Uri[] getCardUris(Context context) {
final String directory = "/sdcard/duckstation/memcards";
final ArrayList<Uri> results = new ArrayList<>();
if (directory.charAt(0) == '/') {
// native path
final File directoryFile = new File(directory);
final File[] files = directoryFile.listFiles();
for (File file : files) {
if (!file.isFile())
continue;
if (!file.getName().endsWith(FILE_EXTENSION))
continue;
results.add(Uri.fromFile(file));
}
} else {
try {
final Uri baseUri = null;
final String[] scanProjection = new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE};
final ContentResolver resolver = context.getContentResolver();
final String treeDocId = DocumentsContract.getTreeDocumentId(baseUri);
final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(baseUri, treeDocId);
final Cursor cursor = resolver.query(queryUri, scanProjection, null, null, null);
while (cursor.moveToNext()) {
try {
final String mimeType = cursor.getString(2);
final String documentId = cursor.getString(0);
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(baseUri, documentId);
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
continue;
}
final String uriString = uri.toString();
if (!uriString.endsWith(FILE_EXTENSION))
continue;
results.add(uri);
} catch (Exception e) {
e.printStackTrace();
}
}
cursor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (results.isEmpty())
return null;
Collections.sort(results, (a, b) -> a.compareTo(b));
final Uri[] resultArray = new Uri[results.size()];
results.toArray(resultArray);
return resultArray;
}
private static native boolean isValid(byte[] data);
private static native void format(byte[] data);
private static native int getFreeBlocks(byte[] data);
private static native MemoryCardFileInfo[] getFiles(byte[] data);
private static native boolean hasFile(byte[] data, String filename);
private static native byte[] readFile(byte[] data, String filename);
private static native boolean writeFile(byte[] data, String filename, byte[] fileData);
private static native boolean deleteFile(byte[] data, String filename);
private static native boolean importCard(byte[] data, String filename, byte[] importData);
public boolean save() {
return FileUtil.writeBytesToUri(context, uri, data);
}
public boolean delete() {
return FileUtil.deleteFileAtUri(context, uri);
}
public boolean format() {
format(data);
return save();
}
public Uri getUri() {
return uri;
}
public String getTitle() {
return getTitleForUri(uri);
}
public int getFreeBlocks() {
return getFreeBlocks(data);
}
public MemoryCardFileInfo[] getFiles() {
return getFiles(data);
}
public boolean hasFile(String filename) {
return hasFile(data, filename);
}
public byte[] readFile(String filename) {
return readFile(data, filename);
}
public boolean writeFile(String filename, byte[] fileData) {
if (!writeFile(data, filename, fileData))
return false;
return save();
}
public boolean deleteFile(String filename) {
if (!deleteFile(data, filename))
return false;
return save();
}
public boolean importCard(String filename, byte[] importData) {
if (!importCard(data, filename, importData))
return false;
return save();
}
}