Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#11: Re-implementation of version tracking #13

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ARK Memo: Notes App by ARK Builders

_Implementation in progress_
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ android {
namespace 'dev.arkbuilders.arkmemo'
compileSdk 33

namespace 'dev.arkbuilders.arkmemo'

defaultConfig {
applicationId "dev.arkbuilders.arkmemo"
minSdk 26
Expand Down Expand Up @@ -96,8 +98,6 @@ dependencies {

implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'

implementation 'com.google.code.gson:gson:2.10.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dev.arkbuilders.arkmemo.models.GraphicNote
import dev.arkbuilders.arkmemo.models.TextNote
import dev.arkbuilders.arkmemo.preferences.MemoPreferences
import dev.arkbuilders.arkmemo.repo.NotesRepoHelper
import dev.arkbuilders.arkmemo.repo.versions.VersionStorageRepo


@InstallIn(SingletonComponent::class)
Expand All @@ -30,5 +31,10 @@ abstract class RepositoryModule {
memoPreferences: MemoPreferences,
propertiesStorageRepo: PropertiesStorageRepo
) = NotesRepoHelper(memoPreferences, propertiesStorageRepo)

@Provides
fun provideVersionStorageRepo(
memoPreferences: MemoPreferences
) = VersionStorageRepo(memoPreferences)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class GraphicNote(
override val title: String = "",
val description: String = "",
override val description: String = "",
override var isForked: Boolean = false,
@IgnoredOnParcel
val svg: SVG? = null,
@IgnoredOnParcel
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package dev.arkbuilders.arkmemo.models

import android.os.Parcelable
import dev.arkbuilders.arklib.data.index.Resource

interface Note {
interface Note: Parcelable {
val title: String
val description: String
var resource: Resource?
var isForked: Boolean
}
5 changes: 3 additions & 2 deletions app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class TextNote (
override val title: String = "",
val description: String = "",
override val description: String = "",
val text: String = "",
override var isForked: Boolean = false,
@IgnoredOnParcel
override var resource: Resource? = null
): Note, Parcelable
): Note, Parcelable
10 changes: 10 additions & 0 deletions app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.arkbuilders.arkmemo.models

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arkmemo.repo.versions.Version

data class VersionsResult (
val versions: List<Version>,
val parents: Set<ResourceId>,
val children: List<ResourceId>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package dev.arkbuilders.arkmemo.repo.versions

import android.util.Log
import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.arkFolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import dev.arkbuilders.arkmemo.models.VersionsResult
import space.taran.arkmemo.utils.arkVersions
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import kotlin.io.path.writeLines

class RootVersionStorage(private val root: Path): VersionStorage {

private val storageFile = root.arkFolder().arkVersions()
private var lastModified = FileTime.fromMillis(0L)
private val versions = mutableListOf<Version>()
private val parents = mutableSetOf<ResourceId>()
private val children = mutableListOf<ResourceId>()


suspend fun init() =
withContext(Dispatchers.IO) {
if (Files.exists(storageFile)) {
val result = readStorage()
lastModified = Files.getLastModifiedTime(storageFile)
Log.d(
VERSIONS_STORAGE,
"file $storageFile exists," +
" last modified at $lastModified"
)
versions.addAll(result.versions)
parents.addAll(result.parents)
children.addAll(result.children)
} else Log.d(
VERSIONS_STORAGE,
"file $storageFile doesn't exists"
)
}

override fun isLatestResourceVersion(id: ResourceId): Boolean =
childrenNotParents().contains(id)

private fun replace(oldVersion: Version, newVersion: Version) {
val replaceIndex = versions.indexOf(oldVersion)
versions[replaceIndex] = newVersion
}

override fun contains(id: ResourceId): Boolean {
return parents.contains(id) || children.contains(id)
}

override suspend fun add(version: Version) {
versions.add(version)
parents.add(version.parent)
children.add(version.child)
persist()
}

override suspend fun forget(id: ResourceId) {
if (!parents.contains(id)) {
val myParents = parentsTreeByChild(id)
myParents[id]?.forEach { parent ->
val version = versions
.find { it.parent == parent }
versions.remove(version)
parents.remove(version?.parent)
children.remove(version?.child)
}
}
if (parents.contains(id) && !children.contains(id)) {
val version = versions.find {
it.parent == id
}
versions.remove(version)
parents.remove(id)
}
if (parents.contains(id) && children.contains(id)) {
val versionIdIsChild = versions.find {
it.child == id
}
val versionIdIsParent = versions.find {
it.parent == id
}
val newVersion = Version(
versionIdIsChild?.parent!!,
versionIdIsParent?.child!!
)
replace(versionIdIsChild, newVersion)
versions.remove(versionIdIsParent)
parents.remove(id)
children.remove(id)
}
persist()
}

override fun versions() = versions

override fun parentsTreeByChild(
child: ResourceId
): Map<ResourceId, List<ResourceId>> {
var localChild = child
var parent: ResourceId?
val parents = mutableListOf<ResourceId>()
for (version in versions) {
parent = versions.find {
it.child == localChild
}?.parent
if (parent != null && children.contains(parent))
localChild = parent
if (parent != null) parents.add(parent)
if (!children.contains(parent))
break
}
return mapOf(child to parents)
}

override fun childrenNotParents(): List<ResourceId> {
return children.filter {
!parents.contains(it)
}
}

private suspend fun writeToStorage() =
withContext(Dispatchers.IO) {
val lines = mutableListOf<String>()
lines.add(
"$STORAGE_VERSION_PREFIX$STORAGE_VERSION"
)
lines.addAll(
versions.map {
"${it.parent}$KEY_VALUE_SEPARATOR${it.child}"
}
)
storageFile.writeLines(lines, Charsets.UTF_8)
}

private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile)
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
}

override suspend fun persist() =
withContext(Dispatchers.IO) {
writeToStorage()
return@withContext
}

companion object {
private const val VERSIONS_STORAGE = "versions"
private const val STORAGE_VERSION_PREFIX = "version "
private const val STORAGE_VERSION = 1
private const val KEY_VALUE_SEPARATOR = "->"

private fun verifyVersion(header: String) {
if (!header.startsWith(STORAGE_VERSION_PREFIX))
throw IllegalStateException("Unknown storage version")
val version = header.removePrefix(STORAGE_VERSION_PREFIX).toInt()
if (version > STORAGE_VERSION)
throw IllegalStateException("Storage version is newer than app")
if (version < STORAGE_VERSION)
throw IllegalStateException("Storage version is older than app")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.arkFolder
import dev.arkbuilders.arklib.data.storage.FileStorage
import kotlinx.coroutines.CoroutineScope
import space.taran.arkmemo.utils.arkVersions
import java.nio.file.Path

class RootVersionsStorage(
private val scope: CoroutineScope,
private val root: Path
):
FileStorage<Versions>("versions", scope, root.arkFolder().arkVersions(), VersionsMonoid),
VersionsStorage {

override fun valueFromString(raw: String): Versions =
raw.split(",").filter { it.isNotEmpty() }.map {
ResourceId.fromString(it)
}.toSet()

override fun valueToString(value: Versions): String = value.joinToString(",")
}
10 changes: 10 additions & 0 deletions app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId

typealias Version2 = ResourceId

data class Version(
val parent: ResourceId,
val child: ResourceId
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.data.storage.Storage

interface VersionStorage: Storage<Version2> {

suspend fun add(version: Version)

suspend fun forget(id: ResourceId)

fun versions(): List<Version>

fun contains(id: ResourceId): Boolean

fun parentsTreeByChild(
child: ResourceId
): Map<ResourceId, List<ResourceId>>

fun childrenNotParents(): List<ResourceId>

fun isLatestResourceVersion(id: ResourceId): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arkmemo.preferences.MemoPreferences
import java.nio.file.Path
import javax.inject.Inject

class VersionStorageRepo @Inject constructor(
private val memoPreferences: MemoPreferences
) {

private val storageByRoot = mutableMapOf<Path, RootVersionStorage>()

suspend fun provide(): VersionStorage {
val root = memoPreferences.getNotesStorage()
if (storageByRoot[root] == null) {
val versionStorage = RootVersionStorage(root)
versionStorage.init()
storageByRoot[root] = versionStorage
}
return storageByRoot[root]!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.arkbuilders.arkmemo.repo.versions

import android.util.Log
import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.data.storage.Monoid

typealias Versions = Set<ResourceId>

object VersionsMonoid : Monoid<Versions> {

override val neutral: Versions = setOf()

override fun combine(a: Versions, b: Versions): Versions {
val result = a.union(b)
Log.d(LOG_PREFIX, "merging $a and $b into $result")
return result
}
}

internal val LOG_PREFIX: String = "[versions]"
Loading
Loading