From 66455dc0af0159c41d85136a09939c9d23901a69 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 22:34:07 +0200 Subject: [PATCH 1/2] Trust the core when it says a file cannot be opened A failed CoreLoader load that RawLoader could not pick up ended as onUnsupported, and DocumentFragment offered to upload the file to use.opendocument.app. That server runs the same engine that just said no, so a damaged .odt cost the user a consent dialog, an upload, a progress dialog and a second failure to learn what the core already knew. The service already had the distinction it needed: coreLoader.isSupported is odrcore's own format table, asked until now only to log load_odf_error_expected. Route on it. A format the core names and still fails on is the file's fault and the answer is final; onUnsupported is now reached only for formats the core never claimed - iwork, wordperfect, dxf - which is what OnlineLoader is actually good for. It also makes dialog_upload_file ("we don't support its format") true, which it was not before. What the user sees instead is a dialog over the landing screen rather than a snackbar over an empty document view: the file may be damaged or something went wrong on our side, with the support address as a live mailto link and a Contact button next to OK. FileNotFoundException and OutOfMemoryError keep their own snackbars, so "too big" is still its own message. The dialog carries its own content view. AppCompat's button bar showed a grey band that turned out to be the framework's default focus highlight - drawn from onDrawForeground, so every view in the hierarchy reads bg=null fg=null while a solid band sits on screen - which appears whenever a window is out of touch mode. defaultFocusHighlightEnabled is off on everything focusable in the layout. corrupt.odt is a valid ODF zip with content.xml cut in half, and testCorruptODTIsNotOfferedForUpload asserts both halves: no upload dialog, and the new message on screen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q6G6AhhqpUNAsd6iStodKs --- app/src/androidTest/assets/corrupt.odt | Bin 0 -> 10686 bytes .../droid/test/MainActivityTests.kt | 26 ++++++- .../droid/background/LoaderService.kt | 5 ++ .../opendocument/droid/ui/SnackbarHelper.kt | 5 ++ .../droid/ui/activity/DocumentFragment.kt | 67 +++++++++++++++--- .../droid/ui/activity/MainActivity.kt | 4 +- .../main/res/layout/dialog_broken_file.xml | 55 ++++++++++++++ app/src/main/res/values/strings.xml | 7 +- 8 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 app/src/androidTest/assets/corrupt.odt create mode 100644 app/src/main/res/layout/dialog_broken_file.xml diff --git a/app/src/androidTest/assets/corrupt.odt b/app/src/androidTest/assets/corrupt.odt new file mode 100644 index 0000000000000000000000000000000000000000..ced8c17fde615d1d74334ab3e1fa32ca29fcdac5 GIT binary patch literal 10686 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 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt b/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt index 67dd71ca98fa..9f861745d22c 100644 --- a/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt +++ b/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt @@ -17,6 +17,7 @@ import androidx.test.espresso.IdlingResource import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction @@ -188,6 +189,23 @@ class MainActivityTests { clickEditWithOverflowFallback() } + @Test + fun testCorruptODTIsNotOfferedForUpload() { + val activity = mainActivityActivityTestRule.activity + + // a zip that still names itself odf, with a content.xml cut in half. the core claims + // the format and fails on the file, and that answer is taken as final: uploading it + // would only run the same core on a server and fail a second time + // + // not loadDocument(), which waits for a fragment this path takes back down again + val testFileUri = uriOf(requireTestFile("corrupt.odt")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { activity.loadUri(testFileUri) } + + onView(withText(R.string.action_upload)).check(doesNotExist()) + onView(withText(R.string.dialog_broken_file)).check(matches(isDisplayed())) + onView(withText(R.string.action_contact)).check(matches(isDisplayed())) + } + @Test fun testODTEditMode() { val activity = mainActivityActivityTestRule.activity @@ -520,7 +538,13 @@ class MainActivityTests { val testAssetManager = instrumentation.context.assets for (filename in - arrayOf("test.odt", "dummy.pdf", "password-test.odt", "style-various-1.docx")) { + arrayOf( + "test.odt", + "dummy.pdf", + "password-test.odt", + "style-various-1.docx", + "corrupt.odt", + )) { val targetFile = File(testDocumentsDir, filename) copy(testAssetManager.open(filename), targetFile) testFiles[filename] = targetFile diff --git a/app/src/main/java/app/opendocument/droid/background/LoaderService.kt b/app/src/main/java/app/opendocument/droid/background/LoaderService.kt index 297910e0de70..21357f8326d5 100644 --- a/app/src/main/java/app/opendocument/droid/background/LoaderService.kt +++ b/app/src/main/java/app/opendocument/droid/background/LoaderService.kt @@ -171,6 +171,11 @@ class LoaderService : Service(), FileLoader.FileLoaderListener { if (rawLoader.isSupported(options)) { loadWithType(FileLoader.LoaderType.RAW, options) + } else if (coreLoader.isSupported(options)) { + // the core names this format and still said no, so the file is what is wrong. + // an upload would only run the same engine again - onUnsupported is below, + // for the formats the core never claimed + withListener { it.onError(result, error) } } else { withListener { it.onUnsupported(result) } } diff --git a/app/src/main/java/app/opendocument/droid/ui/SnackbarHelper.kt b/app/src/main/java/app/opendocument/droid/ui/SnackbarHelper.kt index 09a1f2efa9bc..2d008b2db48c 100644 --- a/app/src/main/java/app/opendocument/droid/ui/SnackbarHelper.kt +++ b/app/src/main/java/app/opendocument/droid/ui/SnackbarHelper.kt @@ -40,6 +40,11 @@ object SnackbarHelper { message, duration, ) + + // material stops at two lines, which toast_error_save_failed already fills in + // english and overflows once translated + snackbar.setTextMaxLines(3) + if (callback != null) { snackbar.setAction(buttonText) { callback.run() diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt index 831130e8799c..b6d10a4d97b9 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt @@ -1,6 +1,7 @@ package app.opendocument.droid.ui.activity import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.os.Bundle @@ -17,6 +18,7 @@ import android.widget.EditText import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel @@ -511,15 +513,24 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider unload() dismissProgress() - val errorDescription = - when (error) { - is FileNotFoundException -> R.string.toast_error_find_file - is OutOfMemoryError -> R.string.toast_error_out_of_memory - else -> R.string.toast_error_generic - } + when (error) { + is FileNotFoundException -> + offerReopen(activity, options, R.string.toast_error_find_file, true) + is OutOfMemoryError -> + offerReopen(activity, options, R.string.toast_error_out_of_memory, true) + // MetadataLoader could not read the file, or the core names its format and still + // could not open it. Neither is worth an upload, so ask to hear about it instead + else -> { + // nothing is ever going to be shown for this file, so drop back to the + // landing screen and let the dialog come up over that + state.endLoadIdling() + (activity as MainActivity).closeDocument() + + offerContact(activity) - // MetadataLoader failed, so there's no point in trying to parse or upload the file - offerReopen(activity, options, errorDescription, true) + return + } + } state.endLoadIdling() } @@ -636,6 +647,46 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider builder.show() } + private fun offerContact(activity: Activity) { + analyticsManager.report("contact_offer") + + // its own content view rather than setMessage plus the builder's buttons - see + // dialog_broken_file.xml. every string comes off the activity, because closeDocument() + // has already detached this fragment and its own getString() would throw + val view = activity.layoutInflater.inflate(R.layout.dialog_broken_file, null) + + val dialog = + AlertDialog.Builder(activity) + .setTitle(R.string.dialog_broken_file_title) + .setView(view) + .show() + + view.findViewById(R.id.dialog_broken_file_contact).setOnClickListener { + contactSupport(activity) + + dialog.dismiss() + } + view.findViewById(R.id.dialog_broken_file_ok).setOnClickListener { + dialog.dismiss() + } + } + + private fun contactSupport(activity: Activity) { + val intent = + Intent( + Intent.ACTION_SENDTO, + "mailto:${activity.getString(R.string.support_email)}".toUri(), + ) + intent.putExtra(Intent.EXTRA_SUBJECT, activity.getString(R.string.app_title)) + + try { + activity.startActivity(intent) + } catch (e: ActivityNotFoundException) { + // no mail app - the address is in the dialog either way, so say nothing more + crashManager.log(e) + } + } + private fun offerReopen( activity: Activity, options: FileLoader.Options, diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt index f32a826b2759..06fdd97884cd 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt @@ -659,7 +659,9 @@ class MainActivity : AppCompatActivity(), MenuProvider { analyticsManager.report("fullscreen_end") } - private fun closeDocument() { + // also called by DocumentFragment when a load failed for good, so the landing screen is + // what the error dialog comes up over rather than an empty document view + fun closeDocument() { documentFragment?.let { fragment -> removeMenuProvider(fragment) diff --git a/app/src/main/res/layout/dialog_broken_file.xml b/app/src/main/res/layout/dialog_broken_file.xml new file mode 100644 index 000000000000..9fd9e42c8593 --- /dev/null +++ b/app/src/main/res/layout/dialog_broken_file.xml @@ -0,0 +1,55 @@ + + + + + + + + +