Skip to content

Commit 43851c1

Browse files
committed
bounding box
1 parent 6e5e30c commit 43851c1

5 files changed

Lines changed: 72 additions & 14 deletions

File tree

app/src/main/java/com/alexdremov/notate/data/CanvasRepository.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class CanvasRepository(
4343
val savedPath: String,
4444
val newLastModified: Long,
4545
val newSize: Long,
46+
val isConflict: Boolean = false,
4647
)
4748

4849
private val sessionsDir: File by lazy {
@@ -632,6 +633,7 @@ class CanvasRepository(
632633
}
633634
}
634635

636+
val isConflict = targetPath != path
635637
atomicStorage.pack(session.sessionDir, targetPath)
636638

637639
val savedFile = File(targetPath)
@@ -648,7 +650,7 @@ class CanvasRepository(
648650
Logger.e("CanvasRepository", "Failed to update origin_info.txt after save", e)
649651
}
650652

651-
SaveResult(targetPath, newLastModified, newSize)
653+
SaveResult(targetPath, newLastModified, newSize, isConflict)
652654
}
653655
} catch (e: Exception) {
654656
Logger.e("CanvasRepository", "Failed to save session", e, showToUser = true)

app/src/main/java/com/alexdremov/notate/data/StorageProvider.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,45 @@ internal object StorageUtils {
8080
"$originalName Copy"
8181
}
8282

83+
fun getOriginInfo(
84+
context: Context,
85+
path: String,
86+
): Pair<Long, Long> {
87+
if (path.startsWith("content://")) {
88+
val uri = android.net.Uri.parse(path)
89+
return try {
90+
context.contentResolver
91+
.query(
92+
uri,
93+
arrayOf(
94+
android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED,
95+
android.provider.OpenableColumns.SIZE,
96+
),
97+
null,
98+
null,
99+
null,
100+
)?.use { cursor ->
101+
if (cursor.moveToFirst()) {
102+
val lastMod = cursor.getLong(0)
103+
val size = cursor.getLong(1)
104+
Pair(lastMod, size)
105+
} else {
106+
Pair(0L, 0L)
107+
}
108+
} ?: Pair(0L, 0L)
109+
} catch (e: Exception) {
110+
Pair(0L, 0L)
111+
}
112+
} else {
113+
val file = File(path)
114+
return if (file.exists()) {
115+
Pair(file.lastModified(), file.length())
116+
} else {
117+
Pair(0L, 0L)
118+
}
119+
}
120+
}
121+
83122
fun injectUuidIntoJson(
84123
inputStream: InputStream,
85124
outputStream: OutputStream,

app/src/main/java/com/alexdremov/notate/ui/input/PenInputHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ class PenInputHandler(
602602
segmentPath.moveTo(newTouchPoints[0].x, newTouchPoints[0].y)
603603
for (i in 1 until newTouchPoints.size) {
604604
segmentPath.lineTo(
605-
newTouchPoints[i].size,
605+
newTouchPoints[i].x,
606606
newTouchPoints[i].y,
607607
)
608608
}

app/src/main/java/com/alexdremov/notate/vm/DrawingViewModel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import com.alexdremov.notate.model.ToolType
1111
import com.alexdremov.notate.model.ToolbarItem
1212
import com.alexdremov.notate.util.Logger
1313
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.flow.MutableSharedFlow
1415
import kotlinx.coroutines.flow.MutableStateFlow
1516
import kotlinx.coroutines.flow.StateFlow
17+
import kotlinx.coroutines.flow.asSharedFlow
1618
import kotlinx.coroutines.flow.asStateFlow
1719
import kotlinx.coroutines.launch
1820
import kotlinx.coroutines.sync.withLock
@@ -26,6 +28,16 @@ class DrawingViewModel
2628
com.alexdremov.notate.data
2729
.CanvasRepository(application),
2830
) : AndroidViewModel(application) {
31+
sealed class SaveEvent {
32+
data class Conflict(
33+
val originalPath: String,
34+
val conflictPath: String,
35+
) : SaveEvent()
36+
}
37+
38+
private val _saveEvents = MutableSharedFlow<SaveEvent>()
39+
val saveEvents = _saveEvents.asSharedFlow()
40+
2941
// Session State
3042
private val _currentSession = MutableStateFlow<com.alexdremov.notate.data.CanvasSession?>(null)
3143
val currentSession: StateFlow<com.alexdremov.notate.data.CanvasSession?> = _currentSession.asStateFlow()
@@ -140,6 +152,11 @@ class DrawingViewModel
140152
// Update origin timestamps in-place
141153
session.updateOrigin(result.newLastModified, result.newSize)
142154

155+
if (result.isConflict) {
156+
Logger.w("DrawingViewModel", "Conflict detected! Saved as copy: ${result.savedPath}")
157+
_saveEvents.emit(SaveEvent.Conflict(path, result.savedPath))
158+
}
159+
143160
if (commit) {
144161
Logger.i("DrawingViewModel", "Full Save complete: ${result.savedPath}")
145162
}

app/src/test/java/com/alexdremov/notate/data/CanvasRepositoryCachingTest.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@ class CanvasRepositoryCachingTest {
3131
fun `test active session caching - same file should return same instance`() =
3232
runBlocking {
3333
val path = File(testDir, "cache_test.notate").absolutePath
34-
34+
3535
// 1. Open session first time
3636
val session1 = repository.openCanvasSession(path)!!
37-
37+
3838
// 2. Open session second time without releasing first
3939
val session2 = repository.openCanvasSession(path)!!
40-
40+
4141
// 3. They MUST be the exact same instance (cached)
4242
assertSame("Should return the exact same session instance from cache", session1, session2)
43-
43+
4444
repository.releaseCanvasSession(session1)
4545
repository.releaseCanvasSession(session2)
4646
}
@@ -49,37 +49,37 @@ class CanvasRepositoryCachingTest {
4949
fun `test session caching after release - should return same instance if not closed`() =
5050
runBlocking {
5151
val path = File(testDir, "hot_handoff_test.notate").absolutePath
52-
52+
5353
val session1 = repository.openCanvasSession(path)!!
54-
54+
5555
// Release session1, but it might still be in activeSessions if no close was triggered
5656
// Actually, release returns if it was the last client.
5757
repository.releaseCanvasSession(session1)
58-
58+
5959
// Since it was the only client, it should have been closed and removed from cache.
6060
// Let's verify that a NEW open returns a DIFFERENT instance (reloaded)
6161
val session2 = repository.openCanvasSession(path)!!
6262
assertNotSame("Should return a new instance after full release/close", session1, session2)
63-
63+
6464
repository.releaseCanvasSession(session2)
6565
}
6666

6767
@Test
6868
fun `test session persistence - cold re-open should return different instance but same state`() =
6969
runBlocking {
7070
val path = File(testDir, "persistence_test.notate").absolutePath
71-
71+
7272
val session1 = repository.openCanvasSession(path)!!
7373
session1.updateMetadata(session1.metadata.copy(offsetX = 999f))
7474
repository.saveCanvasSession(path, session1)
7575
repository.releaseCanvasSession(session1)
76-
76+
7777
// Cold re-open (new instance, but reuses session directory if valid)
7878
val session2 = repository.openCanvasSession(path)!!
79-
79+
8080
assertNotSame("Should be a different instance", session1, session2)
8181
assertEquals("Should have persisted state", 999f, session2.metadata.offsetX)
82-
82+
8383
repository.releaseCanvasSession(session2)
8484
}
8585
}

0 commit comments

Comments
 (0)