ktaglib: implement iostream/file shim

This commit is contained in:
Alexander Capehart 2024-12-12 14:23:13 -07:00
parent 55e77707ea
commit be54ee9c18
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 224 additions and 86 deletions

View file

@ -1,65 +0,0 @@
//
// Created by oxycblt on 12/12/24.
//
#include "AndroidIOStream.h"
AndroidIOStream::AndroidIOStream(JNIEnv *env, jobject &fileRef) : env(env), fileRef(fileRef) {}
AndroidIOStream::~AndroidIOStream() {}
/*!
* Returns the stream name in the local file system encoding.
*/
TagLib::FileName AndroidIOStream::name() const {
return TagLib::FileName();
};
/*!
* Reads a block of size \a length at the current get pointer.
*/
TagLib::ByteVector AndroidIOStream::readBlock(size_t length) {
return {};
};
void AndroidIOStream::writeBlock(const TagLib::ByteVector &data) {
throw std::runtime_error("Not implemented");
};
void AndroidIOStream::insert(const TagLib::ByteVector &data,
TagLib::offset_t start, size_t replace) {
throw std::runtime_error("Not implemented");
};
void AndroidIOStream::removeBlock(TagLib::offset_t start, size_t length) {
throw std::runtime_error("Not implemented");
};
bool AndroidIOStream::readOnly() const {
return true;
};
bool AndroidIOStream::isOpen() const {
};
void AndroidIOStream::seek(TagLib::offset_t offset, Position p) {
};
void AndroidIOStream::clear() {
}
TagLib::offset_t AndroidIOStream::tell() const {
return 0;
};
TagLib::offset_t AndroidIOStream::length() {
return 0;
};
void AndroidIOStream::truncate(TagLib::offset_t length) {
};

View file

@ -46,7 +46,7 @@ set_target_properties(
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
ktaglib.cpp
AndroidIOStream.cpp)
JVMInputStream.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this

View file

@ -0,0 +1,101 @@
//
// Created by oxycblt on 12/12/24.
//
#include "JVMInputStream.h"
#include <cmath>
// TODO: Handle stream exceptions
JVMInputStream::JVMInputStream(JNIEnv *env, jobject inputStream) :
env(env), inputStream(inputStream) {
if (!env->IsInstanceOf(inputStream, env->FindClass("org/oxycblt/ktaglib/NativeInputStream"))) {
throw std::runtime_error("oStream is not an instance of TagLibOStream");
}
jclass inputStreamClass = env->FindClass("org/oxycblt/ktaglib/NativeInputStream");
inputStreamNameMethod = env->GetMethodID(inputStreamClass, "name", "()Ljava/lang/String;");
inputStreamReadBlockMethod = env->GetMethodID(inputStreamClass, "readBlock", "(J)[B");
inputStreamIsOpenMethod = env->GetMethodID(inputStreamClass, "isOpen", "()Z");
inputStreamSeekFromBeginningMethod = env->GetMethodID(inputStreamClass, "seekFromBeginning",
"(J)V");
inputStreamSeekFromCurrentMethod = env->GetMethodID(inputStreamClass, "seekFromCurrent",
"(J)V");
inputStreamSeekFromEndMethod = env->GetMethodID(inputStreamClass, "seekFromEnd", "(J)V");
inputStreamClearMethod = env->GetMethodID(inputStreamClass, "clear", "()V");
inputStreamTellMethod = env->GetMethodID(inputStreamClass, "tell", "()J");
inputStreamLengthMethod = env->GetMethodID(inputStreamClass, "length", "()J");
}
JVMInputStream::~JVMInputStream() {}
TagLib::FileName JVMInputStream::name() const {
auto name = (jstring) env->CallObjectMethod(inputStream, inputStreamNameMethod);
const char *nameChars = env->GetStringUTFChars(name, nullptr);
auto fileName = TagLib::FileName(nameChars);
env->ReleaseStringUTFChars(name, nameChars);
return fileName;
}
TagLib::ByteVector JVMInputStream::readBlock(size_t length) {
auto data = (jbyteArray) env->CallObjectMethod(inputStream, inputStreamReadBlockMethod, length);
jsize dataLength = env->GetArrayLength(data);
auto dataBytes = env->GetByteArrayElements(data, nullptr);
TagLib::ByteVector byteVector(reinterpret_cast<const char *>(dataBytes), dataLength);
env->ReleaseByteArrayElements(data, dataBytes, JNI_ABORT);
return byteVector;
}
void JVMInputStream::writeBlock(const TagLib::ByteVector &data) {
throw std::runtime_error("Not implemented");
}
void JVMInputStream::insert(const TagLib::ByteVector &data,
TagLib::offset_t start, size_t replace) {
throw std::runtime_error("Not implemented");
}
void JVMInputStream::removeBlock(TagLib::offset_t start, size_t length) {
throw std::runtime_error("Not implemented");
}
bool JVMInputStream::readOnly() const {
return true;
}
bool JVMInputStream::isOpen() const {
return env->CallBooleanMethod(inputStream, inputStreamIsOpenMethod);
}
void JVMInputStream::seek(TagLib::offset_t offset, Position p) {
auto joffset = static_cast<jlong>(std::llround(offset));
switch (p) {
case Beginning:
env->CallVoidMethod(inputStream, inputStreamSeekFromBeginningMethod, joffset);
break;
case Current:
env->CallVoidMethod(inputStream, inputStreamSeekFromCurrentMethod, joffset);
break;
case End:
env->CallVoidMethod(inputStream, inputStreamSeekFromEndMethod, joffset);
break;
}
}
void JVMInputStream::clear() {
env->CallVoidMethod(inputStream, inputStreamClearMethod);
}
TagLib::offset_t JVMInputStream::tell() const {
jlong jposition = env->CallLongMethod(inputStream, inputStreamTellMethod);
return static_cast<TagLib::offset_t>(jposition);
}
TagLib::offset_t JVMInputStream::length() {
jlong jlength = env->CallLongMethod(inputStream, inputStreamLengthMethod);
return static_cast<TagLib::offset_t>(jlength);
}
void JVMInputStream::truncate(TagLib::offset_t length) {
throw std::runtime_error("Not implemented");
}

View file

@ -2,21 +2,21 @@
// Created by oxycblt on 12/12/24.
//
#ifndef AUXIO_ANDROIDIOSTREAM_H
#define AUXIO_ANDROIDIOSTREAM_H
#ifndef AUXIO_JVMINPUTSTREAM_H
#define AUXIO_JVMINPUTSTREAM_H
#include <jni.h>
#include "taglib/tiostream.h"
class AndroidIOStream : public TagLib::IOStream {
class JVMInputStream : public TagLib::IOStream {
public:
AndroidIOStream(JNIEnv *env, jobject &fileRef);
JVMInputStream(JNIEnv *env, jobject inputStream);
~AndroidIOStream();
~JVMInputStream();
AndroidIOStream(const AndroidIOStream &) = delete;
AndroidIOStream &operator=(const AndroidIOStream &) = delete;
JVMInputStream(const JVMInputStream &) = delete;
JVMInputStream &operator=(const JVMInputStream &) = delete;
/*!
* Returns the stream name in the local file system encoding.
@ -99,8 +99,18 @@ public:
private:
JNIEnv *env;
jobject &fileRef;
jobject inputStream;
jmethodID inputStreamNameMethod;
jmethodID inputStreamReadBlockMethod;
jmethodID inputStreamIsOpenMethod;
jmethodID inputStreamSeekFromBeginningMethod;
jmethodID inputStreamSeekFromCurrentMethod;
jmethodID inputStreamSeekFromEndMethod;
jmethodID inputStreamClearMethod;
jmethodID inputStreamTellMethod;
jmethodID inputStreamLengthMethod;
};
#endif //AUXIO_ANDROIDIOSTREAM_H
#endif //AUXIO_JVMINPUTSTREAM_H

View file

@ -1,16 +1,16 @@
#include <jni.h>
#include <string>
#include "AndroidIOStream.h"
#include "JVMInputStream.h"
#include <taglib/fileref.h>
#include <taglib/tag.h>
extern "C" JNIEXPORT jobject JNICALL
Java_org_oxycblt_ktaglib_KTagLib_load(
Java_org_oxycblt_ktaglib_KTagLib_openNative(
JNIEnv* env,
jobject /* this */,
jobject fileRef) {
AndroidIOStream stream { env, fileRef };
jobject inputStream) {
JVMInputStream stream { env, inputStream };
TagLib::FileRef file { &stream };
if (file.isNull()) {
return nullptr;
@ -27,7 +27,8 @@ Java_org_oxycblt_ktaglib_KTagLib_load(
jclass tagClass = env->FindClass("org/oxycblt/ktaglib/Tag");
jmethodID tagInit = env->GetMethodID(tagClass, "<init>", "(Ljava/util/Map;Ljava/util/Map;[B)V");
// Create tag
jobject tagObj = env->NewObject(tagClass, tagInit, id3v2, vorbis, coverData);
return tagObj;
}

View file

@ -0,0 +1,55 @@
package org.oxycblt.ktaglib
import android.content.Context
import java.io.FileInputStream
import java.nio.ByteBuffer
class AndroidInputStream(
context: Context,
fileRef: FileRef
) : NativeInputStream {
private val fileName = fileRef.fileName
private val fd = requireNotNull(context.contentResolver.openFileDescriptor(fileRef.uri, "r")) {
"Failed to open file descriptor for ${fileRef.fileName}"
}
private val fis = FileInputStream(fd.fileDescriptor)
private val channel = fis.channel
override fun name() = fileName
override fun readBlock(length: Long): ByteArray {
val buffer = ByteBuffer.allocate(length.toInt())
channel.read(buffer)
return buffer.array()
}
override fun isOpen(): Boolean {
return channel.isOpen
}
override fun seekFromBeginning(offset: Long) {
channel.position(offset)
}
override fun seekFromCurrent(offset: Long) {
channel.position(channel.position() + offset)
}
override fun seekFromEnd(offset: Long) {
channel.position(channel.size() - offset)
}
override fun clear() {
// Nothing to clear
}
override fun tell() = channel.position()
override fun length() = channel.size()
fun close() {
channel.close()
fis.close()
fd.close()
}
}

View file

@ -1,23 +1,32 @@
package org.oxycblt.ktaglib
import java.io.InputStream
import android.content.Context
import android.net.Uri
object KTagLib {
// Used to load the 'ktaglib' library on application startup.
init {
System.loadLibrary("ktaglib")
}
/**
* A native method that is implemented by the 'ktaglib' native library,
* which is packaged with this application.
* Open a file and extract a tag.
*
* Note: This method is blocking and should be handled as such if
* calling from a coroutine.
*/
external fun load(fileRef: FileRef): Tag?
fun open(context: Context, ref: FileRef): Tag? {
val inputStream = AndroidInputStream(context, ref)
val tag = openNative(inputStream)
inputStream.close()
return tag
}
private external fun openNative(ioStream: AndroidInputStream): Tag?
}
data class FileRef(
val fileName: String,
val inputStream: InputStream
val uri: Uri
)
data class Tag(

View file

@ -0,0 +1,27 @@
package org.oxycblt.ktaglib
/**
* Java interface for the read-only methods in TagLib's IOStream API.
*
* The vast majority of IO shim between Taglib/KTaglib should occur here
* to minimize JNI calls.
*/
interface NativeInputStream {
fun name(): String
fun readBlock(length: Long): ByteArray
fun isOpen(): Boolean
fun seekFromBeginning(offset: Long)
fun seekFromCurrent(offset: Long)
fun seekFromEnd(offset: Long)
fun clear()
fun tell(): Long
fun length(): Long
}