diff --git a/app/build.gradle b/app/build.gradle index b8c466533b5c..bb0a0a2f8a10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -147,6 +147,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' // espresso-idling-resource is used in main sourceSet as well. cannot be just androidTestImplementation implementation 'androidx.test.espresso:espresso-idling-resource:3.6.1' implementation 'androidx.annotation:annotation:1.9.1' 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/assets/style-various-1.docx b/app/src/androidTest/assets/style-various-1.docx new file mode 100644 index 000000000000..6845510f63d4 Binary files /dev/null and b/app/src/androidTest/assets/style-various-1.docx 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..1a6943c44eee 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,50 @@ @LargeTest @RunWith(AndroidJUnit4.class) public class CoreTest { + private static Thread serverThread; private File m_testFile; private File m_passwordTestFile; + private File m_spreadsheetTestFile; + private File m_docxTestFile; - @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 +86,14 @@ 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); + } + m_docxTestFile = new File(appCtx.getCacheDir(), "style-various-1.docx"); + try (InputStream inputStream = assetManager.open("style-various-1.docx")) { + copy(inputStream, m_docxTestFile); + } } @After @@ -57,6 +104,12 @@ public void cleanupTestFile() { if (null != m_passwordTestFile) { m_passwordTestFile.delete(); } + if (null != m_spreadsheetTestFile) { + m_spreadsheetTestFile.delete(); + } + if (null != m_docxTestFile) { + m_docxTestFile.delete(); + } } private static void copy(InputStream src, File dst) throws IOException { @@ -81,7 +134,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"); @@ -93,6 +146,30 @@ public void test() { Assert.assertEquals(0, result.errorCode); } + @Test + public void testDocxEdit() { + File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir(); + File outputPath = new File(cacheDir, "core_output_docx"); + File cachePath = new File(cacheDir, "core_cache"); + + CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions(); + coreOptions.inputPath = m_docxTestFile.getAbsolutePath(); + coreOptions.outputPath = outputPath.getPath(); + coreOptions.editable = true; + coreOptions.cachePath = cachePath.getPath(); + + CoreWrapper.CoreResult coreResult = CoreWrapper.hostFile("docx-edit", coreOptions); + Assert.assertEquals(0, coreResult.errorCode); + + File resultFile = new File(cacheDir, "result_docx"); + coreOptions.outputPath = resultFile.getPath(); + + String htmlDiff = "{\"modifiedText\":{\"/child:16/child:0/child:0\":\"Outasdfsdafdline\",\"/child:24/child:0/child:0\":\"Colorasdfasdfasdfed Line\",\"/child:6/child:0/child:0\":\"Text hello world!\"}}"; + + CoreWrapper.CoreResult result = CoreWrapper.backtranslate(coreOptions, htmlDiff); + Assert.assertEquals(0, result.errorCode); + } + @Test public void testPasswordProtectedDocumentWithoutPassword() { File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir(); @@ -105,7 +182,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 +199,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 +216,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/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java b/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java index 8d388ca80423..c5f1e2224f49 100644 --- a/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java +++ b/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java @@ -1,34 +1,17 @@ package at.tomtasche.reader.test; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.clearText; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.typeText; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; -import static androidx.test.espresso.matcher.ViewMatchers.withClassName; -import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.equalTo; - -import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.net.Uri; +import android.os.SystemClock; import android.util.ArrayMap; import android.util.Log; import androidx.core.content.FileProvider; import androidx.test.espresso.IdlingRegistry; import androidx.test.espresso.IdlingResource; -import androidx.test.espresso.intent.Intents; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -48,16 +31,27 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Field; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; -import at.tomtasche.reader.R; +import at.tomtasche.reader.background.FileLoader; +import at.tomtasche.reader.ui.EditActionModeCallback; import at.tomtasche.reader.ui.activity.MainActivity; +import at.tomtasche.reader.ui.activity.DocumentFragment; +import at.tomtasche.reader.ui.widget.PageView; @LargeTest @RunWith(AndroidJUnit4.class) public class MainActivityTests { private IdlingResource m_idlingResource; private static final Map s_testFiles = new ArrayMap<>(); + private static final String EXPECTED_FIRST_WORD_ODT = "This"; + private static final String EXPECTED_FIRST_WORD_PDF = "Dummy"; + private static final String EXPECTED_FIRST_WORD_DOCX = "Table"; + private static final String EXPECTED_FIRST_WORD_PASSWORD_ODT = "Hallo"; // Yes, this is ActivityTestRule instead of ActivityScenario, because ActivityScenario does not actually work. // Issue ID may or may not be added later. @@ -76,8 +70,6 @@ public void setUp() { // Close system dialogs which may cover our Activity. // Happens frequently on slow emulators. mainActivity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - - Intents.init(); // Log test setup for debugging Log.d("MainActivityTests", "setUp() called for test: " + getClass().getName()); @@ -86,8 +78,6 @@ public void setUp() { @After public void tearDown() { Log.d("MainActivityTests", "tearDown() called"); - - Intents.release(); if (null != m_idlingResource) { IdlingRegistry.getInstance().unregister(m_idlingResource); @@ -126,7 +116,7 @@ public static void extractTestFiles() throws IOException { AssetManager testAssetManager = instrumentation.getContext().getAssets(); - for (String filename: new String[] {"test.odt", "dummy.pdf", "password-test.odt"}) { + for (String filename: new String[] {"test.odt", "dummy.pdf", "password-test.odt", "style-various-1.docx"}) { File targetFile = new File(testDocumentsDir, filename); try (InputStream inputStream = testAssetManager.open(filename)) { copy(inputStream, targetFile); @@ -143,78 +133,41 @@ public static void cleanupTestFiles() { } @Test - public void testODT() { + public void testODT() throws InterruptedException { File testFile = s_testFiles.get("test.odt"); Assert.assertNotNull(testFile); - Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile); - Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith( - new Instrumentation.ActivityResult(Activity.RESULT_OK, - new Intent() - .setData(testFileUri) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - ) - ); - - onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed())) - .perform(click()); - - // The menu item could be either Documents or Files. - onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed())) - .perform(click()); + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment documentFragment = loadDocument(activity, testFile); - // next onView will be blocked until m_idlingResource is idle. - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled())) - .withFailureHandler((error, viewMatcher) -> { - // fails on small screens, try again with overflow menu - onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click()); + PageView pageView = documentFragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("ODT should load", waitForPageLoaded(pageView, 10000)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_ODT, "ODT"); - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed())) - .perform(click()); - }); + String fileType = documentFragment.getLastFileType(); + Assert.assertNotNull(fileType); + Assert.assertTrue("Expected ODT file type", fileType.startsWith("application/vnd.oasis.opendocument")); } @Test - public void testPDF() { + public void testPDF() throws InterruptedException { File testFile = s_testFiles.get("dummy.pdf"); Assert.assertNotNull(testFile); - Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile); - Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith( - new Instrumentation.ActivityResult(Activity.RESULT_OK, - new Intent() - .setData(testFileUri) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - ) - ); - - onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed())) - .perform(click()); - - // The menu item could be either Documents or Files. - onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed())) - .perform(click()); - - // next onView will be blocked until m_idlingResource is idle. - - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled())) - .withFailureHandler((error, viewMatcher) -> { - // fails on small screens, try again with overflow menu - onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click()); + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment documentFragment = loadDocument(activity, testFile); - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed())) - .perform(click()); - }); + PageView pageView = documentFragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("PDF should load", waitForPageLoaded(pageView, 10000)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_PDF, "PDF"); - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + String fileType = documentFragment.getLastFileType(); + Assert.assertNotNull(fileType); + Assert.assertTrue("Expected PDF file type", fileType.startsWith("application/pdf")); } @Test - public void testPasswordProtectedODT() { + public void testPasswordProtectedODT() throws InterruptedException { File testFile = s_testFiles.get("password-test.odt"); Assert.assertNotNull(testFile); @@ -230,51 +183,213 @@ public void testPasswordProtectedODT() { // Double-check we're using the right file Assert.assertEquals("password-test.odt file size mismatch", 12671L, testFile.length()); + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment documentFragment = loadDocument(activity, testFile); + + setPasswordAndReload(documentFragment, "passwort"); + + PageView pageView = documentFragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("Password-protected ODT should load with correct password", + waitForPageLoaded(pageView, 10000)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_PASSWORD_ODT, "Password-protected ODT"); + } + + @Test + public void testODTEditMode() throws InterruptedException { + File testFile = s_testFiles.get("test.odt"); + Assert.assertNotNull(testFile); + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment documentFragment = loadDocument(activity, testFile); + + PageView pageView = documentFragment.getPageView(); + Assert.assertNotNull(pageView); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_ODT, "ODT"); + + enterEditMode(activity, documentFragment); + Assert.assertTrue( + "ODT should become editable after entering edit mode", + waitForEditableState(pageView, true, 10000) + ); + } + + @Test + public void testDOCXEditMode() throws InterruptedException { + File testFile = s_testFiles.get("style-various-1.docx"); + Assert.assertNotNull(testFile); + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment documentFragment = loadDocument(activity, testFile); + + PageView pageView = documentFragment.getPageView(); + Assert.assertNotNull(pageView); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_DOCX, "DOCX"); + + enterEditMode(activity, documentFragment); + Assert.assertTrue( + "DOCX should become editable after entering edit mode", + waitForEditableState(pageView, true, 10000) + ); + } + + private DocumentFragment loadDocument(MainActivity activity, File testFile) throws InterruptedException { Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile); - Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith( - new Instrumentation.ActivityResult(Activity.RESULT_OK, - new Intent() - .setData(testFileUri) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - ) - ); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> activity.loadUri(testFileUri)); - onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed())) - .perform(click()); + DocumentFragment fragment = waitForDocumentFragment(activity, 10000); + Assert.assertNotNull(fragment); + Assert.assertTrue("Timed out waiting for document to load", waitForLastResult(fragment, 10000)); + return fragment; + } - onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed())) - .perform(click()); + private DocumentFragment waitForDocumentFragment(MainActivity activity, long timeoutMs) + throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + DocumentFragment fragment; + do { + fragment = (DocumentFragment) activity.getSupportFragmentManager() + .findFragmentByTag("document_fragment"); + if (fragment != null) { + return fragment; + } + SystemClock.sleep(100); + } while (SystemClock.elapsedRealtime() - startMs < timeoutMs); + return null; + } - // Wait for the password dialog to appear - onView(withText("This document is password-protected")) - .check(matches(isDisplayed())); + private boolean waitForLastResult(DocumentFragment fragment, long timeoutMs) throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + if (fragment.hasLastResult()) { + return true; + } + SystemClock.sleep(100); + } + return false; + } - // Enter wrong password first - onView(withClassName(equalTo("android.widget.EditText"))) - .perform(typeText("wrongpassword")); + private void setPasswordAndReload(DocumentFragment documentFragment, String password) { + FileLoader.Result result = getLastResult(documentFragment); + Assert.assertNotNull(result); + Assert.assertNotNull(result.options); + result.options.password = password; + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> documentFragment.reloadUri(false)); + } - onView(withId(android.R.id.button1)) - .perform(click()); + private FileLoader.Result getLastResult(DocumentFragment documentFragment) { + try { + Field field = DocumentFragment.class.getDeclaredField("lastResult"); + field.setAccessible(true); + return (FileLoader.Result) field.get(documentFragment); + } catch (NoSuchFieldException | IllegalAccessException e) { + Assert.fail("Failed to access lastResult: " + e.getMessage()); + return null; + } + } - // Should show password dialog again for wrong password - onView(withText("This document is password-protected")) - .check(matches(isDisplayed())); + private void enterEditMode(MainActivity activity, DocumentFragment documentFragment) { + AtomicReference started = new AtomicReference<>(false); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + started.set(activity.startSupportActionMode( + new EditActionModeCallback(activity, documentFragment)) != null); + }); + Assert.assertTrue("Failed to enter edit mode", started.get()); + } - // Clear the text field and enter correct password - onView(withClassName(equalTo("android.widget.EditText"))) - .perform(clearText(), typeText("passwort")); + private boolean waitForPageLoaded(PageView pageView, long timeoutMs) throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + String url = getPageViewUrl(pageView); + if (url != null && !url.isEmpty() && !"about:blank".equals(url)) { + return true; + } + SystemClock.sleep(250); + } + return false; + } - onView(withId(android.R.id.button1)) - .perform(click()); + private void assertFirstWord(PageView pageView, String expected, String label) throws InterruptedException { + String firstWord = waitForFirstWord(pageView, 10000); + Assert.assertEquals(label + " first word mismatch", expected, firstWord); + } - // Check if edit button becomes available (indicating successful load) - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled())) - .withFailureHandler((error, viewMatcher) -> { - onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click()); + private String waitForFirstWord(PageView pageView, long timeoutMs) throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + String firstWord = getFirstWord(pageView); + if (!firstWord.isEmpty()) { + return firstWord; + } + SystemClock.sleep(250); + } + return ""; + } + + private String getFirstWord(PageView pageView) throws InterruptedException { + String result = evaluateJavascript(pageView, + "(function(){" + + "var text = document.body ? (document.body.innerText || '') : '';" + + "text = text.replace(/\\s+/g,' ').trim();" + + "if (!text) return '';" + + "return text.split(' ')[0];" + + "})()"); + if (result == null) { + return ""; + } + return result.replace("\"", "").trim(); + } + + private String getPageViewUrl(PageView pageView) throws InterruptedException { + AtomicReference url = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + url.set(pageView.getUrl()); + latch.countDown(); + }); + if (!latch.await(5, TimeUnit.SECONDS)) { + Assert.fail("Timed out waiting for WebView URL"); + } + return url.get(); + } - onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed())) - .perform(click()); - }); + private boolean waitForEditableState(PageView pageView, boolean expected, long timeoutMs) + throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + if (expected == isEditableDom(pageView)) { + return true; + } + SystemClock.sleep(250); + } + return false; + } + + private boolean isEditableDom(PageView pageView) throws InterruptedException { + String result = evaluateJavascript(pageView, + "(function(){" + + "var bodyEditable = document.body && document.body.isContentEditable;" + + "var editableNode = document.querySelector('[contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]');" + + "return !!(bodyEditable || editableNode);" + + "})()"); + if (result == null) { + return false; + } + String normalized = result.replace("\"", ""); + return "true".equalsIgnoreCase(normalized); + } + + private String evaluateJavascript(PageView pageView, String script) throws InterruptedException { + AtomicReference result = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + pageView.evaluateJavascript(script, value -> { + result.set(value); + latch.countDown(); + }); + }); + if (!latch.await(10, TimeUnit.SECONDS)) { + Assert.fail("Timed out waiting for JS evaluation result"); + } + return result.get(); } } diff --git a/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityUiTests.java b/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityUiTests.java new file mode 100644 index 000000000000..5a20ae14c481 --- /dev/null +++ b/app/src/androidTest/java/at/tomtasche/reader/test/MainActivityUiTests.java @@ -0,0 +1,421 @@ +package at.tomtasche.reader.test; + +import android.content.Context; +import android.content.Intent; +import android.content.res.AssetManager; +import android.net.Uri; +import android.os.SystemClock; + +import androidx.core.content.FileProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; +import androidx.test.espresso.intent.Intents; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import android.util.ArrayMap; + +import at.tomtasche.reader.R; +import at.tomtasche.reader.ui.activity.DocumentFragment; +import at.tomtasche.reader.ui.activity.MainActivity; +import at.tomtasche.reader.ui.widget.PageView; + +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class MainActivityUiTests { + private static final long UI_TIMEOUT_MS = 10000; + private static final Map s_testFiles = new ArrayMap<>(); + private static final String EXPECTED_FIRST_WORD_ODT = "This"; + private static final String EXPECTED_FIRST_WORD_PDF = "Dummy"; + private static final String EXPECTED_FIRST_WORD_DOCX = "Table"; + private static final String EXPECTED_FIRST_WORD_PASSWORD_ODT = "Hallo"; + + @Rule + public ActivityTestRule mainActivityActivityTestRule = + new ActivityTestRule<>(MainActivity.class, false, false); + + private UiDevice device; + + @BeforeClass + public static void extractTestFiles() throws IOException { + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + File appCacheDir = appContext.getCacheDir(); + File testDocumentsDir = new File(appCacheDir, "test-documents"); + + testDocumentsDir.mkdirs(); + Assert.assertTrue(testDocumentsDir.exists()); + + AssetManager testAssetManager = InstrumentationRegistry.getInstrumentation().getContext().getAssets(); + for (String filename : new String[] {"test.odt", "dummy.pdf", "password-test.odt", "style-various-1.docx"}) { + File targetFile = new File(testDocumentsDir, filename); + try (InputStream inputStream = testAssetManager.open(filename)) { + copy(inputStream, targetFile); + } + s_testFiles.put(filename, targetFile); + } + } + + @AfterClass + public static void cleanupTestFiles() { + for (File file : s_testFiles.values()) { + file.delete(); + } + } + + @Before + public void setUp() { + MainActivity activity = mainActivityActivityTestRule.launchActivity(null); + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + device.wait(Until.hasObject(By.pkg(activity.getPackageName()).depth(0)), UI_TIMEOUT_MS); + Intents.init(); + } + + @After + public void tearDown() { + Intents.release(); + + MainActivity activity = mainActivityActivityTestRule.getActivity(); + if (activity != null) { + mainActivityActivityTestRule.finishActivity(); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + } + + @Test + public void testODTUi() throws Exception { + File testFile = s_testFiles.get("test.odt"); + Assert.assertNotNull(testFile); + stubOpenDocumentIntent(testFile); + + openDocumentViaUi(); + + DocumentFragment fragment = waitForDocumentFragment(); + PageView pageView = fragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("ODT should load", waitForPageLoaded(pageView, UI_TIMEOUT_MS)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_ODT, "ODT"); + } + + @Test + public void testPDFUi() throws Exception { + File testFile = s_testFiles.get("dummy.pdf"); + Assert.assertNotNull(testFile); + stubOpenDocumentIntent(testFile); + + openDocumentViaUi(); + + DocumentFragment fragment = waitForDocumentFragment(); + PageView pageView = fragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("PDF should load", waitForPageLoaded(pageView, UI_TIMEOUT_MS)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_PDF, "PDF"); + } + + @Test + public void testPasswordProtectedODTUi() throws Exception { + File testFile = s_testFiles.get("password-test.odt"); + Assert.assertNotNull(testFile); + stubOpenDocumentIntent(testFile); + + openDocumentViaUi(); + + waitForText("This document is password-protected"); + setPassword("wrongpassword"); + clickDialogOk(); + + waitForText("This document is password-protected"); + setPassword("passwort"); + clickDialogOk(); + + DocumentFragment fragment = waitForDocumentFragment(); + PageView pageView = fragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("Password-protected ODT should load", waitForPageLoaded(pageView, UI_TIMEOUT_MS)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_PASSWORD_ODT, "Password-protected ODT"); + } + + @Test + public void testODTEditModeUi() throws Exception { + File testFile = s_testFiles.get("test.odt"); + Assert.assertNotNull(testFile); + stubOpenDocumentIntent(testFile); + + openDocumentViaUi(); + + DocumentFragment fragment = waitForDocumentFragment(); + PageView pageView = fragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("ODT should load", waitForPageLoaded(pageView, UI_TIMEOUT_MS)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_ODT, "ODT"); + + clickEditMenu(); + String bannerText = mainActivityActivityTestRule.getActivity().getString(R.string.action_edit_banner); + waitForText(bannerText); + + Assert.assertTrue( + "ODT should become editable after entering edit mode", + waitForEditableState(pageView, true, UI_TIMEOUT_MS) + ); + } + + @Test + public void testDOCXEditModeUi() throws Exception { + File testFile = s_testFiles.get("style-various-1.docx"); + Assert.assertNotNull(testFile); + stubOpenDocumentIntent(testFile); + + openDocumentViaUi(); + + DocumentFragment fragment = waitForDocumentFragment(); + PageView pageView = fragment.getPageView(); + Assert.assertNotNull(pageView); + Assert.assertTrue("DOCX should load", waitForPageLoaded(pageView, UI_TIMEOUT_MS)); + assertFirstWord(pageView, EXPECTED_FIRST_WORD_DOCX, "DOCX"); + + clickEditMenu(); + String bannerText = mainActivityActivityTestRule.getActivity().getString(R.string.action_edit_banner); + waitForText(bannerText); + + Assert.assertTrue( + "DOCX should become editable after entering edit mode", + waitForEditableState(pageView, true, UI_TIMEOUT_MS) + ); + } + + private static void copy(InputStream src, File dst) throws IOException { + try (OutputStream out = new FileOutputStream(dst)) { + byte[] buf = new byte[1024]; + int len; + while ((len = src.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + + private void stubOpenDocumentIntent(File testFile) { + Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); + Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile); + Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith( + new android.app.Instrumentation.ActivityResult(android.app.Activity.RESULT_OK, + new Intent() + .setData(testFileUri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + ) + ); + } + + private void openDocumentViaUi() { + clickOpenMenu(); + clickFileManagerOption(); + } + + private void clickOpenMenu() { + UiObject2 open = device.wait(Until.findObject(By.desc("Open document")), UI_TIMEOUT_MS); + if (open == null) { + open = device.wait(Until.findObject(By.text("Open document")), UI_TIMEOUT_MS); + } + if (open == null) { + clickOverflowMenu(); + open = device.wait(Until.findObject(By.text("Open document")), UI_TIMEOUT_MS); + } + Assert.assertNotNull("Open document menu not found", open); + open.click(); + device.waitForIdle(); + } + + private void clickEditMenu() { + UiObject2 edit = device.wait(Until.findObject(By.desc("Edit document")), UI_TIMEOUT_MS); + if (edit == null) { + edit = device.wait(Until.findObject(By.text("Edit document")), UI_TIMEOUT_MS); + } + if (edit == null) { + clickOverflowMenu(); + edit = device.wait(Until.findObject(By.text("Edit document")), UI_TIMEOUT_MS); + } + Assert.assertNotNull("Edit document menu not found", edit); + edit.click(); + device.waitForIdle(); + } + + private void clickOverflowMenu() { + UiObject2 more = device.wait(Until.findObject(By.desc("More options")), UI_TIMEOUT_MS); + Assert.assertNotNull("Overflow menu not found", more); + more.click(); + device.waitForIdle(); + } + + private void clickFileManagerOption() { + UiObject2 documents = device.wait(Until.findObject(By.text("Documents")), UI_TIMEOUT_MS); + if (documents != null) { + documents.click(); + device.waitForIdle(); + return; + } + UiObject2 files = device.wait(Until.findObject(By.text("Files")), UI_TIMEOUT_MS); + if (files != null) { + files.click(); + device.waitForIdle(); + return; + } + Assert.fail("No file manager option found"); + } + + private void waitForText(String text) { + UiObject2 obj = device.wait(Until.findObject(By.text(text)), UI_TIMEOUT_MS); + Assert.assertNotNull("Expected text not found: " + text, obj); + } + + private void setPassword(String password) { + UiObject2 input = device.wait(Until.findObject(By.clazz("android.widget.EditText")), UI_TIMEOUT_MS); + Assert.assertNotNull("Password input not found", input); + input.setText(password); + device.waitForIdle(); + } + + private void clickDialogOk() { + UiObject2 ok = device.wait(Until.findObject(By.res("android", "button1")), UI_TIMEOUT_MS); + if (ok == null) { + ok = device.wait(Until.findObject(By.text("OK")), UI_TIMEOUT_MS); + } + Assert.assertNotNull("Dialog OK button not found", ok); + ok.click(); + device.waitForIdle(); + } + + private DocumentFragment waitForDocumentFragment() throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < UI_TIMEOUT_MS) { + MainActivity activity = mainActivityActivityTestRule.getActivity(); + DocumentFragment fragment = (DocumentFragment) activity.getSupportFragmentManager() + .findFragmentByTag("document_fragment"); + if (fragment != null && fragment.hasLastResult()) { + return fragment; + } + SystemClock.sleep(100); + } + Assert.fail("Timed out waiting for document fragment"); + return null; + } + + private boolean waitForPageLoaded(PageView pageView, long timeoutMs) throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + String url = getPageViewUrl(pageView); + if (url != null && !url.isEmpty() && !"about:blank".equals(url)) { + return true; + } + SystemClock.sleep(250); + } + return false; + } + + private void assertFirstWord(PageView pageView, String expected, String label) throws InterruptedException { + String firstWord = waitForFirstWord(pageView, UI_TIMEOUT_MS); + Assert.assertEquals(label + " first word mismatch", expected, firstWord); + } + + private String waitForFirstWord(PageView pageView, long timeoutMs) throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + String firstWord = getFirstWord(pageView); + if (!firstWord.isEmpty()) { + return firstWord; + } + SystemClock.sleep(250); + } + return ""; + } + + private String getFirstWord(PageView pageView) throws InterruptedException { + String result = evaluateJavascript(pageView, + "(function(){" + + "var text = document.body ? (document.body.innerText || '') : '';" + + "text = text.replace(/\\s+/g,' ').trim();" + + "if (!text) return '';" + + "return text.split(' ')[0];" + + "})()"); + if (result == null) { + return ""; + } + return result.replace("\"", "").trim(); + } + + private String getPageViewUrl(PageView pageView) throws InterruptedException { + AtomicReference url = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + url.set(pageView.getUrl()); + latch.countDown(); + }); + if (!latch.await(5, TimeUnit.SECONDS)) { + Assert.fail("Timed out waiting for WebView URL"); + } + return url.get(); + } + + private boolean waitForEditableState(PageView pageView, boolean expected, long timeoutMs) + throws InterruptedException { + long startMs = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() - startMs < timeoutMs) { + if (expected == isEditableDom(pageView)) { + return true; + } + SystemClock.sleep(250); + } + return false; + } + + private boolean isEditableDom(PageView pageView) throws InterruptedException { + String result = evaluateJavascript(pageView, + "(function(){" + + "var bodyEditable = document.body && document.body.isContentEditable;" + + "var editableNode = document.querySelector('[contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]');" + + "return !!(bodyEditable || editableNode);" + + "})()"); + if (result == null) { + return false; + } + String normalized = result.replace("\"", ""); + return "true".equalsIgnoreCase(normalized); + } + + private String evaluateJavascript(PageView pageView, String script) throws InterruptedException { + AtomicReference result = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + pageView.evaluateJavascript(script, value -> { + result.set(value); + latch.countDown(); + }); + }); + if (!latch.await(10, TimeUnit.SECONDS)) { + Assert.fail("Timed out waiting for JS evaluation result"); + } + return result.get(); + } +} 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");