508 lines
16 KiB
C
508 lines
16 KiB
C
//go:build android
|
|
// +build android
|
|
|
|
#include <android/log.h>
|
|
#include <dirent.h>
|
|
#include <jni.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__)
|
|
|
|
static jclass find_class(JNIEnv *env, const char *class_name) {
|
|
jclass clazz = (*env)->FindClass(env, class_name);
|
|
if (clazz == NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
LOG_FATAL("cannot find %s", class_name);
|
|
return NULL;
|
|
}
|
|
return clazz;
|
|
}
|
|
|
|
static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
|
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
|
|
if (m == 0) {
|
|
(*env)->ExceptionClear(env);
|
|
LOG_FATAL("cannot find method %s %s", name, sig);
|
|
return 0;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
|
jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig);
|
|
if (m == 0) {
|
|
(*env)->ExceptionClear(env);
|
|
LOG_FATAL("cannot find method %s %s", name, sig);
|
|
return 0;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
const char* getString(uintptr_t jni_env, uintptr_t ctx, jstring str) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
|
|
const char *chars = (*env)->GetStringUTFChars(env, str, NULL);
|
|
|
|
const char *copy = strdup(chars);
|
|
(*env)->ReleaseStringUTFChars(env, str, chars);
|
|
return copy;
|
|
}
|
|
|
|
jobject parseURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
|
|
jstring uriStr = (*env)->NewStringUTF(env, uriCstr);
|
|
jclass uriClass = find_class(env, "android/net/Uri");
|
|
jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
|
|
|
|
return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr);
|
|
}
|
|
|
|
// clipboard
|
|
|
|
jobject getClipboard(uintptr_t jni_env, uintptr_t ctx) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
|
|
jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
|
|
|
|
jstring service = (*env)->NewStringUTF(env, "clipboard");
|
|
jobject ret = (*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service);
|
|
jthrowable err = (*env)->ExceptionOccurred(env);
|
|
|
|
if (err != NULL) {
|
|
LOG_FATAL("cannot lookup clipboard");
|
|
(*env)->ExceptionClear(env);
|
|
return NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
const char *getClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject mgr = getClipboard(jni_env, ctx);
|
|
if (mgr == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
jclass mgrClass = (*env)->GetObjectClass(env, mgr);
|
|
jmethodID getText = find_method(env, mgrClass, "getText", "()Ljava/lang/CharSequence;");
|
|
|
|
jobject content = (jstring)(*env)->CallObjectMethod(env, mgr, getText);
|
|
if (content == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
jclass clzCharSequence = (*env)->GetObjectClass(env, content);
|
|
jmethodID toString = (*env)->GetMethodID(env, clzCharSequence, "toString", "()Ljava/lang/String;");
|
|
jobject s = (*env)->CallObjectMethod(env, content, toString);
|
|
|
|
return getString(jni_env, ctx, s);
|
|
}
|
|
|
|
void setClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *content) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject mgr = getClipboard(jni_env, ctx);
|
|
if (mgr == NULL) {
|
|
return;
|
|
}
|
|
|
|
jclass mgrClass = (*env)->GetObjectClass(env, mgr);
|
|
jmethodID setText = find_method(env, mgrClass, "setText", "(Ljava/lang/CharSequence;)V");
|
|
|
|
jstring str = (*env)->NewStringUTF(env, content);
|
|
(*env)->CallVoidMethod(env, mgr, setText, str);
|
|
}
|
|
|
|
// file handling
|
|
|
|
jobject getContentResolver(uintptr_t jni_env, uintptr_t ctx) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
|
|
jmethodID getContentResolver = find_method(env, ctxClass, "getContentResolver", "()Landroid/content/ContentResolver;");
|
|
|
|
return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getContentResolver);
|
|
}
|
|
|
|
void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject resolver = getContentResolver(jni_env, ctx);
|
|
|
|
jclass resolverClass = (*env)->GetObjectClass(env, resolver);
|
|
jmethodID openInputStream = find_method(env, resolverClass, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;");
|
|
|
|
jobject uri = parseURI(jni_env, ctx, uriCstr);
|
|
jobject stream = (jobject)(*env)->CallObjectMethod(env, resolver, openInputStream, uri);
|
|
jthrowable loadErr = (*env)->ExceptionOccurred(env);
|
|
|
|
if (loadErr != NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
return NULL;
|
|
}
|
|
|
|
return (*env)->NewGlobalRef(env, stream);
|
|
}
|
|
|
|
void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject resolver = getContentResolver(jni_env, ctx);
|
|
|
|
jclass resolverClass = (*env)->GetObjectClass(env, resolver);
|
|
jmethodID saveOutputStream = find_method(env, resolverClass, "openOutputStream", "(Landroid/net/Uri;Ljava/lang/String;)Ljava/io/OutputStream;");
|
|
|
|
jobject uri = parseURI(jni_env, ctx, uriCstr);
|
|
jstring modes = (*env)->NewStringUTF(env, "wt"); // truncate before write
|
|
jobject stream = (jobject)(*env)->CallObjectMethod(env, resolver, saveOutputStream, uri, modes);
|
|
jthrowable loadErr = (*env)->ExceptionOccurred(env);
|
|
|
|
if (loadErr != NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
return NULL;
|
|
}
|
|
|
|
return (*env)->NewGlobalRef(env, stream);
|
|
}
|
|
|
|
jbyte* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jclass streamClass = (*env)->GetObjectClass(env, stream);
|
|
jmethodID read = find_method(env, streamClass, "read", "([BII)I");
|
|
|
|
jbyteArray data = (*env)->NewByteArray(env, len);
|
|
int count = (int)(*env)->CallIntMethod(env, stream, read, data, 0, len);
|
|
*total = count;
|
|
|
|
if (count == -1) {
|
|
return NULL;
|
|
}
|
|
|
|
jbyte* bytes = (jbyte*)malloc(sizeof(jbyte)*count);
|
|
(*env)->GetByteArrayRegion(env, data, 0, count, bytes);
|
|
return bytes;
|
|
}
|
|
|
|
void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, jbyte* buf, int len) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jclass streamClass = (*env)->GetObjectClass(env, stream);
|
|
jmethodID write = find_method(env, streamClass, "write", "([BII)V");
|
|
|
|
jbyteArray data = (*env)->NewByteArray(env, len);
|
|
(*env)->SetByteArrayRegion(env, data, 0, len, buf);
|
|
|
|
(*env)->CallVoidMethod(env, stream, write, data, 0, len);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
void closeStream(uintptr_t jni_env, uintptr_t ctx, void* stream) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jclass streamClass = (*env)->GetObjectClass(env, stream);
|
|
jmethodID close = find_method(env, streamClass, "close", "()V");
|
|
(*env)->CallVoidMethod(env, stream, close);
|
|
|
|
(*env)->DeleteGlobalRef(env, stream);
|
|
}
|
|
|
|
bool hasPrefix(char* string, char* prefix) {
|
|
size_t lp = strlen(prefix);
|
|
size_t ls = strlen(string);
|
|
if (ls < lp) {
|
|
return false;
|
|
}
|
|
return memcmp(prefix, string, lp) == 0;
|
|
}
|
|
|
|
bool canListContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject resolver = getContentResolver(jni_env, ctx);
|
|
jobject uri = parseURI(jni_env, ctx, uriCstr);
|
|
jthrowable loadErr = (*env)->ExceptionOccurred(env);
|
|
|
|
if (loadErr != NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
return false;
|
|
}
|
|
|
|
jclass contractClass = find_class(env, "android/provider/DocumentsContract");
|
|
if (contractClass == NULL) { // API 19
|
|
return false;
|
|
}
|
|
jmethodID getDoc = find_static_method(env, contractClass, "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;");
|
|
if (getDoc == NULL) { // API 21
|
|
return false;
|
|
}
|
|
jstring docID = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getDoc, uri);
|
|
|
|
jmethodID getTree = find_static_method(env, contractClass, "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;");
|
|
jobject treeUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getTree, uri, docID);
|
|
|
|
jclass resolverClass = (*env)->GetObjectClass(env, resolver);
|
|
jmethodID getType = find_method(env, resolverClass, "getType", "(Landroid/net/Uri;)Ljava/lang/String;");
|
|
jstring type = (jstring)(*env)->CallObjectMethod(env, resolver, getType, treeUri);
|
|
|
|
if (type == NULL) {
|
|
return false;
|
|
}
|
|
|
|
const char *str = getString(jni_env, ctx, type);
|
|
return strcmp(str, "vnd.android.document/directory") == 0;
|
|
}
|
|
|
|
bool canListFileURI(char* uriCstr) {
|
|
// Get file path from URI
|
|
size_t length = strlen(uriCstr)-7;// -7 for 'file://'
|
|
char* path = malloc(sizeof(char)*(length+1));// +1 for '\0'
|
|
memcpy(path, &uriCstr[7], length);
|
|
path[length] = '\0';
|
|
|
|
// Stat path to determine if it points to a directory
|
|
struct stat statbuf;
|
|
int result = stat(path, &statbuf);
|
|
|
|
free(path);
|
|
|
|
return (result == 0) && S_ISDIR(statbuf.st_mode);
|
|
}
|
|
|
|
bool canListURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
if (hasPrefix(uriCstr, "file://")) {
|
|
return canListFileURI(uriCstr);
|
|
} else if (hasPrefix(uriCstr, "content://")) {
|
|
return canListContentURI(jni_env, ctx, uriCstr);
|
|
}
|
|
LOG_FATAL("Unrecognized scheme: %s", uriCstr);
|
|
return false;
|
|
}
|
|
|
|
bool createListableFileURI(char* uriCstr) {
|
|
// Get file path from URI
|
|
size_t length = strlen(uriCstr)-7;// -7 for 'file://'
|
|
char* path = malloc(sizeof(char)*(length+1));// +1 for '\0'
|
|
memcpy(path, &uriCstr[7], length);
|
|
path[length] = '\0';
|
|
|
|
int result = mkdir(path, S_IRWXU);
|
|
free(path);
|
|
|
|
return result == 0;
|
|
}
|
|
|
|
bool createListableURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
if (hasPrefix(uriCstr, "file://")) {
|
|
return createListableFileURI(uriCstr);
|
|
}
|
|
LOG_FATAL("Cannot create directory for scheme: %s", uriCstr);
|
|
return false;
|
|
}
|
|
|
|
const char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject resolver = getContentResolver(jni_env, ctx);
|
|
jobject uri = parseURI(jni_env, ctx, uriCstr);
|
|
jthrowable loadErr = (*env)->ExceptionOccurred(env);
|
|
|
|
if (loadErr != NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
return "";
|
|
}
|
|
|
|
jclass stringClass = find_class(env, "java/lang/String");
|
|
jobjectArray project = (*env)->NewObjectArray(env, 1, stringClass, (*env)->NewStringUTF(env, "_display_name"));
|
|
|
|
jclass resolverClass = (*env)->GetObjectClass(env, resolver);
|
|
jmethodID query = find_method(env, resolverClass, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;");
|
|
|
|
jobject cursor = (jobject)(*env)->CallObjectMethod(env, resolver, query, uri, project, NULL, NULL, NULL);
|
|
jclass cursorClass = (*env)->GetObjectClass(env, cursor);
|
|
|
|
jmethodID first = find_method(env, cursorClass, "moveToFirst", "()Z");
|
|
jmethodID get = find_method(env, cursorClass, "getString", "(I)Ljava/lang/String;");
|
|
|
|
if (((jboolean)(*env)->CallBooleanMethod(env, cursor, first)) == JNI_TRUE) {
|
|
jstring name = (jstring)(*env)->CallObjectMethod(env, cursor, get, 0);
|
|
const char *fname = getString(jni_env, ctx, name);
|
|
return fname;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
char *filePath(char *uriCstr) {
|
|
// Get file path from URI
|
|
size_t length = strlen(uriCstr)-7;// -7 for 'file://'
|
|
char* path = malloc(sizeof(char)*(length+1));// +1 for '\0'
|
|
memcpy(path, &uriCstr[7], length);
|
|
path[length] = '\0';
|
|
|
|
return path;
|
|
}
|
|
|
|
bool deleteFileURI(char *uriCstr) {
|
|
char* path = filePath(uriCstr);
|
|
int result = remove(path);
|
|
|
|
free(path);
|
|
|
|
return result == 0;
|
|
}
|
|
|
|
bool deleteURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
if (!hasPrefix(uriCstr, "file://")) {
|
|
LOG_FATAL("Cannot delete for scheme: %s", uriCstr);
|
|
return false;
|
|
}
|
|
|
|
return deleteFileURI(uriCstr);
|
|
}
|
|
|
|
bool existsFileURI(char* uriCstr) {
|
|
char* path = filePath(uriCstr);
|
|
|
|
// Stat path to determine if it points to an existing file
|
|
struct stat statbuf;
|
|
int result = stat(path, &statbuf);
|
|
|
|
free(path);
|
|
|
|
return result == 0;
|
|
}
|
|
|
|
bool existsURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
if (hasPrefix(uriCstr, "file://")) {
|
|
return existsFileURI(uriCstr);
|
|
}
|
|
LOG_FATAL("Cannot check exists for scheme: %s", uriCstr);
|
|
return false;
|
|
}
|
|
|
|
char* listContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
JNIEnv *env = (JNIEnv*)jni_env;
|
|
jobject resolver = getContentResolver(jni_env, ctx);
|
|
jobject uri = parseURI(jni_env, ctx, uriCstr);
|
|
jthrowable loadErr = (*env)->ExceptionOccurred(env);
|
|
|
|
if (loadErr != NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
return "";
|
|
}
|
|
|
|
jclass contractClass = find_class(env, "android/provider/DocumentsContract");
|
|
if (contractClass == NULL) { // API 19
|
|
return "";
|
|
}
|
|
jmethodID getDoc = find_static_method(env, contractClass, "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;");
|
|
if (getDoc == NULL) { // API 21
|
|
return "";
|
|
}
|
|
jstring docID = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getDoc, uri);
|
|
|
|
jmethodID getChild = find_static_method(env, contractClass, "buildChildDocumentsUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;");
|
|
jobject childrenUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getChild, uri, docID);
|
|
|
|
jclass stringClass = find_class(env, "java/lang/String");
|
|
jobjectArray project = (*env)->NewObjectArray(env, 1, stringClass, (*env)->NewStringUTF(env, "document_id"));
|
|
|
|
jclass resolverClass = (*env)->GetObjectClass(env, resolver);
|
|
jmethodID query = find_method(env, resolverClass, "query", "(Landroid/net/Uri;[Ljava/lang/String;Landroid/os/Bundle;Landroid/os/CancellationSignal;)Landroid/database/Cursor;");
|
|
if (getDoc == NULL) { // API 26
|
|
return "";
|
|
}
|
|
|
|
jobject cursor = (jobject)(*env)->CallObjectMethod(env, resolver, query, childrenUri, project, NULL, NULL);
|
|
jclass cursorClass = (*env)->GetObjectClass(env, cursor);
|
|
jmethodID next = find_method(env, cursorClass, "moveToNext", "()Z");
|
|
jmethodID get = find_method(env, cursorClass, "getString", "(I)Ljava/lang/String;");
|
|
jmethodID getChildURI = find_static_method(env, contractClass, "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;");
|
|
|
|
char *ret = NULL;
|
|
int len = 0;
|
|
while (((jboolean)(*env)->CallBooleanMethod(env, cursor, next)) == JNI_TRUE) {
|
|
jstring childDocId = (jstring)(*env)->CallObjectMethod(env, cursor, get, 0);
|
|
jobject childUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getChildURI, uri, childDocId);
|
|
jclass uriClass = (*env)->GetObjectClass(env, childUri);
|
|
jmethodID toString = (*env)->GetMethodID(env, uriClass, "toString", "()Ljava/lang/String;");
|
|
jstring s = (jstring)(*env)->CallObjectMethod(env, childUri, toString);
|
|
|
|
const char *uid = getString(jni_env, ctx, s);
|
|
|
|
// append
|
|
char *old = ret;
|
|
len = len + strlen(uid) + 1;
|
|
ret = malloc(sizeof(char)*(len+1));
|
|
if (old != NULL) {
|
|
strcpy(ret, old);
|
|
free(old);
|
|
} else {
|
|
ret[0] = '\0';
|
|
}
|
|
strcat(ret, uid);
|
|
strcat(ret, "|");
|
|
}
|
|
|
|
if (ret != NULL) {
|
|
ret[len-1] = '\0';
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
char* listFileURI(char* uriCstr) {
|
|
|
|
size_t uriLength = strlen(uriCstr);
|
|
|
|
// Get file path from URI
|
|
size_t length = uriLength-7;// -7 for 'file://'
|
|
char* path = malloc(sizeof(char)*(length+1));// +1 for '\0'
|
|
memcpy(path, &uriCstr[7], length);
|
|
path[length] = '\0';
|
|
|
|
char *ret = NULL;
|
|
DIR *dfd;
|
|
if ((dfd = opendir(path)) != NULL) {
|
|
struct dirent *dp;
|
|
int len = 0;
|
|
while ((dp = readdir(dfd)) != NULL) {
|
|
if (strcmp(dp->d_name, ".") == 0) {
|
|
// Ignore current directory
|
|
continue;
|
|
}
|
|
if (strcmp(dp->d_name, "..") == 0) {
|
|
// Ignore parent directory
|
|
continue;
|
|
}
|
|
// append
|
|
char *old = ret;
|
|
len = len + uriLength + 1 /* / */ + strlen(dp->d_name) + 1 /* | */;
|
|
ret = malloc(sizeof(char)*(len+1));
|
|
if (old != NULL) {
|
|
strcpy(ret, old);
|
|
free(old);
|
|
} else {
|
|
ret[0] = '\0';
|
|
}
|
|
strcat(ret, uriCstr);
|
|
strcat(ret, "/");
|
|
strcat(ret, dp->d_name);
|
|
strcat(ret, "|");
|
|
}
|
|
if (ret != NULL) {
|
|
ret[len-1] = '\0';
|
|
}
|
|
}
|
|
|
|
free(path);
|
|
|
|
return ret;
|
|
}
|
|
|
|
char* listURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
|
|
if (hasPrefix(uriCstr, "file://")) {
|
|
return listFileURI(uriCstr);
|
|
} else if (hasPrefix(uriCstr, "content://")) {
|
|
return listContentURI(jni_env, ctx, uriCstr);
|
|
}
|
|
LOG_FATAL("Unrecognized scheme: %s", uriCstr);
|
|
return "";
|
|
}
|