Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ version code per locale under `fastlane/metadata/android/<locale>/changelogs/`,
written by `scripts/store-copy.py` before the release and uploaded by it. Play
takes 500 characters, so not everything here reaches the store.

## Unreleased

- A spreadsheet too big to show in full says so, and names how many of its rows
and columns are on screen. It used to stop without a word.
- How much of a sheet is shown follows the device's memory now, rather than one
number for every phone. A big sheet used to take the app past what the phone
could hold and fail to open at all.

## 4.19.1

- The app no longer closes when the screen is rotated while an advertisement is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ class DocumentParcelTest {
Uri.parse("http://localhost:29665/file/odr/1.html"),
Uri.parse("http://localhost:29665/file/odr/2.html"),
),
// the middle sheet is the only one the budget cut
listOf(null, SheetCut(80000, 12, 8333, 12), null),
isEditable = true,
readsAsDocument = true,
)
Expand All @@ -116,6 +118,17 @@ class DocumentParcelTest {
assertEquals(document.partUris, restored.partUris)
assertTrue(restored.isEditable)
assertTrue(restored.readsAsDocument)

assertNull(restored.partCuts[0])
assertNull(restored.partCuts[2])

val cut = checkNotNull(restored.partCuts[1])
assertEquals(80000, cut.contentRows)
assertEquals(12, cut.contentColumns)
assertEquals(8333, cut.renderedRows)
assertEquals(12, cut.renderedColumns)
assertTrue(cut.rowsWereCut)
assertEquals(false, cut.columnsWereCut)
}

/** Everything but a spreadsheet: one part, and the core does not name it. */
Expand All @@ -133,6 +146,7 @@ class DocumentParcelTest {
),
listOf<String?>(null),
listOf(Uri.parse("http://localhost:29665/file/odr/document.html")),
listOf(null),
isEditable = false,
readsAsDocument = true,
),
Expand All @@ -141,6 +155,7 @@ class DocumentParcelTest {

assertEquals(1, restored.partTitles.size)
assertNull(restored.partTitles[0])
assertNull(restored.partCuts[0])
assertEquals(false, restored.isEditable)
assertTrue(restored.readsAsDocument)
}
Expand Down
68 changes: 68 additions & 0 deletions app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import app.opendocument.core.FileType
import app.opendocument.core.OdrException
import app.opendocument.droid.background.CoreLoader
import app.opendocument.droid.background.SpreadsheetBudget
import app.opendocument.droid.nonfree.CrashManager
import java.io.File
import java.io.FileOutputStream
Expand Down Expand Up @@ -286,6 +287,48 @@ class CoreTest {
return URL(views.first().url).readText()
}

/**
* A cut sheet says how much of it was written. Generated rather than shipped, since
* `SpreadsheetBudget` answers per device.
*/
@Test
fun aSheetPastTheBudgetSaysWhatItLeftOut() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val budget = SpreadsheetBudget.cells(context)

val columns = 10
val expectedRows = (budget / columns).toInt()
val rows = expectedRows + 500

val views =
coreLoader.host(
prefix = "big-sheet",
inputPath = generateCsv(rows, columns).absolutePath,
cachePath = File(cacheDir(), "big_sheet_cache").path,
)

val cut =
checkNotNull(views.first().sheetCut) { "a sheet past the budget should report a cut" }

Assert.assertEquals("every row written should be counted", rows, cut.contentRows)
Assert.assertEquals(columns, cut.contentColumns)
Assert.assertEquals("the budget decides the rows", expectedRows, cut.renderedRows)
Assert.assertEquals("a narrow sheet loses no columns", columns, cut.renderedColumns)
}

/** The other side of it: a sheet written whole reports nothing to say. */
@Test
fun aSheetInsideTheBudgetReportsNoCut() {
val views =
coreLoader.host(
prefix = "whole-sheet",
inputPath = spreadsheetTestFile.absolutePath,
cachePath = File(cacheDir(), "whole_sheet_cache").path,
)

views.forEach { Assert.assertNull("nothing was cut from " + it.name, it.sheetCut) }
}

@Test
fun testSpreadsheetSheetNames() {
val views =
Expand Down Expand Up @@ -372,6 +415,31 @@ class CoreTest {
private fun cacheDir(): File =
InstrumentationRegistry.getInstrumentation().targetContext.cacheDir

/** A csv of [rows] x [columns] cells, each one a short string. */
private fun generateCsv(rows: Int, columns: Int): File {
val target = File(cacheDir(), "generated-sheet.csv")

target.bufferedWriter().use { writer ->
for (row in 1..rows) {
for (column in 1..columns) {
if (column > 1) {
writer.write(",")
}

writer.write("r")
writer.write(row.toString())
writer.write("c")
writer.write(column.toString())
}

writer.write("\n")
}
}
extracted += target

return target
}

private fun extract(name: String): File {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val target = File(instrumentation.targetContext.cacheDir, name)
Expand Down
29 changes: 17 additions & 12 deletions app/src/main/java/app/opendocument/droid/background/CoreLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class CoreLoader(private val context: Context) {
file,
views.map { it.name },
views.map { Uri.parse(it.url) },
views.map { it.sheetCut },
isDocumentEditable,
readsAsDocument,
)
Expand Down Expand Up @@ -186,10 +187,10 @@ class CoreLoader(private val context: Context) {
// document. PageView.setDarkeningAllowed picks between them
htmlConfig.colorScheme = HtmlColorScheme.SYSTEM

// stated rather than inherited: a sheet past it is cut off silently
// stated rather than inherited, and this device's answer - see SpreadsheetBudget
htmlConfig.spreadsheetLimit =
TableDimensions(SPREADSHEET_LIMIT_ROWS, SPREADSHEET_LIMIT_COLUMNS)
htmlConfig.spreadsheetCellLimit = SPREADSHEET_LIMIT_CELLS
TableDimensions(SpreadsheetBudget.ROWS, SpreadsheetBudget.COLUMNS)
htmlConfig.spreadsheetCellLimit = SpreadsheetBudget.cells(context)
htmlConfig.spreadsheetLimitByContent = true

val cacheDirectory = File(cachePath)
Expand All @@ -204,6 +205,14 @@ class CoreLoader(private val context: Context) {
HostedView(
view.name(),
"http://$SERVER_URL_HOST:$sharedServerPort/file/$prefix/" + view.path(),
view.sheetCut()?.let {
SheetCut(
it.content.rows,
it.content.columns,
it.rendered.rows,
it.rendered.columns,
)
},
)
}
}
Expand Down Expand Up @@ -330,22 +339,18 @@ class CoreLoader(private val context: Context) {
document = null
}

/** A translated view of a document, ready to be opened in the WebView. */
data class HostedView(val name: String, val url: String)
/**
* A translated view of a document, ready to be opened in the WebView. [sheetCut] is set only
* where the budget cut this view's sheet.
*/
data class HostedView(val name: String, val url: String, val sheetCut: SheetCut?)

/** An encrypted file whose format odrcore cannot decrypt, whatever the password. */
class UndecryptableFile(path: String) : IOException("cannot be decrypted: $path")

companion object {
private const val TAG = "CoreLoader"

/** The largest sheet region translated - every cell in it becomes a `<td>`. */
private const val SPREADSHEET_LIMIT_ROWS = 100000
private const val SPREADSHEET_LIMIT_COLUMNS = 500

/** Bounds the rows by the sheet's width. */
private const val SPREADSHEET_LIMIT_CELLS = 500000L

/**
* The one http server of the process, started on the first [initialize] and never stopped.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import android.os.Parcelable
* be, and one uri per part (spreadsheets have one per sheet, everything else a single one with a
* null title).
*
* [partCuts] runs alongside them, null for every part but a sheet that was cut.
*
* [isEditable] and [readsAsDocument] are the core's own answers about this document, never a guess
* from its mime type - see `CoreLoader.isDocumentEditable` and `CoreLoader.readsAsDocument`.
*/
Expand All @@ -17,6 +19,7 @@ class LoadedDocument(
val file: IdentifiedFile,
val partTitles: List<String?>,
val partUris: List<Uri>,
val partCuts: List<SheetCut?>,
val isEditable: Boolean,
val readsAsDocument: Boolean,
) : Parcelable {
Expand All @@ -28,6 +31,7 @@ class LoadedDocument(
parcel.writeParcelable(file, 0)
parcel.writeList(partTitles)
parcel.writeList(partUris)
parcel.writeList(partCuts)
ParcelUtil.writeBoolean(parcel, isEditable)
ParcelUtil.writeBoolean(parcel, readsAsDocument)
}
Expand All @@ -51,11 +55,15 @@ class LoadedDocument(
val partUris = ArrayList<Uri>()
parcel.readList(partUris, classLoader)

val partCuts = ArrayList<SheetCut?>()
parcel.readList(partCuts, classLoader)

return LoadedDocument(
request,
file,
partTitles,
partUris,
partCuts,
ParcelUtil.readBoolean(parcel),
ParcelUtil.readBoolean(parcel),
)
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/app/opendocument/droid/background/SheetCut.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package app.opendocument.droid.background

import android.os.Parcel
import android.os.Parcelable

/**
* How much of a sheet the markup carries against how much was written - odrcore's own
* `HtmlView.sheetCut()`, and only present where [SpreadsheetBudget] cut the sheet.
*/
class SheetCut(
val contentRows: Int,
val contentColumns: Int,
val renderedRows: Int,
val renderedColumns: Int,
) : Parcelable {

val rowsWereCut: Boolean
get() = renderedRows < contentRows

val columnsWereCut: Boolean
get() = renderedColumns < contentColumns

override fun describeContents(): Int = 0

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(contentRows)
parcel.writeInt(contentColumns)
parcel.writeInt(renderedRows)
parcel.writeInt(renderedColumns)
}

companion object {
// @JvmField because the framework looks CREATOR up as a static field
@JvmField
val CREATOR: Parcelable.Creator<SheetCut> =
object : Parcelable.Creator<SheetCut> {
override fun createFromParcel(parcel: Parcel): SheetCut =
SheetCut(
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
)

override fun newArray(size: Int): Array<SheetCut?> = arrayOfNulls(size)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app.opendocument.droid.background

import android.app.ActivityManager
import android.content.Context
import androidx.core.content.getSystemService

/**
* How much of a sheet is translated to html.
*
* The budget is the *WebView's*, not the core's: a rendered cell costs 10-20 KB in the renderer
* process against some 226 bytes of html, so a budget too high shows none of the document rather
* than more of it - the page fails to load and the file is reported as one that cannot be opened.
*/
object SpreadsheetBudget {

/** Each direction on its own, before [cells] narrows the two together. */
const val ROWS = 100000
const val COLUMNS = 500

/** What the device this is running on can afford to show. */
fun cells(context: Context): Long {
val activityManager = context.getSystemService<ActivityManager>()
val memoryInfo = ActivityManager.MemoryInfo()
activityManager?.getMemoryInfo(memoryInfo)

return cellsFor(memoryInfo.totalMem, activityManager?.isLowRamDevice == true)
}

/**
* Memory decides, since what is budgeted is the renderer process. [totalMemoryBytes] of zero is
* a device that would not answer, and takes the smallest step.
*/
fun cellsFor(totalMemoryBytes: Long, isLowRamDevice: Boolean): Long {
if (isLowRamDevice || totalMemoryBytes < 3L * GIGABYTE) {
return 50000
}

if (totalMemoryBytes < 6L * GIGABYTE) {
return 100000
}

return 150000
}

private const val GIGABYTE = 1024L * 1024L * 1024L
}
18 changes: 18 additions & 0 deletions app/src/main/java/app/opendocument/droid/ui/SnackbarHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ object SnackbarHelper {
)
}

/** Same, where the message carries numbers and is built rather than looked up. */
fun show(
activity: Activity,
message: String,
callback: Runnable?,
isIndefinite: Boolean,
isError: Boolean,
) {
show(
activity,
activity.getString(android.R.string.ok),
message,
callback,
isIndefinite,
isError,
)
}

private fun show(
activity: Activity,
buttonText: String,
Expand Down
Loading
Loading