Skip to content
Merged

Dev #277

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
37 changes: 19 additions & 18 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ android {

versionCode 35
versionName '0.2.1'
if (project.hasProperty('IS_NEXT') && project.IS_NEXT.toBoolean()) {
if (project.hasProperty('IS_NEXT') && project.property('IS_NEXT').toBoolean()) {
def timestamp = new Date().format('dd.MM.YYYY-HH:mm')
versionName = "${versionName}-next-${timestamp}"
}
Expand All @@ -28,7 +28,7 @@ android {
useSupportLibrary = true
}
ksp {
arg('room.schemaLocation', "$projectDir/schemas")
arg('room.schemaLocation', "${project.projectDir}/schemas")
}
}

Expand Down Expand Up @@ -95,7 +95,7 @@ android {
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = rootProject.compose_version
kotlinCompilerExtensionVersion = libs.versions.compose.get()
}
packaging {

Expand All @@ -108,7 +108,7 @@ android {
}
namespace = 'com.ethran.notable'
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
androidTest.assets.srcDirs += files("${project.projectDir}/schemas")
}
testOptions {
unitTests {
Expand All @@ -117,22 +117,23 @@ android {
// HiddenApiBypass needs deep reflection into java.lang on JDK 17+, which
// requires explicit --add-opens flags. Without these, Robolectric tests
// fail at class-init with ExceptionInInitializerError / NoSuchMethodException.
all {
jvmArgs += [
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.ch=ALL-UNNAMED',
]
}
}
}
}

tasks.withType(Test).configureEach {
jvmArgs += [
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED',
'--add-opens=java.base/sun.nio.ch=ALL-UNNAMED',
]
}

dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.ui
Expand All @@ -144,7 +145,7 @@ dependencies {
implementation libs.androidx.fragment.ktx
implementation libs.androidx.graphics.core
implementation libs.androidx.input.motionprediction
implementation libs.androidx.junit.ktx
androidTestImplementation libs.androidx.junit.ktx

//implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation(libs.onyxsdk.device) {
Expand Down Expand Up @@ -191,7 +192,7 @@ dependencies {
ksp libs.androidx.room.compiler

// For testing:
implementation libs.androidx.room.testing.android
androidTestImplementation libs.androidx.room.testing.android
testImplementation libs.androidx.room.testing
testImplementation libs.androidx.core.testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
Expand All @@ -47,6 +46,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

/**
* Thread-safety tests for SelectionState.
Expand Down Expand Up @@ -136,7 +137,7 @@ class EditorSelectionThreadSafetyTests {
val watchedStates = selectionState.snapshotDelegateStatesForTest()
val violations = ConcurrentLinkedQueue<String>()
val handle = Snapshot.registerGlobalWriteObserver { stateObject ->
if (stateObject in watchedStates && Thread.currentThread() != mainThread) {
if ((stateObject in watchedStates) && (Thread.currentThread() != mainThread)) {
violations.add("write on ${Thread.currentThread().name}")
}
}
Expand Down Expand Up @@ -217,42 +218,41 @@ class EditorSelectionThreadSafetyTests {
}

private suspend fun awaitToolbarPage(viewModel: EditorViewModel, expectedPageId: String) {
withTimeout(5_000) {
withTimeout(5.seconds) {
viewModel.toolbarState
.filter { it.pageId == expectedPageId }
.first()
.first { it.pageId == expectedPageId }
}
}

private suspend fun awaitSelectionReset(viewModel: EditorViewModel) {
withTimeout(5_000) {
withTimeout(5.seconds) {
while (viewModel.selectionState.selectedStrokes != null ||
viewModel.selectionState.placementMode != null
) {
delay(16)
delay(16.milliseconds)
}
}
}
}

private fun SelectionState.snapshotDelegateStatesForTest(): Set<Any> {
return setOf(
delegate("firstPageCut\$delegate"),
delegate("secondPageCut\$delegate"),
delegate("selectedStrokes\$delegate"),
delegate("selectedImages\$delegate"),
delegate("selectedBitmap\$delegate"),
delegate("selectionStartOffset\$delegate"),
delegate("selectionDisplaceOffset\$delegate"),
delegate("selectionRect\$delegate"),
delegate("placementMode\$delegate"),
).filterNotNull().toSet()
delegate($$"firstPageCut$delegate"),
delegate($$"secondPageCut$delegate"),
delegate($$"selectedStrokes$delegate"),
delegate($$"selectedImages$delegate"),
delegate($$"selectedBitmap$delegate"),
delegate($$"selectionStartOffset$delegate"),
delegate($$"selectionDisplaceOffset$delegate"),
delegate($$"selectionRect$delegate"),
delegate($$"placementMode$delegate"),
).asSequence().filterNotNull().toSet()
}

private fun SelectionState.delegate(fieldName: String): Any? {
return try {
val field = javaClass.getDeclaredField(fieldName).apply { isAccessible = true }
field.get(this)
field[this]
} catch (e: Exception) {
android.util.Log.e("EditorTest", "Could not find delegate field: $fieldName", e)
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ethran.notable.data.AppRepository
Expand Down Expand Up @@ -38,11 +38,7 @@ import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
Expand Down Expand Up @@ -84,7 +80,10 @@ class EditorUnsupportedConcurrentChangeTests {
*/
@Test(timeout = 60_000)
fun selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose() {
android.util.Log.i("EditorTest", "Starting test: selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose")
android.util.Log.i(
"EditorTest",
"Starting test: selectionState_isNotWrittenOffMainThread_duringPageSwitch_compose"
)
val seeded = runBlocking {
android.util.Log.i("EditorTest", "Seeding notebook...")
TestNotebookSeeder.seedNotebook(db, pageCount = 3, strokesPerPage = 10)
Expand Down Expand Up @@ -114,7 +113,8 @@ class EditorUnsupportedConcurrentChangeTests {
android.util.Log.i("EditorTest", "Seeding selection on UI thread...")
composeRule.runOnUiThread {
try {
viewModel.selectionState.selectedStrokes = listOf(dummyStroke(pageId = seeded.pageIds.first()))
viewModel.selectionState.selectedStrokes =
listOf(dummyStroke(pageId = seeded.pageIds.first()))
viewModel.selectionState.placementMode = PlacementMode.Move
Snapshot.sendApplyNotifications()
} catch (e: Throwable) {
Expand Down
18 changes: 1 addition & 17 deletions app/src/main/java/com/ethran/notable/data/db/Kv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.ethran.notable.data.db

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
Expand Down Expand Up @@ -39,9 +38,6 @@ interface KvDao {
@Query("SELECT * FROM kv WHERE `key`=:key")
suspend fun get(key: String): Kv?

@Query("SELECT * FROM kv WHERE `key`=:key")
fun getLive(key: String): LiveData<Kv?>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun set(kv: Kv)

Expand All @@ -67,10 +63,6 @@ class KvRepository @Inject constructor(
db.get(key)
}

fun getLive(key: String): LiveData<Kv?> {
return db.getLive(key)
}

suspend fun set(kv: Kv) = withContext(Dispatchers.IO) {
checkPermission()
db.set(kv)
Expand Down Expand Up @@ -101,17 +93,9 @@ class KvProxy @Inject constructor(
private val cryptoHelper: CryptoHelper
) {
private val log = ShipBook.getLogger("KvProxy")
private val json = Json //{ ignoreUnknownKeys = true }
private val json = Json { ignoreUnknownKeys = true }


fun <T> observeKv(key: String, serializer: KSerializer<T>, default: T): LiveData<T?> {
return kvRepository.getLive(key).map {
if (it == null) return@map default
val jsonValue = it.value
json.decodeFromString(serializer, jsonValue)
}
}

suspend fun <T> get(key: String, serializer: KSerializer<T>): T? = withContext(Dispatchers.IO) {
val kv = kvRepository.get(key)
?: return@withContext null //returns null when there is no database
Expand Down
24 changes: 15 additions & 9 deletions app/src/main/java/com/ethran/notable/io/XoppFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import javax.inject.Inject
private const val PRESSURE_FACTOR = 0.5f

/**
* How many strokes are handed off to [importBook]'s onStrokeBatch callback before the next
* How many strokes are handed off to [XoppFile.importBook]'s onStrokeBatch callback before the next
* batch starts. Keeping this bounded means peak memory during import is proportional to one
* batch, not one full page.
*/
Expand All @@ -59,7 +59,7 @@ class XoppFile @Inject constructor(
@param:ApplicationContext private val context: Context,
private val pageRepo: PageRepository,
private val bookRepo: BookRepository,
private val appEventBus: AppEventBus
private val appEventBus: AppEventBus,
) {
private val log = ShipBook.getLogger("XoppFile")
private val scaleFactor = A4_WIDTH.toFloat() / SCREEN_WIDTH
Expand Down Expand Up @@ -165,7 +165,7 @@ class XoppFile @Inject constructor(
writer.write("\" width=\"")
writer.write((stroke.size * scaleFactor).toString())

if (stroke.pen == Pen.FOUNTAIN || stroke.pen == Pen.BRUSH || stroke.pen == Pen.PENCIL) {
if ((stroke.pen == Pen.FOUNTAIN) || (stroke.pen == Pen.BRUSH) || (stroke.pen == Pen.PENCIL)) {
stroke.points.forEach { point ->
writer.write(" ")
writer.write(
Expand Down Expand Up @@ -486,10 +486,9 @@ class XoppFile @Inject constructor(
var isFraction = false

while (i < end) {
val c = input[i]
when {
c == '.' -> isFraction = true
c in '0'..'9' -> {
when (val c = input[i]) {
'.' -> isFraction = true
in '0'..'9' -> {
val digit = c - '0'
if (isFraction) {
divisor *= 10.0
Expand All @@ -499,7 +498,7 @@ class XoppFile @Inject constructor(
}
}
// Scientific notation is rare; only then pay the allocation cost
c == 'e' || c == 'E' -> return input.subSequence(start, end).toString().toFloat()
'e', 'E' -> return input.subSequence(start, end).toString().toFloat()
else -> return input.subSequence(start, end).toString().toFloat()
}
i++
Expand Down Expand Up @@ -566,7 +565,14 @@ class XoppFile @Inject constructor(
}

points.add(StrokePoint(x, y, pressure, 0, 0))
if (i == 0) boundingBox.set(x, y, x, y) else boundingBox.union(x, y)
if (i == 0) {
boundingBox.left = x
boundingBox.top = y
boundingBox.right = x
boundingBox.bottom = y
} else {
boundingBox.union(x, y)
}
}

boundingBox.inset(-strokeSize, -strokeSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import javax.inject.Singleton

@Singleton
class FolderSyncService @Inject constructor(
private val appRepository: AppRepository
private val appRepository: AppRepository,
) {
private val folderSerializer = FolderSerializer

Expand All @@ -37,7 +37,7 @@ class FolderSyncService @Inject constructor(

localFolders.forEach { local ->
val remote = folderMap[local.id]
if (remote == null || local.updatedAt.after(remote.updatedAt)) {
if ((remote == null) || (local.updatedAt.after(remote.updatedAt))) {
folderMap[local.id] = local
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/ethran/notable/sync/SyncSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class SyncSettings(
val syncOnNoteClose: Boolean = true,
val wifiOnly: Boolean = false,
val uploadOnly: Boolean = false,
val syncedNotebookIds: Set<String> = emptySet()
val syncedNotebookIds: Set<String> = emptySet(),
)
3 changes: 2 additions & 1 deletion app/src/main/java/com/ethran/notable/ui/components/Switch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

Expand Down Expand Up @@ -133,7 +134,7 @@ fun OnOffSwitch(
Surface(
color = thumbColor,
modifier = Modifier
.offset(x = thumbOffset)
.offset { IntOffset(x = thumbOffset.roundToPx(), y = 0) }
.size(thumbSize)
.padding(2.dp)
) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
Expand Down Expand Up @@ -257,8 +258,9 @@ private fun SyncBehaviorSection(

if (state.syncSettings.syncEnabled) {
SettingToggleRow(
label = stringResource(
R.string.sync_auto_sync_label,
label = pluralStringResource(
R.plurals.sync_auto_sync_label,
state.syncSettings.syncInterval,
state.syncSettings.syncInterval
),
value = state.syncSettings.autoSync,
Expand Down
Loading
Loading