collabora-online/android/lib/src/main/cpp/androidapp.cpp

558 lines
21 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <config.h>
#include <jni.h>
#include <android/log.h>
#include <chrono>
#include <thread>
#include <FakeSocket.hpp>
#include <Kit.hpp>
#include <Log.hpp>
#include <LOOLWSD.hpp>
#include <Protocol.hpp>
#include <SetupKitEnvironment.hpp>
#include <Util.hpp>
#include <osl/detail/android-bootstrap.h>
#include <Poco/Base64Encoder.h>
const int SHOW_JS_MAXLEN = 70;
int loolwsd_server_socket_fd = -1;
static std::string fileURL;
static int fakeClientFd;
static int closeNotificationPipeForForwardingThread[2] = {-1, -1};
static JavaVM* javaVM = nullptr;
static bool lokInitialized = false;
// Remember the reference to the LOActivity
jclass g_loActivityClz = nullptr;
jobject g_loActivityObj = nullptr;
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void*) {
javaVM = vm;
libreofficekit_set_javavm(vm);
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // JNI version not supported.
}
setupKitEnvironment("");
// Uncomment the following to see the logs from the core too
//setenv("SAL_LOG", "+WARN+INFO", 0);
Log::initialize("Mobile", "debug", false, false, {});
return JNI_VERSION_1_6;
}
// Exception safe JVM detach, JNIEnv is TLS for Java - so per-thread.
class JNIThreadContext
{
JNIEnv *_env;
public:
JNIThreadContext()
{
assert(javaVM != nullptr);
jint res = javaVM->GetEnv((void**)&_env, JNI_VERSION_1_6);
if (res == JNI_EDETACHED) {
LOG_DBG("Attach worker thread");
res = javaVM->AttachCurrentThread(&_env, nullptr);
if (JNI_OK != res) {
LOG_DBG("Failed to AttachCurrentThread");
}
}
else if (res == JNI_EVERSION) {
LOG_DBG("GetEnv version not supported");
return;
}
else if (res != JNI_OK) {
LOG_DBG("GetEnv another error " << res);
return;
}
}
~JNIThreadContext()
{
javaVM->DetachCurrentThread();
}
JNIEnv *getEnv() const { return _env; }
};
static void send2JS(const JNIThreadContext &jctx, const std::vector<char>& buffer)
{
LOG_DBG("Send to JS: " << LOOLProtocol::getAbbreviatedMessage(buffer.data(), buffer.size()));
std::string js;
// Check if the message is binary. We say that any message that isn't just a single line is
// "binary" even if that strictly speaking isn't the case; for instance the commandvalues:
// message has a long bunch of non-binary JSON on multiple lines. But _onMessage() in Socket.js
// handles it fine even if such a message, too, comes in as an ArrayBuffer. (Look for the
// "textMsg = String.fromCharCode.apply(null, imgBytes);".)
const char *newline = (const char *)memchr(buffer.data(), '\n', buffer.size());
if (newline != nullptr)
{
// The data needs to be an ArrayBuffer
std::stringstream ss;
ss << "Base64ToArrayBuffer('";
Poco::Base64Encoder encoder(ss);
encoder.rdbuf()->setLineLength(0); // unlimited
encoder << std::string(buffer.data(), buffer.size());
encoder.close();
ss << "')";
js = ss.str();
}
else
{
const unsigned char *ubufp = (const unsigned char *)buffer.data();
std::vector<char> data;
data.push_back('\'');
for (int i = 0; i < buffer.size(); i++)
{
if (ubufp[i] < ' ' || ubufp[i] == '\'' || ubufp[i] == '\\')
{
data.push_back('\\');
data.push_back('x');
data.push_back("0123456789abcdef"[(ubufp[i] >> 4) & 0x0F]);
data.push_back("0123456789abcdef"[ubufp[i] & 0x0F]);
}
else
{
data.push_back(ubufp[i]);
}
}
data.push_back('\'');
js = std::string(data.data(), data.size());
}
std::string subjs = js.substr(0, std::min(std::string::size_type(SHOW_JS_MAXLEN), js.length()));
if (js.length() > SHOW_JS_MAXLEN)
subjs += "...";
LOG_DBG("Sending to JavaScript: " << subjs);
JNIEnv *env = jctx.getEnv();
jstring jstr = env->NewStringUTF(js.c_str());
jmethodID callFakeWebsocket = env->GetMethodID(g_loActivityClz, "callFakeWebsocketOnMessage", "(Ljava/lang/String;)V");
env->CallVoidMethod(g_loActivityObj, callFakeWebsocket, jstr);
env->DeleteLocalRef(jstr);
if (env->ExceptionCheck())
env->ExceptionDescribe();
}
void postDirectMessage(std::string message)
{
JNIThreadContext ctx;
JNIEnv *env = ctx.getEnv();
jstring jstr = env->NewStringUTF(message.c_str());
jmethodID callPostMobileMessage = env->GetMethodID(g_loActivityClz, "postMobileMessage", "(Ljava/lang/String;)V");
env->CallVoidMethod(g_loActivityObj, callPostMobileMessage, jstr);
env->DeleteLocalRef(jstr);
if (env->ExceptionCheck())
env->ExceptionDescribe();
}
/// Close the document.
void closeDocument()
{
// Close one end of the socket pair, that will wake up the forwarding thread that was constructed in HULLO
fakeSocketClose(closeNotificationPipeForForwardingThread[0]);
LOG_DBG("Waiting for LOOLWSD to finish...");
std::unique_lock<std::mutex> lock(LOOLWSD::lokit_main_mutex);
LOG_DBG("LOOLWSD has finished.");
}
/// Handle a message from JavaScript.
extern "C" JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_postMobileMessageNative(JNIEnv *env, jobject, jstring message)
{
const char *string_value = env->GetStringUTFChars(message, nullptr);
if (string_value)
{
LOG_DBG("From JS: lool: " << string_value);
// we need a copy, because we can get a new one while we are still
// taking down the old one
const int currentFakeClientFd = fakeClientFd;
if (strcmp(string_value, "HULLO") == 0)
{
// Now we know that the JS has started completely
// Contact the permanently (during app lifetime) listening LOOLWSD server
// "public" socket
assert(loolwsd_server_socket_fd != -1);
int rc = fakeSocketConnect(currentFakeClientFd, loolwsd_server_socket_fd);
assert(rc != -1);
// Create a socket pair to notify the below thread when the document has been closed
fakeSocketPipe2(closeNotificationPipeForForwardingThread);
// Start another thread to read responses and forward them to the JavaScript
std::thread([currentFakeClientFd]
{
Util::setThreadName("app2js");
JNIThreadContext ctx;
while (true)
{
struct pollfd pollfd[2];
pollfd[0].fd = currentFakeClientFd;
pollfd[0].events = POLLIN;
pollfd[1].fd = closeNotificationPipeForForwardingThread[1];
pollfd[1].events = POLLIN;
if (fakeSocketPoll(pollfd, 2, -1) > 0)
{
if (pollfd[1].revents == POLLIN)
{
LOG_DBG("app2js: closing the sockets");
// The code below handling the "BYE" fake Websocket
// message has closed the other end of the
// closeNotificationPipeForForwardingThread. Let's close
// the other end too just for cleanliness, even if a
// FakeSocket as such is not a system resource so nothing
// is saved by closing it.
fakeSocketClose(closeNotificationPipeForForwardingThread[1]);
// Close our end of the fake socket connection to the
// ClientSession thread, so that it terminates
fakeSocketClose(currentFakeClientFd);
return;
}
if (pollfd[0].revents == POLLIN)
{
int n = fakeSocketAvailableDataLength(currentFakeClientFd);
if (n == 0)
return;
std::vector<char> buf(n);
n = fakeSocketRead(currentFakeClientFd, buf.data(), n);
send2JS(ctx, buf);
}
}
else
break;
}
assert(false);
}).detach();
// First we simply send it the URL. This corresponds to the GET request with Upgrade to
// WebSocket.
LOG_DBG("Actually sending to Online:" << fileURL);
// Send the document URL to LOOLWSD to setup the docBroker URL
struct pollfd pollfd;
pollfd.fd = currentFakeClientFd;
pollfd.events = POLLOUT;
fakeSocketPoll(&pollfd, 1, -1);
fakeSocketWrite(currentFakeClientFd, fileURL.c_str(), fileURL.size());
}
else if (strcmp(string_value, "BYE") == 0)
{
LOG_DBG("Document window terminating on JavaScript side. Closing our end of the socket.");
closeDocument();
}
else
{
// Send the message to LOOLWSD
char *string_copy = strdup(string_value);
struct pollfd pollfd;
pollfd.fd = currentFakeClientFd;
pollfd.events = POLLOUT;
fakeSocketPoll(&pollfd, 1, -1);
fakeSocketWrite(currentFakeClientFd, string_copy, strlen(string_copy));
free(string_copy);
}
}
else
LOG_DBG("From JS: lool: some object");
}
extern "C" jboolean libreofficekit_initialize(JNIEnv* env, jstring dataDir, jstring cacheDir, jstring apkFile, jobject assetManager);
/// Create the LOOLWSD instance.
extern "C" JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_createLOOLWSD(JNIEnv *env, jobject instance, jstring dataDir, jstring cacheDir, jstring apkFile, jobject assetManager, jstring loadFileURL)
{
fileURL = std::string(env->GetStringUTFChars(loadFileURL, nullptr));
// remember the LOActivity class and object to be able to call back
env->DeleteGlobalRef(g_loActivityClz);
env->DeleteGlobalRef(g_loActivityObj);
jclass clz = env->GetObjectClass(instance);
g_loActivityClz = (jclass) env->NewGlobalRef(clz);
g_loActivityObj = env->NewGlobalRef(instance);
// already initialized?
if (lokInitialized)
{
// close the previous document so that we can wait for the new HULLO
closeDocument();
return;
}
lokInitialized = true;
libreofficekit_initialize(env, dataDir, cacheDir, apkFile, assetManager);
Util::setThreadName("main");
fakeSocketSetLoggingCallback([](const std::string& line)
{
LOG_DBG(line);
});
std::thread([]
{
char *argv[2];
argv[0] = strdup("mobile");
argv[1] = nullptr;
Util::setThreadName("app");
while (true)
{
LOG_DBG("Creating LOOLWSD");
{
fakeClientFd = fakeSocketSocket();
LOG_DBG("createLOOLWSD created fakeClientFd: " << fakeClientFd);
std::unique_ptr<LOOLWSD> loolwsd(new LOOLWSD());
loolwsd->run(1, argv);
}
LOG_DBG("One run of LOOLWSD completed");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}).detach();
}
extern "C"
JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_saveAs(JNIEnv *env, jobject,
jstring fileUri_, jstring format_) {
const char *fileUri = env->GetStringUTFChars(fileUri_, 0);
const char *format = env->GetStringUTFChars(format_, 0);
getLOKDocumentForAndroidOnly()->saveAs(fileUri, format, nullptr);
env->ReleaseStringUTFChars(fileUri_, fileUri);
env->ReleaseStringUTFChars(format_, format);
}
extern "C"
JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_postUnoCommand(JNIEnv* pEnv, jobject,
jstring command, jstring arguments, jboolean bNotifyWhenFinished)
{
const char* pCommand = pEnv->GetStringUTFChars(command, nullptr);
const char* pArguments = nullptr;
if (arguments != nullptr)
pArguments = pEnv->GetStringUTFChars(arguments, nullptr);
getLOKDocumentForAndroidOnly()->postUnoCommand(pCommand, pArguments, bNotifyWhenFinished);
pEnv->ReleaseStringUTFChars(command, pCommand);
if (arguments != nullptr)
pEnv->ReleaseStringUTFChars(arguments, pArguments);
}
static jstring tojstringAndFree(JNIEnv *env, char *str)
{
if (!str)
return env->NewStringUTF("");
jstring ret = env->NewStringUTF(str);
free(str);
return ret;
}
const char* copyJavaString(JNIEnv* pEnv, jstring aJavaString)
{
const char* pTemp = pEnv->GetStringUTFChars(aJavaString, nullptr);
const char* pClone = strdup(pTemp);
pEnv->ReleaseStringUTFChars(aJavaString, pTemp);
return pClone;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_org_libreoffice_androidlib_LOActivity_getClipboardContent(JNIEnv *env, jobject, jobject lokClipboardData)
{
const char** mimeTypes = nullptr;
size_t outCount = 0;
char **outMimeTypes = nullptr;
size_t *outSizes = nullptr;
char **outStreams = nullptr;
bool bResult = false;
jclass jclazz = env->FindClass("java/util/ArrayList");
jmethodID methodId_ArrayList_Add = env->GetMethodID(jclazz, "add", "(Ljava/lang/Object;)Z");
jclass class_LokClipboardEntry = env->FindClass("org/libreoffice/androidlib/lok/LokClipboardEntry");
jmethodID methodId_LokClipboardEntry_Constructor = env->GetMethodID(class_LokClipboardEntry, "<init>", "()V");
jfieldID fieldId_LokClipboardEntry_Mime = env->GetFieldID(class_LokClipboardEntry , "mime", "Ljava/lang/String;");
jfieldID fieldId_LokClipboardEntry_Data = env->GetFieldID(class_LokClipboardEntry, "data", "[B");
jclass class_LokClipboardData = env->GetObjectClass(lokClipboardData);
jfieldID fieldId_LokClipboardData_clipboardEntries = env->GetFieldID(class_LokClipboardData , "clipboardEntries", "Ljava/util/ArrayList;");
if (getLOKDocumentForAndroidOnly()->getClipboard(mimeTypes,
&outCount, &outMimeTypes,
&outSizes, &outStreams))
{
// return early
if (outCount == 0)
return bResult;
for (size_t i = 0; i < outCount; ++i)
{
// Create new LokClipboardEntry instance
jobject clipboardEntry = env->NewObject(class_LokClipboardEntry, methodId_LokClipboardEntry_Constructor);
jstring mimeType = tojstringAndFree(env, outMimeTypes[i]);
// clipboardEntry.mime= mimeType
env->SetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Mime, mimeType);
env->DeleteLocalRef(mimeType);
size_t aByteArraySize = outSizes[i];
jbyteArray aByteArray = env->NewByteArray(aByteArraySize);
// Copy char* to bytearray
env->SetByteArrayRegion(aByteArray, 0, aByteArraySize, (jbyte*) outStreams[i]);
// clipboardEntry.data = aByteArray
env->SetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Data, aByteArray);
// clipboardData.clipboardEntries
jobject lokClipboardData_clipboardEntries = env->GetObjectField(lokClipboardData, fieldId_LokClipboardData_clipboardEntries);
// clipboardEntries.add(clipboardEntry)
env->CallBooleanMethod(lokClipboardData_clipboardEntries, methodId_ArrayList_Add, clipboardEntry);
}
bResult = true;
}
else
LOG_DBG("failed to fetch mime-types");
const char* mimeTypesHTML[] = { "text/plain;charset=utf-8", "text/html", nullptr };
if (getLOKDocumentForAndroidOnly()->getClipboard(mimeTypesHTML,
&outCount, &outMimeTypes,
&outSizes, &outStreams))
{
// return early
if (outCount == 0)
return bResult;
for (size_t i = 0; i < outCount; ++i)
{
// Create new LokClipboardEntry instance
jobject clipboardEntry = env->NewObject(class_LokClipboardEntry, methodId_LokClipboardEntry_Constructor);
jstring mimeType = tojstringAndFree(env, outMimeTypes[i]);
// clipboardEntry.mime= mimeType
env->SetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Mime, mimeType);
env->DeleteLocalRef(mimeType);
size_t aByteArraySize = outSizes[i];
jbyteArray aByteArray = env->NewByteArray(aByteArraySize);
// Copy char* to bytearray
env->SetByteArrayRegion(aByteArray, 0, aByteArraySize, (jbyte*) outStreams[i]);
// clipboardEntry.data = aByteArray
env->SetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Data, aByteArray);
// clipboardData.clipboardEntries
jobject lokClipboardData_clipboardEntries = env->GetObjectField(lokClipboardData, fieldId_LokClipboardData_clipboardEntries);
// clipboardEntries.add(clipboardEntry)
env->CallBooleanMethod(lokClipboardData_clipboardEntries, methodId_ArrayList_Add, clipboardEntry);
}
bResult = true;
}
else
LOG_DBG("failed to fetch mime-types");
return bResult;
}
extern "C"
JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_setClipboardContent(JNIEnv *env, jobject, jobject lokClipboardData) {
jclass class_ArrayList= env->FindClass("java/util/ArrayList");
jmethodID methodId_ArrayList_ToArray = env->GetMethodID(class_ArrayList, "toArray", "()[Ljava/lang/Object;");
jclass class_LokClipboardEntry = env->FindClass("org/libreoffice/androidlib/lok/LokClipboardEntry");
jfieldID fieldId_LokClipboardEntry_Mime = env->GetFieldID(class_LokClipboardEntry , "mime", "Ljava/lang/String;");
jfieldID fieldId_LokClipboardEntry_Data = env->GetFieldID(class_LokClipboardEntry, "data", "[B");
jclass class_LokClipboardData = env->GetObjectClass(lokClipboardData);
jfieldID fieldId_LokClipboardData_clipboardEntries = env->GetFieldID(class_LokClipboardData , "clipboardEntries", "Ljava/util/ArrayList;");
jobject lokClipboardData_clipboardEntries = env->GetObjectField(lokClipboardData, fieldId_LokClipboardData_clipboardEntries);
jobjectArray clipboardEntryArray = (jobjectArray) env->CallObjectMethod(lokClipboardData_clipboardEntries, methodId_ArrayList_ToArray);
size_t nEntrySize= env->GetArrayLength(clipboardEntryArray);
if (nEntrySize == 0)
return;
size_t pSizes[nEntrySize];
const char* pMimeTypes[nEntrySize];
const char* pStreams[nEntrySize];
for (size_t nEntryIndex = 0; nEntryIndex < nEntrySize; ++nEntryIndex)
{
jobject clipboardEntry = env->GetObjectArrayElement(clipboardEntryArray, nEntryIndex);
jstring mimetype = (jstring) env->GetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Mime);
jbyteArray data = (jbyteArray) env->GetObjectField(clipboardEntry, fieldId_LokClipboardEntry_Data);
pMimeTypes[nEntryIndex] = copyJavaString(env, mimetype);
size_t dataArrayLength = env->GetArrayLength(data);
char* dataArray = new char[dataArrayLength];
env->GetByteArrayRegion(data, 0, dataArrayLength, reinterpret_cast<jbyte*>(dataArray));
pSizes[nEntryIndex] = dataArrayLength;
pStreams[nEntryIndex] = dataArray;
}
getLOKDocumentForAndroidOnly()->setClipboard(nEntrySize, pMimeTypes, pSizes, pStreams);
}
extern "C"
JNIEXPORT void JNICALL
Java_org_libreoffice_androidlib_LOActivity_paste(JNIEnv *env, jobject, jstring inMimeType, jbyteArray inData) {
const char* mimeType = env->GetStringUTFChars(inMimeType, nullptr);
size_t dataArrayLength = env->GetArrayLength(inData);
char* dataArray = new char[dataArrayLength];
env->GetByteArrayRegion(inData, 0, dataArrayLength, reinterpret_cast<jbyte*>(dataArray));
getLOKDocumentForAndroidOnly()->paste(mimeType, dataArray, dataArrayLength);
env->ReleaseStringUTFChars(inMimeType, mimeType);
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */