I have a class named FJpegStreamReader, that has loaded a jni (.so):
System.loadLibrary("fjpeg");
and has a singleton constructor:
private static volatile FJpegStreamReader mInstances;
public static FJpegStreamReader getInstance() {
if (mInstances == null) {
synchronized (FJpegStreamReader.class) {
if (mInstances == null) {
mInstances = new FJpegStreamReader();
}
}
}
return mInstances;
}
when i invoke it with singleton, it work well,
FJpegStreamReader.getInstance().open("/sdcard/markers.jpg", FJpeg.MODEL_OPEN);
But now, i dont want to use singleton mode, I try to change FJpegStreamReader , and invoke like this :
FJpegStreamReader readerA = new FJpegStreamReader();
FJpegStreamReader readerB = new FJpegStreamReader();
readerA.open("/sdcard/markers1.jpg", FJpeg.MODEL_OPEN);
readerB.open("/sdcard/markers2.jpg", FJpeg.MODEL_OPEN);
Bitmap bmA = readerA.getBackgroundImage();
Bitmap bmB = readerB.getBackgroundImage();
and readerA work well, but readerB goes error. i want it can be instanced more than one time in the same time, how to make that readerA and readerB all work?
25884-25884 I: JNI_OnLoad Called
25884-25884 I: FilePath: /sdcard/markers1.jpg, and OpenMode: 0
25884-25884 I: offset is:15079
25884-25884 I: Start to Parser Data
25884-25884 I: Open Succeed
25884-25884 I: FilePath: /sdcard/markers2.jpg, and OpenMode: 0
25884-25884 E: Fired to Open File(code: -1001)
This is my c++ method:
unique_ptr<FJpeg> f(new FJpeg);
static jboolean
FJpeg_open(JNIEnv *env, jobject thiz, jstring fileName, jint model) {
const char *c_path = NULL;
c_path = env->GetStringUTFChars(fileName, NULL);
LOG_I("FilePath: %s, and OpenMode: %d", c_path, model);
int rel = f->openFile(c_path, model);
if (rel < 0) {
LOG_E("Fired to Open File(code: %d)", rel);
return false;
}
LOG_I("Open Succeed");
return true;
}
...
int FJpeg::openFile() {
if (file_path == NULL || *file_path == '\0' || (open_mode != 0 && open_mode != 1)) return ERROR_CODE_UNSUPPORTED_OPERAND;
if (open_mode == 0) {
f_in.open(file_path, ios::in | ios::binary);
if (f_in.fail()) return ERROR_CODE_FILE_NOT_FOUND;
return checkOffsetAndEnd();
} else {
f_out.open(file_path, ios::out | ios::binary);
if (f_out.fail()) return ERROR_CODE_FILE_NOT_FOUND;
return 0;
}
}
It looks like all your Java FJpegStreamReader objects share a single c++ FJpeg object.
You will need to change that to associate each Java object with a unique FJpeg object, for example bystoring either the raw address in a long field in the Java class, or a handle to it and converting both back to a pointer in your Jni methods.
Related
I am modifying an existing library written in native C to get it to work on Android. To access some hardware elements, I need to use some Java functions. So, I'm using JNI to call Java functions from native C.
My approach is the the following:
Use JNI_OnLoad to get a reference to the Java VM.
Get the JNI environment (JNIEnv)
Find the Java class using FindClass
Create an instance of the Java class using NewObject
Create a reference to the Java method I need to call.
Later on, I call a different function which actually calls the Java method using the previously stored reference. All objects and references are stored in global variables in order to initialize them on JNI_OnLoad and use them at the additional function.
The problem is that when I call the actual Java method I always get 0 (it should return an int). If I check for exceptions, it seems there is one but but I cannot see exactly what is creating the exception.
If I call the method from the JNI_OnLoad function, then it works. This makes me think that some reference is lost when the JNI_OnLoad function exits. I am converting all objects to global references but there must be still something getting lost.
Here is a minimal example of my setup. Any help will be highly appreciated.
Thanks!
JC
Native C:
JNIEnv *jni_env = NULL;
jclass jni_cls = NULL;
jobject jni_obj = NULL;
jmethodID jni_myJavaFunc= NULL;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
(void)reserved;
jint ver = JNI_VERSION_1_6;
if (vm == NULL)
return JNI_ERR;
if ((*vm)->GetEnv(vm, (void **)&jni_env, ver) != JNI_OK)
return JNI_ERR;
jni_cls = (jclass)(*jni_env)->NewGlobalRef(jni_env, (*jni_env)->FindClass(jni_env, "org/java/MyJavaClass"));
if (jni_cls == NULL)
return JNI_ERR;
jmethodID init = (*jni_env)->GetMethodID(jni_env, jni_cls, "<init>", "()V");
if (init == NULL)
return JNI_ERR;
jni_obj = (*jni_env)->NewGlobalRef(jni_env, (*jni_env)->NewObject(jni_env, jni_cls, init));
if (jni_obj == NULL)
return JNI_ERR;
jni_myJavaFunc = (*jni_env)->GetMethodID(jni_env, jni_cls, "myJavaFunc", "()I");
return ver;
}
int myNativeFunc()
{
if (jni_myJavaFunc != NULL) {
(*jni_env)->ExceptionClear(jni_env);
int n = (*jni_env)->CallIntMethod(jni_env, jni_obj, jni_myJavaFunc);
if (!(*jni_env)->ExceptionCheck(jni_env))
return n;
else
return -1;
}
}
Java class:
public class MyJavaClass extends Activity
{
public MyJavaClass()
{
}
public int myJavaFunc()
{
return 1;
}
}
I have a callback class for doing callbacks from native C++ code to Kotlin (not sure if Kotlin/Java makes a difference here, if so, is there any documentation on that?).
I have a working callback with an integer parameter, that I call from different native threads without a problem. Now I want to add a second one that sends a String, but for some reason that doesn't work.
My callback class implementation looks like this:
jclass target;
jmethodID id;
Callback::Callback(JavaVM &jvm, jobject object) : g_jvm(jvm), g_object(object) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (g_env != NULL) {
target = g_env->GetObjectClass(g_object);
id = g_env->GetMethodID(target, "integerCallback", "(I)V");
}
}
void Callback::call(int integerValue, const char *stringValue) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
if (g_jvm.AttachCurrentThread(&g_env, NULL) != 0) {
LOGD("GetEnv: Failed to attach");
}
} else if (getEnvStat == JNI_OK) {
LOGD("GetEnv: JNI_OK");
} else if (getEnvStat == JNI_EVERSION) {
LOGD("GetEnv: version not supported");
}
g_env->CallVoidMethod(g_object, id, (jint) integerValue);
}
It gets instantiated in my native-lib.cpplike this:
extern "C" {
std::unique_ptr<Callback> callback;
JavaVM *g_jvm = nullptr;
static jobject myJNIClass;
jint JNI_OnLoad(JavaVM *pJvm, void *reserved) {
g_jvm = pJvm;
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_com_my_app_common_jni_JniBridge_loadNative(JNIEnv *env, jobject instance,
jstring URI, jboolean isWaitMode) {
myJNIClass = env->NewGlobalRef(instance);
if (callback == nullptr) {
callback = std::make_unique<Callback>(*g_jvm, myJNIClass);
}
}
The callback method it talks to is in my JniBridge.kt (the threading part is probbaly irrelevant to the problem):
object JniBridge {
init {
System.loadLibrary("native-lib")
Timber.d("Native Lib loaded!")
}
fun load(fileName: String, isWaitMode: Boolean) {
loadNative(fileName, isWaitMode)
}
fun integerCallback(value: Int) {
someListener?.invoke(value)
}
private external fun loadNative(fileName: String, isWaitMode: Boolean)
}
So now if my native code triggers the call() method in my callback, my integerCallback() in JniBridge.kt gets called correctly with an integer.
But here's what I don't get: If I change my callback to send a String it doesn't work. If I change it like this:
// in JniBridge.kt
fun stringCallback(value: String) {
someListener?.invoke(value)
}
// in Callback.cpp
//getting the method
id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String)V");
//making the call
g_env->CallVoidMethod(g_object, id, (jstring) stringValue);
Now my app crashes with this error:
JNI DETECTED ERROR IN APPLICATION: JNI GetStringUTFChars called with pending exception java.lang.NoSuchMethodError: no non-static method "Lcom/my/app/common/jni/JniBridge;.stringCallback(Ljava/lang/String)V"
The same happens if I try calling a void method (like this: id = g_env->GetMethodID(target, "voidCallback", "(V)V");or calling one that takes two integers (like this: id = g_env->GetMethodID(target, "twoIntegerCallback", "(I;I)V");, of course with having the corresponding methods in JniBridge.kt in place.
Why does this happen and how can I fix it?
Note: For clarity I have omitted all parts of my code that I believe are not related to the problem, if something crucial is missing, please let me know and I fix it.
You're missing a semi-colon in the method signature that you pass to GetMethodID.
It should be:
id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String;)V");
Note the semi-colon after Ljava/lang/String.
See the part about Type Signatures in Oracle's JNI documentation.
Regarding your other variations:
A Java void function() would have the signature ()V.
A Java void function(int i, int j) would have the signature (II)V.
As a side note, you really ought to verify the return value and check for exceptions after every JNI call.
Background
I need to parse some zip files of various types (getting some inner files content for one purpose or another, including getting their names).
Some of the files are not reachable via file-path, as Android has Uri to reach them, and as sometimes the zip file is inside another zip file. With the push to use SAF, it's even less possible to use file-path in some cases.
For this, we have 2 main ways to handle: ZipFile class and ZipInputStream class.
The problem
When we have a file-path, ZipFile is a perfect solution. It's also very efficient in terms of speed.
However, for the rest of the cases, ZipInputStream could reach issues, such as this one, which has a problematic zip file, and cause this exception:
java.util.zip.ZipException: only DEFLATED entries can have EXT descriptor
at java.util.zip.ZipInputStream.readLOC(ZipInputStream.java:321)
at java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:124)
What I've tried
The only always-working solution would be to copy the file to somewhere else, where you could parse it using ZipFile, but this is inefficient and requires you to have free storage, as well as remove the file when you are done with it.
So, what I've found is that Apache has a nice, pure Java library (here) to parse Zip files, and for some reason its InputStream solution (called "ZipArchiveInputStream") seem even more efficient than the native ZipInputStream class.
As opposed to what we have in the native framework, the library offers a bit more flexibility. I could, for example, load the entire zip file into bytes array, and let the library handle it as usual, and this works even for the problematic Zip files I've mentioned:
org.apache.commons.compress.archivers.zip.ZipFile(SeekableInMemoryByteChannel(byteArray)).use { zipFile ->
for (entry in zipFile.entries) {
val name = entry.name
... // use the zipFile like you do with native framework
gradle dependency:
// http://commons.apache.org/proper/commons-compress/ https://mvnrepository.com/artifact/org.apache.commons/commons-compress
implementation 'org.apache.commons:commons-compress:1.20'
Sadly, this isn't always possible, because it depends on having the heap memory hold the entire zip file, and on Android it gets even more limited, because the heap size could be relatively small (heap could be 100MB while the file is 200MB). As opposed to a PC which can have a huge heap memory being set, for Android it's not flexible at all.
So, I searched for a solution that has JNI instead, to have the entire ZIP file loaded into byte array there, not going to the heap (at least not entirely). This could be a nicer workaround because if the ZIP could be fit in the device's RAM instead of the heap, it could prevent me from reaching OOM while also not needing to have an extra file.
I've found this library called "larray" which seems promising , but sadly when I tried using it, it crashed, because its requirements include having a full JVM, meaning not suitable for Android.
EDIT: seeing that I can't find any library and any built-in class, I tried to use JNI myself. Sadly I'm very rusty with it, and I looked at an old repository I've made a long time ago to perform some operations on Bitmaps (here). This is what I came up with :
native-lib.cpp
#include <jni.h>
#include <android/log.h>
#include <cstdio>
#include <android/bitmap.h>
#include <cstring>
#include <unistd.h>
class JniBytesArray {
public:
uint32_t *_storedData;
JniBytesArray() {
_storedData = NULL;
}
};
extern "C" {
JNIEXPORT jobject JNICALL Java_com_lb_myapplication_JniByteArrayHolder_allocate(
JNIEnv *env, jobject obj, jlong size) {
auto *jniBytesArray = new JniBytesArray();
auto *array = new uint32_t[size];
for (int i = 0; i < size; ++i)
array[i] = 0;
jniBytesArray->_storedData = array;
return env->NewDirectByteBuffer(jniBytesArray, 0);
}
}
JniByteArrayHolder.kt
class JniByteArrayHolder {
external fun allocate(size: Long): ByteBuffer
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
thread {
printMemStats()
val jniByteArrayHolder = JniByteArrayHolder()
val byteBuffer = jniByteArrayHolder.allocate(1L * 1024L)
printMemStats()
}
}
fun printMemStats() {
val memoryInfo = ActivityManager.MemoryInfo()
(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
val nativeHeapSize = memoryInfo.totalMem
val nativeHeapFreeSize = memoryInfo.availMem
val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
Log.d("AppLog", "total:${Formatter.formatFileSize(this, nativeHeapSize)} " +
"free:${Formatter.formatFileSize(this, nativeHeapFreeSize)} " +
"used:${Formatter.formatFileSize(this, usedMemInBytes)} ($usedMemInPercentage%)")
}
This doesn't seem right, because if I try to create a 1GB byte array using jniByteArrayHolder.allocate(1L * 1024L * 1024L * 1024L) , it crashes without any exception or error logs.
The questions
Is it possible to use JNI for Apache's library, so that it will handle the ZIP file content which is contained within JNI's "world" ?
If so, how can I do it? Is there any sample of how to do it? Is there a class for it? Or do I have to implement it myself? If so, can you please show how it's done in JNI?
If it's not possible, what other way is there to do it? Maybe alternative to what Apache has?
For the solution of JNI, how come it doesn't work well ? How could I efficiently copy the bytes from the stream into the JNI byte array (my guess is that it will be via a buffer)?
I took a look at the JNI code you posted and made a couple of changes. Mostly it is defining the size argument for NewDirectByteBuffer and using malloc().
Here is the output of the log after allocating 800mb:
D/AppLog: total:1.57 GB free:1.03 GB used:541 MB (34%)
D/AppLog: total:1.57 GB free:247 MB used:1.32 GB (84%)
And the following is what the buffer looks like after the allocation. As you can see, the debugger is reporting a limit of 800mb which is what we expect.
My C is very rusty, so I am sure that there is some work to be done. I have updated the code to be a little more robust and to allow for the freeing of memory.
native-lib.cpp
extern "C" {
static jbyteArray *_holdBuffer = NULL;
static jobject _directBuffer = NULL;
/*
This routine is not re-entrant and can handle only one buffer at a time. If a buffer is
allocated then it must be released before the next one is allocated.
*/
JNIEXPORT
jobject JNICALL Java_com_example_zipfileinmemoryjni_JniByteArrayHolder_allocate(
JNIEnv *env, jobject obj, jlong size) {
if (_holdBuffer != NULL || _directBuffer != NULL) {
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Call to JNI allocate() before freeBuffer()");
return NULL;
}
// Max size for a direct buffer is the max of a jint even though NewDirectByteBuffer takes a
// long. Clamp max size as follows:
if (size > SIZE_T_MAX || size > INT_MAX || size <= 0) {
jlong maxSize = SIZE_T_MAX < INT_MAX ? SIZE_T_MAX : INT_MAX;
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Native memory allocation request must be >0 and <= %lld but was %lld.\n",
maxSize, size);
return NULL;
}
jbyteArray *array = (jbyteArray *) malloc(static_cast<size_t>(size));
if (array == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Failed to allocate %lld bytes of native memory.\n",
size);
return NULL;
}
jobject directBuffer = env->NewDirectByteBuffer(array, size);
if (directBuffer == NULL) {
free(array);
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Failed to create direct buffer of size %lld.\n",
size);
return NULL;
}
// memset() is not really needed but we call it here to force Android to count
// the consumed memory in the stats since it only seems to "count" dirty pages. (?)
memset(array, 0xFF, static_cast<size_t>(size));
_holdBuffer = array;
// Get a global reference to the direct buffer so Java isn't tempted to GC it.
_directBuffer = env->NewGlobalRef(directBuffer);
return directBuffer;
}
JNIEXPORT void JNICALL Java_com_example_zipfileinmemoryjni_JniByteArrayHolder_freeBuffer(
JNIEnv *env, jobject obj, jobject directBuffer) {
if (_directBuffer == NULL || _holdBuffer == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Attempt to free unallocated buffer.");
return;
}
jbyteArray *bufferLoc = (jbyteArray *) env->GetDirectBufferAddress(directBuffer);
if (bufferLoc == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"Failed to retrieve direct buffer location associated with ByteBuffer.");
return;
}
if (bufferLoc != _holdBuffer) {
__android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
"DirectBuffer does not match that allocated.");
return;
}
// Free the malloc'ed buffer and the global reference. Java can not GC the direct buffer.
free(bufferLoc);
env->DeleteGlobalRef(_directBuffer);
_holdBuffer = NULL;
_directBuffer = NULL;
}
}
I also updated the array holder:
class JniByteArrayHolder {
external fun allocate(size: Long): ByteBuffer
external fun freeBuffer(byteBuffer: ByteBuffer)
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
I can confirm that this code along with the ByteBufferChannel class provided by Botje here works for Android versions before API 24. The SeekableByteChannel interface was introduced in API 24 and is needed by the ZipFile utility.
The maximum buffer size that can be allocated is the size of a jint and is due to the limitation of JNI. Larger data can be accommodated (if available) but would require multiple buffers and a way to handle them.
Here is the main activity for the sample app. An earlier version always assumed the the InputStream read buffer was was always filled and errored out when trying to put it to the ByteBuffer. This was fixed.
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun onClick(view: View) {
button.isEnabled = false
status.text = getString(R.string.running)
thread {
printMemStats("Before buffer allocation:")
var bufferSize = 0L
// testzipfile.zip is not part of the project but any zip can be uploaded through the
// device file manager or adb to test.
val fileToRead = "$filesDir/testzipfile.zip"
val inStream =
if (File(fileToRead).exists()) {
FileInputStream(fileToRead).apply {
bufferSize = getFileSize(this)
close()
}
FileInputStream(fileToRead)
} else {
// If testzipfile.zip doesn't exist, we will just look at this one which
// is part of the APK.
resources.openRawResource(R.raw.appapk).apply {
bufferSize = getFileSize(this)
close()
}
resources.openRawResource(R.raw.appapk)
}
// Allocate the buffer in native memory (off-heap).
val jniByteArrayHolder = JniByteArrayHolder()
val byteBuffer =
if (bufferSize != 0L) {
jniByteArrayHolder.allocate(bufferSize)?.apply {
printMemStats("After buffer allocation")
}
} else {
null
}
if (byteBuffer == null) {
Log.d("Applog", "Failed to allocate $bufferSize bytes of native memory.")
} else {
Log.d("Applog", "Allocated ${Formatter.formatFileSize(this, bufferSize)} buffer.")
val inBytes = ByteArray(4096)
Log.d("Applog", "Starting buffered read...")
while (inStream.available() > 0) {
byteBuffer.put(inBytes, 0, inStream.read(inBytes))
}
inStream.close()
byteBuffer.flip()
ZipFile(ByteBufferChannel(byteBuffer)).use {
Log.d("Applog", "Starting Zip file name dump...")
for (entry in it.entries) {
Log.d("Applog", "Zip name: ${entry.name}")
val zis = it.getInputStream(entry)
while (zis.available() > 0) {
zis.read(inBytes)
}
}
}
printMemStats("Before buffer release:")
jniByteArrayHolder.freeBuffer(byteBuffer)
printMemStats("After buffer release:")
}
runOnUiThread {
status.text = getString(R.string.idle)
button.isEnabled = true
Log.d("Applog", "Done!")
}
}
}
/*
This function is a little misleading since it does not reflect the true status of memory.
After native buffer allocation, it waits until the memory is used before counting is as
used. After release, it doesn't seem to count the memory as released until garbage
collection. (My observations only.) Also, see the comment for memset() in native-lib.cpp
which is a member of this project.
*/
private fun printMemStats(desc: String? = null) {
val memoryInfo = ActivityManager.MemoryInfo()
(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
val nativeHeapSize = memoryInfo.totalMem
val nativeHeapFreeSize = memoryInfo.availMem
val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
val sDesc = desc?.run { "$this:\n" }
Log.d(
"AppLog", "$sDesc total:${Formatter.formatFileSize(this, nativeHeapSize)} " +
"free:${Formatter.formatFileSize(this, nativeHeapFreeSize)} " +
"used:${Formatter.formatFileSize(this, usedMemInBytes)} ($usedMemInPercentage%)"
)
}
// Not a great way to do this but not the object of the demo.
private fun getFileSize(inStream: InputStream): Long {
var bufferSize = 0L
while (inStream.available() > 0) {
val toSkip = inStream.available().toLong()
inStream.skip(toSkip)
bufferSize += toSkip
}
return bufferSize
}
}
A sample GitHub repository is here.
You can steal LWJGL's native memory management functions. It is BSD3 licensed, so you only have to mention somewhere that you are using code from it.
Step 1: given an InputStream is and a file size ZIP_SIZE, slurp the stream into a direct byte buffer created by LWJGL's org.lwjgl.system.MemoryUtil helper class:
ByteBuffer bb = MemoryUtil.memAlloc(ZIP_SIZE);
byte[] buf = new byte[4096]; // Play with the buffer size to see what works best
int read = 0;
while ((read = is.read(buf)) != -1) {
bb.put(buf, 0, read);
}
Step 2: wrap the ByteBuffer in a ByteChannel.
Taken from this gist. You possibly want to strip the writing parts out.
package io.github.ncruces.utils;
import java.nio.ByteBuffer;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import static java.lang.Math.min;
public final class ByteBufferChannel implements SeekableByteChannel {
private final ByteBuffer buf;
public ByteBufferChannel(ByteBuffer buffer) {
if (buffer == null) throw new NullPointerException();
buf = buffer;
}
#Override
public synchronized int read(ByteBuffer dst) {
if (buf.remaining() == 0) return -1;
int count = min(dst.remaining(), buf.remaining());
if (count > 0) {
ByteBuffer tmp = buf.slice();
tmp.limit(count);
dst.put(tmp);
buf.position(buf.position() + count);
}
return count;
}
#Override
public synchronized int write(ByteBuffer src) {
if (buf.isReadOnly()) throw new NonWritableChannelException();
int count = min(src.remaining(), buf.remaining());
if (count > 0) {
ByteBuffer tmp = src.slice();
tmp.limit(count);
buf.put(tmp);
src.position(src.position() + count);
}
return count;
}
#Override
public synchronized long position() {
return buf.position();
}
#Override
public synchronized ByteBufferChannel position(long newPosition) {
if ((newPosition | Integer.MAX_VALUE - newPosition) < 0) throw new IllegalArgumentException();
buf.position((int)newPosition);
return this;
}
#Override
public synchronized long size() { return buf.limit(); }
#Override
public synchronized ByteBufferChannel truncate(long size) {
if ((size | Integer.MAX_VALUE - size) < 0) throw new IllegalArgumentException();
int limit = buf.limit();
if (limit > size) buf.limit((int)size);
return this;
}
#Override
public boolean isOpen() { return true; }
#Override
public void close() {}
}
Step 3: Use ZipFile as before:
ZipFile zf = new ZipFile(ByteBufferChannel(bb);
for (ZipEntry ze : zf) {
...
}
Step 4: Manually release the native buffer (preferably in a finally block):
MemoryUtil.memFree(bb);
I need to access a boolean or int field in activity from jni which is not static, but i get this JNI Warning and throws exception
JNI WARNING: instance fieldID 0x46488338 not valid for class Ljava/lang/Class;
in Lcom/example/filehandler/FileHandlerActivity;.beginFileOperation:(Ljava/lang/String;)I (GetIntField)
I am able to access a static field, why it's not able to access an instance field?
Here is the JNI code i used to access int field in Activity:
jint Java_com_example_filehandler_FileHandlerActivity_beginFileOperation(JNIEnv *env,jobject obj,jstring path)
{
target=(*env)->GetStringUTFChars(env,path,0);
jclass cls=(*env)->GetObjectClass(env,obj);
//jclass cls=(*env)->FindClass(env,"com/example/filehandler/FileHandlerActivity");
if(cls==NULL)
{
LOG_INFO("jni : class not found");
return 0;
}
jfieldID fid=(*env)->GetFieldID(env,cls,"status","I");
if(fid==NULL)
{
LOG_INFO("jni : field not found");
return 0;
}
else
{
LOG_INFO("jni : field found");
}
sdcard_status=(*env)->GetIntField(env,obj,fid);
LOG_INFO("jni : sdcard status = %d",sdcard_status);
//writeToFile(target);
(*env)->ReleaseStringUTFChars(env,path,target);
return 1;
}
(Android, NDK, C++, OpenGL ES)
I need a way to reliably receive the text input from a (soft)keyboard.
The solution can be through Java using a NativeActivity subclass, or anything which works.
At the end I need whatever text is being typed, so I can render it myself with OpenGL
Some background:
Up until now I was triggering the soft keyboard by calling showSoftInput or hideSoftInputFromWindow thought JNI. This never failed so far.
However, the problem is the native activity will not send all characters. Especially some unicode characters outside of ASCII range, or some motion soft keyboard won't work (AKeyEvent_getKeyCode)
It used to be possible to get some of those other unicode characters why checking for KeyEvent.ACTION_MULTIPLE and reading a string of characters.
But even this won't work reliably anymore.
So far I failed to find an alternative method.
I experimented with programmatically adding a EditText, but never got it to work. Even trying to add a simple Button resulted in the OpenGL view to no longer being rendered.
On iOS I worked around it by having a hiding edit box, which I simply activated to make the keyboard show up. I would then read out the edit box and use the string to render myself in OpenGL.
I have the same issues, and I have solved it using a 'Character' event that I process separately from the InputEvent.
The problem is this: AKeyEvent_getKeyCode doesn't return the KeyCode for some softkey events, notably the expanded 'unicode/latin' characters when you hold down a key. This prevents the methods #Shammi and #eozgonul from working because the KeyEvent reconstructed on the Java side doesn't have enough information to get a unicode character.
Another issue is that the InputQueue is drained on the C++/Native side before the dispatchKeyEvent event(s) are fired. This means that the KEYDOWN/KEYUP events all fired before the Java code can process the events. (They are not interleaved).
My solution is to capture the unicode characters on the Java side by overriding dispatchKeyEvent and sending the characters to a Queue<Integer> queueLastInputCharacter = new ConcurrentLinkedQueue<Integer>();
// [JAVA]
#Override
public boolean dispatchKeyEvent (KeyEvent event)
{
int metaState = event.getMetaState();
int unichar = event.getUnicodeChar(metaState);
// We are queuing the Unicode version of the characters for
// sending to the app during processEvents() call.
// We Queue the KeyDown and ActionMultiple Event UnicodeCharacters
if(event.getAction()==KeyEvent.ACTION_DOWN){
if(unichar != 0){
queueLastInputCharacter.offer(Integer.valueOf(unichar));
}
else{
unichar = event.getUnicodeChar();
if(unichar != 0){
queueLastInputCharacter.offer(Integer.valueOf(unichar));
}
else if (event.getDisplayLabel() != 0){
String aText = new String();
aText = "";
aText += event.getDisplayLabel();
queueLastInputCharacter.offer(Integer.valueOf(Character.codePointAt(aText, 0)));
}
else
queueLastInputCharacter.offer(Integer.valueOf(0));
}
}
else if(event.getAction()==KeyEvent.ACTION_MULTIPLE){
unichar = (Character.codePointAt(event.getCharacters(), 0));
queueLastInputCharacter.offer(Integer.valueOf(unichar));
}
return super.dispatchKeyEvent(event);
}
The concurrent queue is going to let the threads play nice together.
I have a Java side method that returns the last input character:
// [JAVA]
public int getLastUnicodeChar(){
if(!queueLastInputCharacter.isEmpty())
return queueLastInputCharacter.poll().intValue();
return 0;
}
At the end of my looper code, I tacked on an extra check to see if the queue retained any unicode characters:
// [C++]
int ident;
int events;
struct android_poll_source* source;
// If not rendering, we will block 250ms waiting for events.
// If animating, we loop until all events are read, then continue
// to draw the next frame of animation.
while ((ident = ALooper_pollAll(((nv_app_status_focused(_lpApp)) ? 1 : 250),
NULL,
&events,
(void**)&source)) >= 0)
{
// Process this event.
if (source != NULL)
source->process(_lpApp, source);
// Check if we are exiting. If so, dump out
if (!nv_app_status_running(_lpApp))
return;
}
static int modtime = 10; // let's not run on every call
if(--modtime == 0) {
long uniChar = androidUnicodeCharFromKeyEvent();
while (uniChar != 0) {
KEvent kCharEvent; // Game engine event
kCharEvent.ptkKey = K_VK_ERROR;
kCharEvent.unicodeChar = uniChar;
kCharEvent.character = uniChar;
/* Send unicode char */
kCharEvent.type = K_EVENT_UNICHAR;
_lpPortableHandler(&kCharEvent);
if (kCharEvent.character < 127) {
/* Send ascii char for source compatibility as well */
kCharEvent.type = K_EVENT_CHAR;
_lpPortableHandler(&kCharEvent);
}
uniChar = androidUnicodeCharFromKeyEvent();
}
modtime = 10;
}
The androidUnicodeCharFromKeyEvent function is very similar to #Shammi 's GetStringFromAInputEvent method, only use CallIntMethod to return the jint.
Notes
This does require modifying your engine to process character events separate from Key events. Android still has key codes like AKEYCODE_BACK or AKEYCODE_ENTER that are not character events and still need to be handled (and can be handled on the main input looper).
Editboxes, consoles, etc... Things that are expecting user input can be modified to receive a separate character event that builds the string. If you are working on multiple platforms, then you will need to generate these new character events in addition to the normal key input events.
I hope this works for you, worked for me so far.
int GetUnicodeChar(struct android_app* app, int eventType, int keyCode, int metaState)
{
JavaVM* javaVM = app->activity->vm;
JNIEnv* jniEnv = app->activity->env;
JavaVMAttachArgs attachArgs;
attachArgs.version = JNI_VERSION_1_6;
attachArgs.name = "NativeThread";
attachArgs.group = NULL;
jint result = javaVM->AttachCurrentThread(&jniEnv, &attachArgs);
if(result == JNI_ERR)
{
return 0;
}
jclass class_key_event = jniEnv->FindClass("android/view/KeyEvent");
int unicodeKey;
if(metaState == 0)
{
jmethodID method_get_unicode_char = jniEnv->GetMethodID(class_key_event, "getUnicodeChar", "()I");
jmethodID eventConstructor = jniEnv->GetMethodID(class_key_event, "<init>", "(II)V");
jobject eventObj = jniEnv->NewObject(class_key_event, eventConstructor, eventType, keyCode);
unicodeKey = jniEnv->CallIntMethod(eventObj, method_get_unicode_char);
}
else
{
jmethodID method_get_unicode_char = jniEnv->GetMethodID(class_key_event, "getUnicodeChar", "(I)I");
jmethodID eventConstructor = jniEnv->GetMethodID(class_key_event, "<init>", "(II)V");
jobject eventObj = jniEnv->NewObject(class_key_event, eventConstructor, eventType, keyCode);
unicodeKey = jniEnv->CallIntMethod(eventObj, method_get_unicode_char, metaState);
}
javaVM->DetachCurrentThread();
LOGI("Unicode key is: %d", unicodeKey);
return unicodeKey;
}
Just call it from your input handler, my structure is approximately as follows:
switch (AInputEvent_getType(event))
{
case AINPUT_EVENT_TYPE_KEY:
switch (AKeyEvent_getAction(event))
{
case AKEY_EVENT_ACTION_DOWN:
int key = AKeyEvent_getKeyCode(event);
int metaState = AKeyEvent_getMetaState(event);
int uniValue;
if(metaState != 0)
uniValue = GetUnicodeChar(app, AKEY_EVENT_ACTION_DOWN, key, metaState);
else
uniValue = GetUnicodeChar(app, AKEY_EVENT_ACTION_DOWN, key, 0);
Since you stated that you already open the soft keyboard, I don't go into that part but the code is kind of straight forward. I basically use the Java function of class KeyEvent which has GetUnicodeChar function.
Eozgonul's solution worked for me. I adopted it and modified it to split the work between Java and the native side. Basically I extend NativeActivity to derive my own class which allows me to move as much as possible to Java. I also ended up passing all the data from the input event. I wanted to make sure I captured as much as possible in the created KeyEvent object.
package com.MyCompany.MyApp;
import android.os.Bundle;
import android.view.inputmethod.InputMethodManager;
import android.content.Context;
import android.view.KeyEvent;
public class MyNativeActivity extends android.app.NativeActivity
{
// Need this for screen rotation to send configuration changed callbacks to native
#Override
public void onConfigurationChanged( android.content.res.Configuration newConfig )
{
super.onConfigurationChanged( newConfig );
}
public void showKeyboard()
{
InputMethodManager imm = ( InputMethodManager )getSystemService( Context.INPUT_METHOD_SERVICE );
imm.showSoftInput( this.getWindow().getDecorView(), InputMethodManager.SHOW_FORCED );
}
public void hideKeyboard()
{
InputMethodManager imm = ( InputMethodManager )getSystemService( Context.INPUT_METHOD_SERVICE );
imm.hideSoftInputFromWindow( this.getWindow().getDecorView().getWindowToken(), 0 );
}
public String stringFromKeyCode( long downTime, long eventTime,
int eventAction, int keyCode, int repeatCount, int metaState,
int deviceId, int scanCode, int flags, int source )
{
String strReturn;
KeyEvent keyEvent = new KeyEvent( downTime, eventTime, eventAction, keyCode, repeatCount, metaState, deviceId, scanCode, flags, source );
if ( metaState == 0 )
{
int unicodeChar = keyEvent.getUnicodeChar();
if ( eventAction == KeyEvent.ACTION_MULTIPLE && unicodeChar == keyEvent.KEYCODE_UNKNOWN )
{
strReturn = keyEvent.getCharacters();
}
else
{
strReturn = Character.toString( ( char )unicodeChar );
}
}
else
{
strReturn = Character.toString( ( char )( keyEvent.getUnicodeChar( metaState ) ) );
}
return strReturn;
}
}
On the native side...
std::string GetStringFromAInputEvent( android_app* pApp, AInputEvent* pInputEvent )
{
std::string strReturn;
JavaVM* pJavaVM = pApp->activity->vm;
JNIEnv* pJNIEnv = pApp->activity->env;
JavaVMAttachArgs javaVMAttachArgs;
javaVMAttachArgs.version = JNI_VERSION_1_6;
javaVMAttachArgs.name = "NativeThread";
javaVMAttachArgs.group = NULL;
jint jResult;
jResult = pJavaVM->AttachCurrentThread( &pJNIEnv, &javaVMAttachArgs );
if ( jResult != JNI_ERR )
{
// Retrieves NativeActivity.
jobject nativeActivity = pNativeActivity->clazz;
jclass ClassNativeActivity = pJNIEnv->GetObjectClass( nativeActivity );
jmethodID MethodStringFromKeyCode = pJNIEnv->GetMethodID( ClassNativeActivity, "stringFromKeyCode", "(JJIIIIIIII)Ljava/lang/String;" );
jlong jDownTime = AKeyEvent_getDownTime( pInputEvent );
jlong jEventTime = AKeyEvent_getEventTime( pInputEvent );
jint jEventAction = AKeyEvent_getAction( pInputEvent );
jint jKeyCode = AKeyEvent_getKeyCode( pInputEvent );
jint jRepeatCount = AKeyEvent_getRepeatCount( pInputEvent );
jint jMetaState = AKeyEvent_getMetaState( pInputEvent );
jint jDeviceID = AInputEvent_getDeviceId( pInputEvent );
jint jScanCode = AKeyEvent_getScanCode( pInputEvent );
jint jFlags = AKeyEvent_getFlags( pInputEvent );
jint jSource = AInputEvent_getSource( pInputEvent );
jstring jKeyCodeString = ( jstring )pJNIEnv->CallObjectMethod( nativeActivity, MethodStringFromKeyCode,
jDownTime, jEventTime, jEventAction,
jKeyCode, jRepeatCount, jMetaState,
jDeviceID, jScanCode, jFlags, jSource );
const char* keyCodeString = pJNIEnv->GetStringUTFChars( keyCodeString, nullptr );
strReturn = std::string( keyCodeString );
pJNIEnv->ReleaseStringUTFChars( jKeyCodeString, keyCodeString );
// Finished with the JVM.
pJavaVM->DetachCurrentThread();
}
return strReturn;
}
The 2 reasons I went with this approach..
Reduces code syntax complexity by moving code to java and only having you to call one jni wrapper method on the native side.
Java is the preferred Android language and this allows me to quickly iterate on java based solutions. Moreover most existing solutions are in java.
Basically this will solve the issue.
NativeActivity override onKeyDown()
But you'll have to implement some other way than the NDK key input to get the onKeyMultiple's event.getCharacters() string into your code.