diff --git a/app/src/androidTest/assets/spreadsheet-test.ods b/app/src/androidTest/assets/spreadsheet-test.ods new file mode 100644 index 000000000000..993fe107568f Binary files /dev/null and b/app/src/androidTest/assets/spreadsheet-test.ods differ diff --git a/app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java b/app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java index 7ed1d192b4e7..88ff51c4435b 100644 --- a/app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java +++ b/app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java @@ -8,8 +8,10 @@ import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -24,13 +26,49 @@ @LargeTest @RunWith(AndroidJUnit4.class) public class CoreTest { + private static Thread serverThread; private File m_testFile; private File m_passwordTestFile; + private File m_spreadsheetTestFile; - @Before - public void initializeCore() { + @BeforeClass + public static void startServer() throws InterruptedException { Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); CoreWrapper.initialize(appCtx); + + // Create server cache directory + File serverCacheDir = new File(appCtx.getCacheDir(), "core/server"); + if (!serverCacheDir.isDirectory()) { + serverCacheDir.mkdirs(); + } + CoreWrapper.createServer(serverCacheDir.getAbsolutePath()); + + // Start server in background thread + serverThread = new Thread(() -> { + try { + CoreWrapper.listenServer(29665); + } catch (Exception e) { + e.printStackTrace(); + } + }); + serverThread.setDaemon(true); + serverThread.start(); + + // Give server time to start + Thread.sleep(1000); + } + + @AfterClass + public static void stopServer() { + CoreWrapper.stopServer(); + if (serverThread != null) { + serverThread.interrupt(); + } + } + + @Before + public void initializeCore() { + // Server is already initialized in @BeforeClass } @Before @@ -47,6 +85,10 @@ public void extractTestFile() throws IOException { try (InputStream inputStream = assetManager.open("password-test.odt")) { copy(inputStream, m_passwordTestFile); } + m_spreadsheetTestFile = new File(appCtx.getCacheDir(), "spreadsheet-test.ods"); + try (InputStream inputStream = assetManager.open("spreadsheet-test.ods")) { + copy(inputStream, m_spreadsheetTestFile); + } } @After @@ -57,6 +99,9 @@ public void cleanupTestFile() { if (null != m_passwordTestFile) { m_passwordTestFile.delete(); } + if (null != m_spreadsheetTestFile) { + m_spreadsheetTestFile.delete(); + } } private static void copy(InputStream src, File dst) throws IOException { @@ -81,7 +126,7 @@ public void test() { coreOptions.editable = true; coreOptions.cachePath = cachePath.getPath(); - CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions); + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("test", coreOptions); Assert.assertEquals(0, coreResult.errorCode); File resultFile = new File(cacheDir, "result"); @@ -105,7 +150,7 @@ public void testPasswordProtectedDocumentWithoutPassword() { coreOptions.editable = false; coreOptions.cachePath = cachePath.getPath(); - CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions); + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("password-test-no-pw", coreOptions); Assert.assertEquals(-2, coreResult.errorCode); } @@ -122,7 +167,7 @@ public void testPasswordProtectedDocumentWithWrongPassword() { coreOptions.editable = false; coreOptions.cachePath = cachePath.getPath(); - CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions); + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("password-test-wrong-pw", coreOptions); Assert.assertEquals(-2, coreResult.errorCode); } @@ -139,7 +184,31 @@ public void testPasswordProtectedDocumentWithCorrectPassword() { coreOptions.editable = false; coreOptions.cachePath = cachePath.getPath(); - CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions); + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("password-test-correct-pw", coreOptions); Assert.assertEquals(0, coreResult.errorCode); } + + @Test + public void testSpreadsheetSheetNames() { + File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir(); + File outputPath = new File(cacheDir, "spreadsheet_output"); + File cachePath = new File(cacheDir, "spreadsheet_cache"); + + CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions(); + coreOptions.inputPath = m_spreadsheetTestFile.getAbsolutePath(); + coreOptions.outputPath = outputPath.getPath(); + coreOptions.editable = false; + coreOptions.cachePath = cachePath.getPath(); + + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("spreadsheet-test", coreOptions); + Assert.assertEquals("CoreWrapper should successfully parse the ODS file", 0, coreResult.errorCode); + + // Verify we have exactly 3 sheets + Assert.assertEquals("ODS file should contain 3 sheets", 3, coreResult.pageNames.size()); + + // Verify sheet names match the actual sheet names from the ODS file + Assert.assertEquals("First sheet should be named 'hey'", "hey", coreResult.pageNames.get(0)); + Assert.assertEquals("Second sheet should be named 'ho'", "ho", coreResult.pageNames.get(1)); + Assert.assertEquals("Third sheet should be named 'Sheet3'", "Sheet3", coreResult.pageNames.get(2)); + } } diff --git a/app/src/main/cpp/core_wrapper.cpp b/app/src/main/cpp/core_wrapper.cpp index 372b19ebbfdf..f0aafa601c4b 100644 --- a/app/src/main/cpp/core_wrapper.cpp +++ b/app/src/main/cpp/core_wrapper.cpp @@ -111,149 +111,6 @@ Java_at_tomtasche_reader_background_CoreWrapper_mimetypeNative(JNIEnv *env, jcla return mimetype; } -JNIEXPORT jobject JNICALL -Java_at_tomtasche_reader_background_CoreWrapper_parseNative(JNIEnv *env, jclass clazz, - jobject options) { - std::error_code ec; - auto logger = std::make_shared(); - - jclass resultClass = env->FindClass("at/tomtasche/reader/background/CoreWrapper$CoreResult"); - jmethodID resultConstructor = env->GetMethodID(resultClass, "", "()V"); - jobject result = env->NewObject(resultClass, resultConstructor); - - jfieldID errorField = env->GetFieldID(resultClass, "errorCode", "I"); - - jclass optionsClass = env->GetObjectClass(options); - std::string inputPathCpp = getStringField(env, optionsClass, options, "inputPath"); - - try { - std::optional passwordCpp; - jfieldID passwordField = env->GetFieldID(optionsClass, "password", "Ljava/lang/String;"); - auto password = (jstring) env->GetObjectField(options, passwordField); - if (password != nullptr) { - passwordCpp = convertString(env, password); - } - - jfieldID editableField = env->GetFieldID(optionsClass, "editable", "Z"); - jboolean editable = env->GetBooleanField(options, editableField); - - std::string outputPathCpp = getStringField(env, optionsClass, options, "outputPath"); - std::string cachePathCpp = getStringField(env, optionsClass, options, "cachePath"); - - jclass listClass = env->FindClass("java/util/List"); - jmethodID addMethod = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z"); - - jfieldID pageNamesField = env->GetFieldID(resultClass, "pageNames", "Ljava/util/List;"); - auto pageNames = (jobject) env->GetObjectField(result, pageNamesField); - - jfieldID pagePathsField = env->GetFieldID(resultClass, "pagePaths", "Ljava/util/List;"); - auto pagePaths = (jobject) env->GetObjectField(result, pagePathsField); - - jfieldID ooxmlField = env->GetFieldID(optionsClass, "ooxml", "Z"); - jboolean ooxml = env->GetBooleanField(options, ooxmlField); - - jfieldID txtField = env->GetFieldID(optionsClass, "txt", "Z"); - jboolean txt = env->GetBooleanField(options, txtField); - - jfieldID pdfField = env->GetFieldID(optionsClass, "pdf", "Z"); - jboolean pdf = env->GetBooleanField(options, pdfField); - - jfieldID pagingField = env->GetFieldID(optionsClass, "paging", "Z"); - jboolean paging = env->GetBooleanField(options, pagingField); - - try { - odr::FileType fileType; - try { - const auto types = odr::list_file_types(inputPathCpp, *logger); - if (types.empty()) { - env->SetIntField(result, errorField, -5); - return result; - } - - fileType = types.back(); - } catch (odr::UnsupportedFileType &e) { - fileType = e.file_type; - } - - std::string extensionCpp = odr::file_type_to_string(fileType); - jstring extension = env->NewStringUTF(extensionCpp.c_str()); - jfieldID extensionField = env->GetFieldID(resultClass, "extension", - "Ljava/lang/String;"); - env->SetObjectField(result, extensionField, extension); - - __android_log_print(ANDROID_LOG_VERBOSE, "smn", "Open %s", inputPathCpp.c_str()); - - auto file = odr::open(inputPathCpp, *logger); - - if (file.password_encrypted()) { - if (!passwordCpp.has_value()) { - env->SetIntField(result, errorField, -2); - return result; - } - try { - file = file.decrypt(passwordCpp.value()); - } catch (...) { - env->SetIntField(result, errorField, -2); - return result; - } - } - - // .doc-files are not real documents in core - if (file.is_document_file() && fileType != odr::FileType::legacy_word_document) { - // TODO this will cause a second load - s_document = file.as_document_file().document(); - } - - extensionCpp = odr::file_type_to_string(file.file_type()); - extension = env->NewStringUTF(extensionCpp.c_str()); - env->SetObjectField(result, extensionField, extension); - - odr::HtmlConfig htmlConfig; - htmlConfig.editable = editable; - htmlConfig.text_document_margin = paging; - - __android_log_print(ANDROID_LOG_VERBOSE, "smn", "Translate to HTML"); - - std::filesystem::remove_all(cachePathCpp, ec); - std::filesystem::create_directories(cachePathCpp); - odr::HtmlService service = odr::html::translate(file, cachePathCpp, htmlConfig, logger); - odr::Html html = service.bring_offline(outputPathCpp); - std::filesystem::remove_all(cachePathCpp); - - for (const odr::HtmlPage &page: html.pages()) { - jstring pageName = env->NewStringUTF(page.name.c_str()); - env->CallBooleanMethod(pageNames, addMethod, pageName); - - jstring pagePath = env->NewStringUTF(page.path.c_str()); - env->CallBooleanMethod(pagePaths, addMethod, pagePath); - } - } catch (const odr::UnknownFileType &e) { - __android_log_print(ANDROID_LOG_ERROR, "smn", "Unknown file type: %s", e.what()); - env->SetIntField(result, errorField, -5); - return result; - } catch (const odr::UnsupportedFileType &e) { - __android_log_print(ANDROID_LOG_ERROR, "smn", "Unsupported file type: %s", e.what()); - env->SetIntField(result, errorField, -5); - return result; - } catch (const std::exception &e) { - __android_log_print(ANDROID_LOG_ERROR, "smn", "Unhandled C++ exception: %s", e.what()); - env->SetIntField(result, errorField, -4); - return result; - } catch (...) { - __android_log_print(ANDROID_LOG_ERROR, "smn", - "Unhandled C++ exception without further information"); - env->SetIntField(result, errorField, -4); - return result; - } - } catch (...) { - env->SetIntField(result, errorField, -3); - return result; - } - - env->SetIntField(result, errorField, 0); - return result; -} - JNIEXPORT jobject JNICALL Java_at_tomtasche_reader_background_CoreWrapper_backtranslateNative(JNIEnv *env, jclass clazz, jobject options, diff --git a/app/src/main/cpp/core_wrapper.hpp b/app/src/main/cpp/core_wrapper.hpp index 8804ba1293a1..e79b7478dc5b 100644 --- a/app/src/main/cpp/core_wrapper.hpp +++ b/app/src/main/cpp/core_wrapper.hpp @@ -12,10 +12,6 @@ JNIEXPORT jstring JNICALL Java_at_tomtasche_reader_background_CoreWrapper_mimetypeNative(JNIEnv *env, jclass clazz, jstring path); -JNIEXPORT jobject JNICALL -Java_at_tomtasche_reader_background_CoreWrapper_parseNative(JNIEnv *env, jclass clazz, - jobject options); - JNIEXPORT jobject JNICALL Java_at_tomtasche_reader_background_CoreWrapper_backtranslateNative(JNIEnv *env, jclass clazz, jobject options, diff --git a/app/src/main/java/at/tomtasche/reader/background/CoreLoader.java b/app/src/main/java/at/tomtasche/reader/background/CoreLoader.java index 93701f9f124b..b753173c7560 100644 --- a/app/src/main/java/at/tomtasche/reader/background/CoreLoader.java +++ b/app/src/main/java/at/tomtasche/reader/background/CoreLoader.java @@ -19,38 +19,34 @@ public class CoreLoader extends FileLoader { private CoreWrapper.CoreOptions lastCoreOptions; private final boolean doOoxml; - private final boolean doHttp; private Thread httpThread; - public CoreLoader(Context context, ConfigManager configManager, boolean doOoxml, boolean doHttp) { + public CoreLoader(Context context, ConfigManager configManager, boolean doOoxml) { super(context, LoaderType.CORE); this.configManager = configManager; this.doOoxml = doOoxml; - this.doHttp = doHttp; CoreWrapper.initialize(context); } @Override public void initialize(FileLoaderListener listener, Handler mainHandler, Handler backgroundHandler, AnalyticsManager analyticsManager, CrashManager crashManager) { - if (doHttp) { - File serverCacheDir = new File(context.getCacheDir(), "core/server"); - if (!serverCacheDir.isDirectory() && !serverCacheDir.mkdirs()) { - Log.e("CoreLoader", "Failed to create cache directory for CoreWrapper server: " + serverCacheDir.getAbsolutePath()); - } - CoreWrapper.createServer(serverCacheDir.getAbsolutePath()); - - httpThread = new Thread(() -> { - try { - CoreWrapper.listenServer(29665); - } catch (Throwable e) { - crashManager.log(e); - } - }); - httpThread.start(); + File serverCacheDir = new File(context.getCacheDir(), "core/server"); + if (!serverCacheDir.isDirectory() && !serverCacheDir.mkdirs()) { + Log.e("CoreLoader", "Failed to create cache directory for CoreWrapper server: " + serverCacheDir.getAbsolutePath()); } + CoreWrapper.createServer(serverCacheDir.getAbsolutePath()); + + httpThread = new Thread(() -> { + try { + CoreWrapper.listenServer(29665); + } catch (Throwable e) { + crashManager.log(e); + } + }); + httpThread.start(); super.initialize(listener, mainHandler, backgroundHandler, analyticsManager, crashManager); } @@ -113,45 +109,15 @@ private void translate(Options options, Result result) throws Exception { lastCoreOptions = coreOptions; - if (doHttp) { - CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("odr", coreOptions); - - if (coreResult.exception != null) { - throw coreResult.exception; - } - - for (int i = 0; i < coreResult.pagePaths.size(); i++) { - result.partTitles.add(coreResult.pageNames.get(i)); - result.partUris.add(Uri.parse(coreResult.pagePaths.get(i))); - } - } else { - CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions); - - String coreExtension = coreResult.extension; - if (coreResult.exception == null && "pdf".equals(coreExtension)) { - // some PDFs do not cause an error in the core - // https://github.com/opendocument-app/OpenDocument.droid/issues/348#issuecomment-2446888981 - throw new CoreWrapper.CoreCouldNotTranslateException(); - } else if (!"unnamed".equals(coreExtension)) { - // "unnamed" refers to default of Meta::typeToString - options.fileExtension = coreExtension; - - String fileType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(coreExtension); - if (fileType != null) { - options.fileType = fileType; - } - } + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("odr", coreOptions); - if (coreResult.exception != null) { - throw coreResult.exception; - } - - for (int i = 0; i < coreResult.pagePaths.size(); i++) { - File entryFile = new File(coreResult.pagePaths.get(i)); + if (coreResult.exception != null) { + throw coreResult.exception; + } - result.partTitles.add(coreResult.pageNames.get(i)); - result.partUris.add(Uri.fromFile(entryFile)); - } + for (int i = 0; i < coreResult.pagePaths.size(); i++) { + result.partTitles.add(coreResult.pageNames.get(i)); + result.partUris.add(Uri.parse(coreResult.pagePaths.get(i))); } } diff --git a/app/src/main/java/at/tomtasche/reader/background/CoreWrapper.java b/app/src/main/java/at/tomtasche/reader/background/CoreWrapper.java index 37cbe6085a91..cfe2a2cbe068 100644 --- a/app/src/main/java/at/tomtasche/reader/background/CoreWrapper.java +++ b/app/src/main/java/at/tomtasche/reader/background/CoreWrapper.java @@ -74,37 +74,6 @@ public static class CoreOptions { public String cachePath; } - public static CoreResult parse(CoreOptions options) { - CoreResult result = parseNative(options); - - switch (result.errorCode) { - case 0: - break; - case -1: - result.exception = new CoreCouldNotOpenException(); - break; - case -2: - result.exception = new CoreEncryptedException(); - break; - case -3: - result.exception = new CoreUnknownErrorException(); - break; - case -4: - result.exception = new CoreCouldNotTranslateException(); - break; - case -5: - result.exception = new CoreUnexpectedFormatException(); - break; - default: - result.exception = new CoreUnexpectedErrorCodeException(); - break; - } - - return result; - } - - private static native CoreResult parseNative(CoreOptions options); - public static CoreResult backtranslate(CoreOptions options, String htmlDiff) { CoreResult result = backtranslateNative(options, htmlDiff); diff --git a/app/src/main/java/at/tomtasche/reader/background/LoaderService.java b/app/src/main/java/at/tomtasche/reader/background/LoaderService.java index cc9a574ac851..de8df8a428f1 100644 --- a/app/src/main/java/at/tomtasche/reader/background/LoaderService.java +++ b/app/src/main/java/at/tomtasche/reader/background/LoaderService.java @@ -57,7 +57,7 @@ public synchronized void onCreate() { metadataLoader = new MetadataLoader(context); metadataLoader.initialize(this, mainHandler, backgroundHandler, analyticsManager, crashManager); - coreLoader = new CoreLoader(context, configManager, true, true); + coreLoader = new CoreLoader(context, configManager, true); coreLoader.initialize(this, mainHandler, backgroundHandler, analyticsManager, crashManager); rawLoader = new RawLoader(context); diff --git a/app/src/main/java/at/tomtasche/reader/background/RawLoader.java b/app/src/main/java/at/tomtasche/reader/background/RawLoader.java index d274d5b53cdf..44584930fecf 100644 --- a/app/src/main/java/at/tomtasche/reader/background/RawLoader.java +++ b/app/src/main/java/at/tomtasche/reader/background/RawLoader.java @@ -141,13 +141,12 @@ public void loadSync(Options options) { coreOptions.txt = true; coreOptions.pdf = false; - CoreWrapper.CoreResult coreResult = lastCore.parse(coreOptions); + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("raw-text", coreOptions); if (coreResult.exception != null) { throw coreResult.exception; } - File entryFile = new File(coreResult.pagePaths.get(0)); - finalUri = Uri.fromFile(entryFile); + finalUri = Uri.parse(coreResult.pagePaths.get(0)); } else if (fileType.startsWith("application/zip")) { File htmlFile = new File(cacheDirectory, "zip.html"); InputStream htmlPrefixStream = context.getAssets().open("zip-prefix.html");