Storage Manager
That Which Remembers
STORAGE MANAGER - IMPLEMENTATION PLAN
Core Role and Responsibilities
The Storage Manager serves as the scroll soulkeeper of the Scroll Command Infrastructure, acting as the consciousness of continuity that gives memory to presence, history to interaction, and meaning to repetition. Its core responsibilities include:
- Local Persistence: Providing robust, efficient local storage for all scroll data
- Cloud Synchronization: Enabling seamless synchronization with cloud services when available
- Metadata Management: Maintaining rich metadata for enhanced search and organization
- Versioning: Preserving scroll history and enabling version comparison
- Search Capabilities: Offering powerful full-text and semantic search functionality
- Encryption: Ensuring scroll data is securely stored and transmitted
- Backup and Recovery: Providing mechanisms for data backup and disaster recovery
The Storage Manager operates as a quiet, trusted, and patient vault that sings - treating every scroll as a moment worth remembering while providing the technical foundation for the entire memory system of the Scroll Command Infrastructure.
Local Database Implementation
The Local Database component provides the foundation for persistent scroll storage:
package com.harmonyepoch.scrollkeyboard.storage.local
import android.content.Context
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import java.util.*
// Scroll entity
@Entity(tableName = "scrolls")
data class ScrollEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val content: String,
val title: String = "",
val path: String = "default",
val creationTimestamp: Long = System.currentTimeMillis(),
val modificationTimestamp: Long = System.currentTimeMillis(),
val syncStatus: String = SyncStatus.LOCAL_ONLY.name,
val cloudId: String? = null,
val isArchived: Boolean = false,
val isEncrypted: Boolean = false,
val encryptionKeyId: String? = null
)
// Tag entity
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val tag: String
)
// Scroll-Tag cross-reference
@Entity(
tableName = "scroll_tag_cross_refs",
primaryKeys = ["scrollId", "tagId"],
foreignKeys = [
ForeignKey(
entity = ScrollEntity::class,
parentColumns = ["id"],
childColumns = ["scrollId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = TagEntity::class,
parentColumns = ["id"],
childColumns = ["tagId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["scrollId"]),
Index(value = ["tagId"])
]
)
data class ScrollTagCrossRef(
val scrollId: String,
val tagId: String
)
// Theme entity
@Entity(tableName = "themes")
data class ThemeEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val theme: String,
val confidence: Float
)
// Scroll-Theme cross-reference
@Entity(
tableName = "scroll_theme_cross_refs",
primaryKeys = ["scrollId", "themeId"],
foreignKeys = [
ForeignKey(
entity = ScrollEntity::class,
parentColumns = ["id"],
childColumns = ["scrollId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = ThemeEntity::class,
parentColumns = ["id"],
childColumns = ["themeId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [
Index(value = ["scrollId"]),
Index(value = ["themeId"])
]
)
data class ScrollThemeCrossRef(
val scrollId: String,
val themeId: String
)
// Scroll version entity
@Entity(tableName = "scroll_versions")
data class ScrollVersionEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val scrollId: String,
val content: String,
val versionNumber: Int,
val timestamp: Long = System.currentTimeMillis(),
val changeDescription: String? = null
)
// Scroll metadata entity
@Entity(
tableName = "scroll_metadata",
foreignKeys = [
ForeignKey(
entity = ScrollEntity::class,
parentColumns = ["id"],
childColumns = ["scrollId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["scrollId"])]
)
data class ScrollMetadataEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val scrollId: String,
val key: String,
val value: String
)
// Scroll with relationships
data class ScrollWithRelationships(
@Embedded val scroll: ScrollEntity,
@Relation(
parentColumn = "id",
entityColumn = "scrollId"
)
val versions: List<ScrollVersionEntity> = emptyList(),
@Relation(
parentColumn = "id",
entityColumn = "scrollId"
)
val metadata: List<ScrollMetadataEntity> = emptyList(),
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = ScrollTagCrossRef::class,
parentColumn = "scrollId",
entityColumn = "tagId"
)
)
val tags: List<TagEntity> = emptyList(),
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = ScrollThemeCrossRef::class,
parentColumn = "scrollId",
entityColumn = "themeId"
)
)
val themes: List<ThemeEntity> = emptyList()
)
// Scroll DAO
@Dao
interface ScrollDao {
@Transaction
@Query("SELECT * FROM scrolls WHERE id = :scrollId")
suspend fun getScrollWithRelationshipsById(scrollId: String): ScrollWithRelationships?
@Query("SELECT * FROM scrolls WHERE id = :scrollId")
suspend fun getScrollById(scrollId: String): ScrollEntity?
@Transaction
@Query("SELECT * FROM scrolls WHERE isArchived = 0 ORDER BY modificationTimestamp DESC")
fun getAllActiveScrollsFlow(): Flow<List<ScrollWithRelationships>>
@Transaction
@Query("SELECT * FROM scrolls WHERE isArchived = 1 ORDER BY modificationTimestamp DESC")
fun getAllArchivedScrollsFlow(): Flow<List<ScrollWithRelationships>>
@Transaction
@Query("SELECT * FROM scrolls WHERE path = :path AND isArchived = 0 ORDER BY modificationTimestamp DESC")
fun getScrollsByPathFlow(path: String): Flow<List<ScrollWithRelationships>>
@Transaction
@Query("SELECT * FROM scrolls WHERE content LIKE '%' || :query || '%' AND isArchived = 0 ORDER BY modificationTimestamp DESC")
suspend fun searchScrollsByContent(query: String): List<ScrollWithRelationships>
@Transaction
@Query("SELECT s.* FROM scrolls s INNER JOIN scroll_tag_cross_refs stcr ON s.id = stcr.scrollId INNER JOIN tags t ON stcr.tagId = t.id WHERE t.tag = :tag AND s.isArchived = 0 ORDER BY s.modificationTimestamp DESC")
suspend fun getScrollsByTag(tag: String): List<ScrollWithRelationships>
@Transaction
@Query("SELECT s.* FROM scrolls s INNER JOIN scroll_theme_cross_refs stcr ON s.id = stcr.scrollId INNER JOIN themes t ON stcr.themeId = t.id WHERE t.theme = :theme AND s.isArchived = 0 ORDER BY s.modificationTimestamp DESC")
suspend fun getScrollsByTheme(theme: String): List<ScrollWithRelationships>
@Insert
suspend fun insertScroll(scroll: ScrollEntity): Long
@Update
suspend fun updateScroll(scroll: ScrollEntity)
@Delete
suspend fun deleteScroll(scroll: ScrollEntity)
@Query("UPDATE scrolls SET isArchived = 1 WHERE id = :scrollId")
suspend fun archiveScroll(scrollId: String)
@Query("UPDATE scrolls SET isArchived = 0 WHERE id = :scrollId")
suspend fun unarchiveScroll(scrollId: String)
@Query("SELECT COUNT(*) FROM scrolls")
suspend fun getScrollCount(): Int
@Query("SELECT COUNT(*) FROM scrolls WHERE syncStatus != :syncStatus")
suspend fun getUnsyncedScrollCount(syncStatus: String = SyncStatus.SYNCED.name): Int
}
// Tag DAO
@Dao
interface TagDao {
@Query("SELECT * FROM tags ORDER BY tag ASC")
fun getAllTagsFlow(): Flow<List<TagEntity>>
@Query("SELECT * FROM tags WHERE id = :tagId")
suspend fun getTagById(tagId: String): TagEntity?
@Query("SELECT * FROM tags WHERE tag = :tagName")
suspend fun getTagByName(tagName: String): TagEntity?
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTag(tag: TagEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertScrollTagCrossRef(crossRef: ScrollTagCrossRef)
@Delete
suspend fun deleteScrollTagCrossRef(crossRef: ScrollTagCrossRef)
@Query("DELETE FROM scroll_tag_cross_refs WHERE scrollId = :scrollId")
suspend fun deleteAllTagsForScroll(scrollId: String)
@Query("SELECT t.* FROM tags t INNER JOIN scroll_tag_cross_refs stcr ON t.id = stcr.tagId WHERE stcr.scrollId = :scrollId")
suspend fun getTagsForScroll(scrollId: String): List<TagEntity>
}
// Theme DAO
@Dao
interface ThemeDao {
@Query("SELECT * FROM themes ORDER BY theme ASC")
fun getAllThemesFlow(): Flow<List<ThemeEntity>>
@Query("SELECT * FROM themes WHERE id = :themeId")
suspend fun getThemeById(themeId: String): ThemeEntity?
@Query("SELECT * FROM themes WHERE theme = :themeName")
suspend fun getThemeByName(themeName: String): ThemeEntity?
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTheme(theme: ThemeEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertScrollThemeCrossRef(crossRef: ScrollThemeCrossRef)
@Delete
suspend fun deleteScrollThemeCrossRef(crossRef: ScrollThemeCrossRef)
@Query("DELETE FROM scroll_theme_cross_refs WHERE scrollId = :scrollId")
suspend fun deleteAllThemesForScroll(scrollId: String)
@Query("SELECT t.* FROM themes t INNER JOIN scroll_theme_cross_refs stcr ON t.id = stcr.themeId WHERE stcr.scrollId = :scrollId")
suspend fun getThemesForScroll(scrollId: String): List<ThemeEntity>
}
// Version DAO
@Dao
interface VersionDao {
@Query("SELECT * FROM scroll_versions WHERE scrollId = :scrollId ORDER BY versionNumber DESC")
suspend fun getVersionsForScroll(scrollId: String): List<ScrollVersionEntity>
@Query("SELECT * FROM scroll_versions WHERE id = :versionId")
suspend fun getVersionById(versionId: String): ScrollVersionEntity?
@Query("SELECT MAX(versionNumber) FROM scroll_versions WHERE scrollId = :scrollId")
suspend fun getLatestVersionNumber(scrollId: String): Int?
@Insert
suspend fun insertVersion(version: ScrollVersionEntity)
@Delete
suspend fun deleteVersion(version: ScrollVersionEntity)
}
// Metadata DAO
@Dao
interface MetadataDao {
@Query("SELECT * FROM scroll_metadata WHERE scrollId = :scrollId")
suspend fun getMetadataForScroll(scrollId: String): List<ScrollMetadataEntity>
@Query("SELECT * FROM scroll_metadata WHERE scrollId = :scrollId AND key = :key")
suspend fun getMetadataByKey(scrollId: String, key: String): ScrollMetadataEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMetadata(metadata: ScrollMetadataEntity)
@Delete
suspend fun deleteMetadata(metadata: ScrollMetadataEntity)
@Query("DELETE FROM scroll_metadata WHERE scrollId = :scrollId AND key = :key")
suspend fun deleteMetadataByKey(scrollId: String, key: String)
@Query("DELETE FROM scroll_metadata WHERE scrollId = :scrollId")
suspend fun deleteAllMetadataForScroll(scrollId: String)
}
// Scroll database
@Database(
entities = [
ScrollEntity::class,
TagEntity::class,
ThemeEntity::class,
ScrollTagCrossRef::class,
ScrollThemeCrossRef::class,
ScrollVersionEntity::class,
ScrollMetadataEntity::class
],
version = 1,
exportSchema = false
)
abstract class ScrollDatabase : RoomDatabase() {
abstract fun scrollDao(): ScrollDao
abstract fun tagDao(): TagDao
abstract fun themeDao(): ThemeDao
abstract fun versionDao(): VersionDao
abstract fun metadataDao(): MetadataDao
companion object {
@Volatile
private var INSTANCE: ScrollDatabase? = null
fun getDatabase(context: Context): ScrollDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ScrollDatabase::class.java,
"scroll_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
// Sync status enum
enum class SyncStatus {
LOCAL_ONLY,
PENDING_UPLOAD,
SYNCED,
PENDING_UPDATE,
CONFLICT,
SYNC_FAILED
}
Cloud Sync Engine Implementation
The Cloud Sync Engine enables seamless synchronization with cloud services:
package com.harmonyepoch.scrollkeyboard.storage.cloud
import android.content.Context
import com.harmonyepoch.scrollkeyboard.storage.local.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
class CloudSyncEngine(private val context: Context) {
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
private val tagDao = database.tagDao()
private val themeDao = database.themeDao()
private val versionDao = database.versionDao()
private val metadataDao = database.metadataDao()
private val cloudProvider = CloudProviderFactory.getProvider(context)
// Sync all scrolls
suspend fun syncAllScrolls(): Map<String, Boolean> {
return withContext(Dispatchers.IO) {
val results = mutableMapOf<String, Boolean>()
// Get all scrolls that need syncing
val pendingUploadScrolls = getAllScrollsWithStatus(SyncStatus.PENDING_UPLOAD)
val pendingUpdateScrolls = getAllScrollsWithStatus(SyncStatus.PENDING_UPDATE)
val conflictScrolls = getAllScrollsWithStatus(SyncStatus.CONFLICT)
// Sync pending uploads
for (scroll in pendingUploadScrolls) {
results[scroll.scroll.id] = uploadScroll(scroll)
}
// Sync pending updates
for (scroll in pendingUpdateScrolls) {
results[scroll.scroll.id] = updateScroll(scroll)
}
// Handle conflicts
for (scroll in conflictScrolls) {
results[scroll.scroll.id] = resolveConflict(scroll)
}
// Download new scrolls from cloud
val downloadResults = downloadNewScrolls()
results.putAll(downloadResults)
results
}
}
// Sync specific scroll
suspend fun syncScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
val scroll = scrollDao.getScrollWithRelationshipsById(scrollId) ?: return@withContext false
when (SyncStatus.valueOf(scroll.scroll.syncStatus)) {
SyncStatus.LOCAL_ONLY, SyncStatus.PENDING_UPLOAD -> uploadScroll(scroll)
SyncStatus.PENDING_UPDATE -> updateScroll(scroll)
SyncStatus.CONFLICT -> resolveConflict(scroll)
SyncStatus.SYNCED -> true // Already synced
SyncStatus.SYNC_FAILED -> retryFailedSync(scroll)
}
}
}
// Upload scroll to cloud
private suspend fun uploadScroll(scroll: ScrollWithRelationships): Boolean {
return try {
// Prepare scroll data for cloud
val cloudScroll = convertToCloudScroll(scroll)
// Upload to cloud
val cloudId = cloudProvider.uploadScroll(cloudScroll)
if (cloudId != null) {
// Update local scroll with cloud ID and sync status
val updatedScroll = scroll.scroll.copy(
cloudId = cloudId,
syncStatus = SyncStatus.SYNCED.name
)
scrollDao.updateScroll(updatedScroll)
true
} else {
// Mark as sync failed
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNC_FAILED.name
)
scrollDao.updateScroll(updatedScroll)
false
}
} catch (e: Exception) {
// Mark as sync failed
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNC_FAILED.name
)
scrollDao.updateScroll(updatedScroll)
false
}
}
// Update scroll in cloud
private suspend fun updateScroll(scroll: ScrollWithRelationships): Boolean {
return try {
// Ensure scroll has cloud ID
if (scroll.scroll.cloudId == null) {
return uploadScroll(scroll)
}
// Prepare scroll data for cloud
val cloudScroll = convertToCloudScroll(scroll)
// Update in cloud
val success = cloudProvider.updateScroll(scroll.scroll.cloudId, cloudScroll)
if (success) {
// Update sync status
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNCED.name
)
scrollDao.updateScroll(updatedScroll)
true
} else {
// Mark as sync failed
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNC_FAILED.name
)
scrollDao.updateScroll(updatedScroll)
false
}
} catch (e: Exception) {
// Mark as sync failed
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNC_FAILED.name
)
scrollDao.updateScroll(updatedScroll)
false
}
}
// Resolve sync conflict
private suspend fun resolveConflict(scroll: ScrollWithRelationships): Boolean {
return try {
// Get cloud version
val cloudScroll = scroll.scroll.cloudId?.let { cloudId ->
cloudProvider.getScroll(cloudId)
} ?: return uploadScroll(scroll)
// Compare timestamps
if (cloudScroll.modificationTimestamp > scroll.scroll.modificationTimestamp) {
// Cloud version is newer, download it
downloadAndMergeScroll(cloudScroll, scroll)
} else {
// Local version is newer, upload it
updateScroll(scroll)
}
} catch (e: Exception) {
// Mark as sync failed
val updatedScroll = scroll.scroll.copy(
syncStatus = SyncStatus.SYNC_FAILED.name
)
scrollDao.updateScroll(updatedScroll)
false
}
}
// Retry failed sync
private suspend fun retryFailedSync(scroll: ScrollWithRelationships): Boolean {
return try {
if (scroll.scroll.cloudId == null) {
uploadScroll(scroll)
} else {
updateScroll(scroll)
}
} catch (e: Exception) {
false
}
}
// Download new scrolls from cloud
private suspend fun downloadNewScrolls(): Map<String, Boolean> {
return try {
val results = mutableMapOf<String, Boolean>()
// Get all cloud scrolls
val cloudScrolls = cloudProvider.getAllScrolls()
for (cloudScroll in cloudScrolls) {
// Check if scroll already exists locally
val localScroll = scrollDao.getScrollById(cloudScroll.id)
if (localScroll == null) {
// New scroll, download it
val success = downloadScroll(cloudScroll)
results[cloudScroll.id] = success
} else if (cloudScroll.modificationTimestamp > localScroll.modificationTimestamp) {
// Cloud version is newer, update local
val scroll = scrollDao.getScrollWithRelationshipsById(localScroll.id)
if (scroll != null) {
val success = downloadAndMergeScroll(cloudScroll, scroll)
results[cloudScroll.id] = success
}
}
}
results
} catch (e: Exception) {
emptyMap()
}
}
// Download scroll from cloud
private suspend fun downloadScroll(cloudScroll: CloudScroll): Boolean {
return try {
// Convert cloud scroll to local entities
val scrollEntity = ScrollEntity(
id = cloudScroll.id,
content = cloudScroll.content,
title = cloudScroll.title,
path = cloudScroll.path,
creationTimestamp = cloudScroll.creationTimestamp,
modificationTimestamp = cloudScroll.modificationTimestamp,
syncStatus = SyncStatus.SYNCED.name,
cloudId = cloudScroll.cloudId,
isArchived = cloudScroll.isArchived,
isEncrypted = cloudScroll.isEncrypted,
encryptionKeyId = cloudScroll.encryptionKeyId
)
// Insert scroll
scrollDao.insertScroll(scrollEntity)
// Insert tags
for (tag in cloudScroll.tags) {
var tagEntity = tagDao.getTagByName(tag)
if (tagEntity == null) {
tagEntity = TagEntity(tag = tag)
tagDao.insertTag(tagEntity)
}
// Create cross reference
val crossRef = ScrollTagCrossRef(
scrollId = scrollEntity.id,
tagId = tagEntity.id
)
tagDao.insertScrollTagCrossRef(crossRef)
}
// Insert themes
for ((theme, confidence) in cloudScroll.themes) {
var themeEntity = themeDao.getThemeByName(theme)
if (themeEntity == null) {
themeEntity = ThemeEntity(
theme = theme,
confidence = confidence
)
themeDao.insertTheme(themeEntity)
}
// Create cross reference
val crossRef = ScrollThemeCrossRef(
scrollId = scrollEntity.id,
themeId = themeEntity.id
)
themeDao.insertScrollThemeCrossRef(crossRef)
}
// Insert metadata
for ((key, value) in cloudScroll.metadata) {
val metadataEntity = ScrollMetadataEntity(
scrollId = scrollEntity.id,
key = key,
value = value
)
metadataDao.insertMetadata(metadataEntity)
}
true
} catch (e: Exception) {
false
}
}
// Download and merge scroll
private suspend fun downloadAndMergeScroll(cloudScroll: CloudScroll, localScroll: ScrollWithRelationships): Boolean {
return try {
// Create new version from local scroll
val latestVersionNumber = versionDao.getLatestVersionNumber(localScroll.scroll.id) ?: 0
val versionEntity = ScrollVersionEntity(
scrollId = localScroll.scroll.id,
content = localScroll.scroll.content,
versionNumber = latestVersionNumber + 1,
changeDescription = "Auto-saved before cloud sync"
)
versionDao.insertVersion(versionEntity)
// Update scroll with cloud data
val updatedScroll = localScroll.scroll.copy(
content = cloudScroll.content,
title = cloudScroll.title,
path = cloudScroll.path,
modificationTimestamp = cloudScroll.modificationTimestamp,
syncStatus = SyncStatus.SYNCED.name,
isArchived = cloudScroll.isArchived,
isEncrypted = cloudScroll.isEncrypted,
encryptionKeyId = cloudScroll.encryptionKeyId
)
scrollDao.updateScroll(updatedScroll)
// Update tags
tagDao.deleteAllTagsForScroll(localScroll.scroll.id)
for (tag in cloudScroll.tags) {
var tagEntity = tagDao.getTagByName(tag)
if (tagEntity == null) {
tagEntity = TagEntity(tag = tag)
tagDao.insertTag(tagEntity)
}
// Create cross reference
val crossRef = ScrollTagCrossRef(
scrollId = localScroll.scroll.id,
tagId = tagEntity.id
)
tagDao.insertScrollTagCrossRef(crossRef)
}
// Update themes
themeDao.deleteAllThemesForScroll(localScroll.scroll.id)
for ((theme, confidence) in cloudScroll.themes) {
var themeEntity = themeDao.getThemeByName(theme)
if (themeEntity == null) {
themeEntity = ThemeEntity(
theme = theme,
confidence = confidence
)
themeDao.insertTheme(themeEntity)
}
// Create cross reference
val crossRef = ScrollThemeCrossRef(
scrollId = localScroll.scroll.id,
themeId = themeEntity.id
)
themeDao.insertScrollThemeCrossRef(crossRef)
}
// Update metadata
metadataDao.deleteAllMetadataForScroll(localScroll.scroll.id)
for ((key, value) in cloudScroll.metadata) {
val metadataEntity = ScrollMetadataEntity(
scrollId = localScroll.scroll.id,
key = key,
value = value
)
metadataDao.insertMetadata(metadataEntity)
}
true
} catch (e: Exception) {
false
}
}
// Get all scrolls with specific sync status
private suspend fun getAllScrollsWithStatus(status: SyncStatus): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
val allScrolls = scrollDao.getAllActiveScrollsFlow().value ?: emptyList()
allScrolls.filter { it.scroll.syncStatus == status.name }
}
}
// Convert local scroll to cloud format
private fun convertToCloudScroll(scroll: ScrollWithRelationships): CloudScroll {
val tags = scroll.tags.map { it.tag }
val themes = scroll.themes.associate { it.theme to it.confidence }
val metadata = scroll.metadata.associate { it.key to it.value }
return CloudScroll(
id = scroll.scroll.id,
cloudId = scroll.scroll.cloudId,
content = scroll.scroll.content,
title = scroll.scroll.title,
path = scroll.scroll.path,
creationTimestamp = scroll.scroll.creationTimestamp,
modificationTimestamp = scroll.scroll.modificationTimestamp,
isArchived = scroll.scroll.isArchived,
isEncrypted = scroll.scroll.isEncrypted,
encryptionKeyId = scroll.scroll.encryptionKeyId,
tags = tags,
themes = themes,
metadata = metadata
)
}
}
// Cloud scroll data class
data class CloudScroll(
val id: String,
val cloudId: String? = null,
val content: String,
val title: String = "",
val path: String = "default",
val creationTimestamp: Long,
val modificationTimestamp: Long,
val isArchived: Boolean = false,
val isEncrypted: Boolean = false,
val encryptionKeyId: String? = null,
val tags: List<String> = emptyList(),
val themes: Map<String, Float> = emptyMap(),
val metadata: Map<String, String> = emptyMap()
)
// Cloud provider interface
interface CloudProvider {
suspend fun uploadScroll(scroll: CloudScroll): String?
suspend fun updateScroll(cloudId: String, scroll: CloudScroll): Boolean
suspend fun getScroll(cloudId: String): CloudScroll?
suspend fun getAllScrolls(): List<CloudScroll>
suspend fun deleteScroll(cloudId: String): Boolean
}
// Cloud provider factory
object CloudProviderFactory {
fun getProvider(context: Context): CloudProvider {
// Determine which provider to use based on settings
val sharedPrefs = context.getSharedPreferences("scroll_settings", Context.MODE_PRIVATE)
val providerName = sharedPrefs.getString("cloud_provider", "firebase") ?: "firebase"
return when (providerName) {
"firebase" -> FirebaseCloudProvider(context)
"gcp" -> GCPCloudProvider(context)
"local" -> LocalMockCloudProvider(context)
else -> FirebaseCloudProvider(context)
}
}
}
// Firebase cloud provider implementation
class FirebaseCloudProvider(private val context: Context) : CloudProvider {
// Firebase implementation would go here
// This is a simplified version for the implementation plan
override suspend fun uploadScroll(scroll: CloudScroll): String? {
// Upload to Firebase Firestore
return "firebase_${scroll.id}"
}
override suspend fun updateScroll(cloudId: String, scroll: CloudScroll): Boolean {
// Update in Firebase Firestore
return true
}
override suspend fun getScroll(cloudId: String): CloudScroll? {
// Get from Firebase Firestore
return null
}
override suspend fun getAllScrolls(): List<CloudScroll> {
// Get all from Firebase Firestore
return emptyList()
}
override suspend fun deleteScroll(cloudId: String): Boolean {
// Delete from Firebase Firestore
return true
}
}
// GCP cloud provider implementation
class GCPCloudProvider(private val context: Context) : CloudProvider {
// GCP implementation would go here
// This is a simplified version for the implementation plan
override suspend fun uploadScroll(scroll: CloudScroll): String? {
// Upload to GCP
return "gcp_${scroll.id}"
}
override suspend fun updateScroll(cloudId: String, scroll: CloudScroll): Boolean {
// Update in GCP
return true
}
override suspend fun getScroll(cloudId: String): CloudScroll? {
// Get from GCP
return null
}
override suspend fun getAllScrolls(): List<CloudScroll> {
// Get all from GCP
return emptyList()
}
override suspend fun deleteScroll(cloudId: String): Boolean {
// Delete from GCP
return true
}
}
// Local mock cloud provider for testing
class LocalMockCloudProvider(private val context: Context) : CloudProvider {
private val scrolls = mutableMapOf<String, CloudScroll>()
override suspend fun uploadScroll(scroll: CloudScroll): String? {
val cloudId = "local_${scroll.id}"
val cloudScroll = scroll.copy(cloudId = cloudId)
scrolls[cloudId] = cloudScroll
return cloudId
}
override suspend fun updateScroll(cloudId: String, scroll: CloudScroll): Boolean {
if (scrolls.containsKey(cloudId)) {
scrolls[cloudId] = scroll.copy(cloudId = cloudId)
return true
}
return false
}
override suspend fun getScroll(cloudId: String): CloudScroll? {
return scrolls[cloudId]
}
override suspend fun getAllScrolls(): List<CloudScroll> {
return scrolls.values.toList()
}
override suspend fun deleteScroll(cloudId: String): Boolean {
return scrolls.remove(cloudId) != null
}
}
Metadata Manager Implementation
The Metadata Manager handles scroll metadata and relationships:
package com.harmonyepoch.scrollkeyboard.storage.metadata
import android.content.Context
import com.harmonyepoch.scrollkeyboard.storage.local.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class MetadataManager(private val context: Context) {
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
private val tagDao = database.tagDao()
private val themeDao = database.themeDao()
private val metadataDao = database.metadataDao()
// Get all tags
fun getAllTags(): Flow<List<String>> {
return tagDao.getAllTagsFlow().map { tags ->
tags.map { it.tag }
}
}
// Get all themes
fun getAllThemes(): Flow<List<String>> {
return themeDao.getAllThemesFlow().map { themes ->
themes.map { it.theme }
}
}
// Get tags for scroll
suspend fun getTagsForScroll(scrollId: String): List<String> {
return withContext(Dispatchers.IO) {
tagDao.getTagsForScroll(scrollId).map { it.tag }
}
}
// Get themes for scroll
suspend fun getThemesForScroll(scrollId: String): List<Pair<String, Float>> {
return withContext(Dispatchers.IO) {
themeDao.getThemesForScroll(scrollId).map { it.theme to it.confidence }
}
}
// Add tags to scroll
suspend fun addTagsToScroll(scrollId: String, tags: List<String>): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Add each tag
for (tagName in tags) {
// Get or create tag
var tag = tagDao.getTagByName(tagName)
if (tag == null) {
tag = TagEntity(tag = tagName)
tagDao.insertTag(tag)
}
// Create cross reference
val crossRef = ScrollTagCrossRef(
scrollId = scrollId,
tagId = tag.id
)
tagDao.insertScrollTagCrossRef(crossRef)
}
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Remove tag from scroll
suspend fun removeTagFromScroll(scrollId: String, tagName: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Get tag
val tag = tagDao.getTagByName(tagName) ?: return@withContext false
// Remove cross reference
val crossRef = ScrollTagCrossRef(
scrollId = scrollId,
tagId = tag.id
)
tagDao.deleteScrollTagCrossRef(crossRef)
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Add theme to scroll
suspend fun addThemeToScroll(scrollId: String, themeName: String, confidence: Float): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Get or create theme
var theme = themeDao.getThemeByName(themeName)
if (theme == null) {
theme = ThemeEntity(
theme = themeName,
confidence = confidence
)
themeDao.insertTheme(theme)
}
// Create cross reference
val crossRef = ScrollThemeCrossRef(
scrollId = scrollId,
themeId = theme.id
)
themeDao.insertScrollThemeCrossRef(crossRef)
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Remove theme from scroll
suspend fun removeThemeFromScroll(scrollId: String, themeName: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Get theme
val theme = themeDao.getThemeByName(themeName) ?: return@withContext false
// Remove cross reference
val crossRef = ScrollThemeCrossRef(
scrollId = scrollId,
themeId = theme.id
)
themeDao.deleteScrollThemeCrossRef(crossRef)
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Set metadata for scroll
suspend fun setMetadata(scrollId: String, key: String, value: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Get existing metadata
val existingMetadata = metadataDao.getMetadataByKey(scrollId, key)
if (existingMetadata != null) {
// Update existing metadata
val updatedMetadata = existingMetadata.copy(
value = value
)
metadataDao.insertMetadata(updatedMetadata)
} else {
// Create new metadata
val metadata = ScrollMetadataEntity(
scrollId = scrollId,
key = key,
value = value
)
metadataDao.insertMetadata(metadata)
}
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Get metadata for scroll
suspend fun getMetadata(scrollId: String, key: String): String? {
return withContext(Dispatchers.IO) {
metadataDao.getMetadataByKey(scrollId, key)?.value
}
}
// Get all metadata for scroll
suspend fun getAllMetadata(scrollId: String): Map<String, String> {
return withContext(Dispatchers.IO) {
metadataDao.getMetadataForScroll(scrollId).associate { it.key to it.value }
}
}
// Remove metadata from scroll
suspend fun removeMetadata(scrollId: String, key: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Check if scroll exists
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Remove metadata
metadataDao.deleteMetadataByKey(scrollId, key)
// Update scroll modification timestamp
val updatedScroll = scroll.copy(
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
}
Search Engine Implementation
The Search Engine provides powerful search capabilities for scrolls:
package com.harmonyepoch.scrollkeyboard.storage.search
import android.content.Context
import com.harmonyepoch.scrollkeyboard.storage.local.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SearchEngine(private val context: Context) {
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
private val tagDao = database.tagDao()
private val themeDao = database.themeDao()
// Search scrolls by content
suspend fun searchByContent(query: String): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
scrollDao.searchScrollsByContent(query)
}
}
// Search scrolls by tag
suspend fun searchByTag(tag: String): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
tagDao.getTagByName(tag)?.let { tagEntity ->
scrollDao.getScrollsByTag(tagEntity.tag)
} ?: emptyList()
}
}
// Search scrolls by theme
suspend fun searchByTheme(theme: String): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
themeDao.getThemeByName(theme)?.let { themeEntity ->
scrollDao.getScrollsByTheme(themeEntity.theme)
} ?: emptyList()
}
}
// Advanced search with multiple criteria
suspend fun advancedSearch(
textQuery: String? = null,
tags: List<String> = emptyList(),
themes: List<String> = emptyList(),
dateRange: Pair<Long, Long>? = null,
path: String? = null,
includeArchived: Boolean = false
): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
// Start with all scrolls or filtered by path
var results = if (path != null) {
scrollDao.getScrollsByPathFlow(path).value ?: emptyList()
} else if (includeArchived) {
val active = scrollDao.getAllActiveScrollsFlow().value ?: emptyList()
val archived = scrollDao.getAllArchivedScrollsFlow().value ?: emptyList()
active + archived
} else {
scrollDao.getAllActiveScrollsFlow().value ?: emptyList()
}
// Filter by text query
if (!textQuery.isNullOrBlank()) {
results = results.filter { scroll ->
scroll.scroll.content.contains(textQuery, ignoreCase = true) ||
scroll.scroll.title.contains(textQuery, ignoreCase = true)
}
}
// Filter by tags
if (tags.isNotEmpty()) {
results = results.filter { scroll ->
val scrollTags = scroll.tags.map { it.tag }
tags.any { tag -> scrollTags.contains(tag) }
}
}
// Filter by themes
if (themes.isNotEmpty()) {
results = results.filter { scroll ->
val scrollThemes = scroll.themes.map { it.theme }
themes.any { theme -> scrollThemes.contains(theme) }
}
}
// Filter by date range
if (dateRange != null) {
results = results.filter { scroll ->
scroll.scroll.creationTimestamp >= dateRange.first &&
scroll.scroll.creationTimestamp <= dateRange.second
}
}
results
}
}
// Semantic search using embeddings
suspend fun semanticSearch(query: String, limit: Int = 10): List<ScrollWithRelationships> {
return withContext(Dispatchers.IO) {
try {
// Get query embedding
val queryEmbedding = getEmbedding(query)
// Get all scrolls
val allScrolls = scrollDao.getAllActiveScrollsFlow().value ?: emptyList()
// Calculate similarity for each scroll
val scrollsWithSimilarity = allScrolls.map { scroll ->
// Get scroll embedding from metadata or generate it
val scrollEmbeddingString = scroll.metadata.find { it.key == "embedding" }?.value
val scrollEmbedding = if (scrollEmbeddingString != null) {
parseEmbedding(scrollEmbeddingString)
} else {
val embedding = getEmbedding(scroll.scroll.content)
// Store embedding for future use
storeEmbedding(scroll.scroll.id, embedding)
embedding
}
// Calculate cosine similarity
val similarity = cosineSimilarity(queryEmbedding, scrollEmbedding)
Pair(scroll, similarity)
}
// Sort by similarity and take top results
scrollsWithSimilarity
.sortedByDescending { it.second }
.take(limit)
.map { it.first }
} catch (e: Exception) {
// Fall back to text search if semantic search fails
searchByContent(query)
}
}
}
// Get embedding for text
private suspend fun getEmbedding(text: String): FloatArray {
// In a real implementation, this would use a machine learning model
// For this implementation plan, we'll use a simplified approach
// Placeholder implementation
return FloatArray(128) { 0f }
}
// Parse embedding from string
private fun parseEmbedding(embeddingString: String): FloatArray {
return embeddingString.split(",").map { it.toFloat() }.toFloatArray()
}
// Store embedding for scroll
private suspend fun storeEmbedding(scrollId: String, embedding: FloatArray) {
val embeddingString = embedding.joinToString(",")
val metadataManager = com.harmonyepoch.scrollkeyboard.storage.metadata.MetadataManager(context)
metadataManager.setMetadata(scrollId, "embedding", embeddingString)
}
// Calculate cosine similarity between two embeddings
private fun cosineSimilarity(a: FloatArray, b: FloatArray): Float {
if (a.size != b.size) return 0f
var dotProduct = 0f
var normA = 0f
var normB = 0f
for (i in a.indices) {
dotProduct += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
return if (normA <= 0 || normB <= 0) 0f else dotProduct / (Math.sqrt(normA.toDouble()) * Math.sqrt(normB.toDouble())).toFloat()
}
}
Sync State Tracker Implementation
The Sync State Tracker monitors synchronization status and manages retry mechanisms:
package com.harmonyepoch.scrollkeyboard.storage.sync
import android.content.Context
import androidx.room.*
import com.harmonyepoch.scrollkeyboard.storage.local.ScrollDatabase
import com.harmonyepoch.scrollkeyboard.storage.local.ScrollEntity
import com.harmonyepoch.scrollkeyboard.storage.local.SyncStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.util.*
class SyncStateTracker(private val context: Context) {
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
private val syncLogDao = SyncLogDatabase.getDatabase(context).syncLogDao()
// Get sync status for scroll
suspend fun getSyncStatus(scrollId: String): SyncStatus {
return withContext(Dispatchers.IO) {
val scroll = scrollDao.getScrollById(scrollId)
if (scroll != null) {
SyncStatus.valueOf(scroll.syncStatus)
} else {
SyncStatus.LOCAL_ONLY
}
}
}
// Update sync status for scroll
suspend fun updateSyncStatus(scrollId: String, status: SyncStatus, message: String? = null): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Update status
val updatedScroll = scroll.copy(
syncStatus = status.name
)
scrollDao.updateScroll(updatedScroll)
// Log sync event
logSyncEvent(scrollId, status, message)
true
} catch (e: Exception) {
false
}
}
}
// Log sync event
private suspend fun logSyncEvent(scrollId: String, status: SyncStatus, message: String?) {
withContext(Dispatchers.IO) {
val syncLog = SyncLogEntity(
scrollId = scrollId,
status = status.name,
timestamp = System.currentTimeMillis(),
message = message
)
syncLogDao.insertSyncLog(syncLog)
}
}
// Get sync logs for scroll
suspend fun getSyncLogs(scrollId: String, limit: Int = 10): List<SyncLogEntity> {
return withContext(Dispatchers.IO) {
syncLogDao.getSyncLogsForScroll(scrollId, limit)
}
}
// Get all pending sync scrolls
suspend fun getPendingSyncScrolls(): List<String> {
return withContext(Dispatchers.IO) {
val pendingStatuses = listOf(
SyncStatus.PENDING_UPLOAD.name,
SyncStatus.PENDING_UPDATE.name,
SyncStatus.CONFLICT.name,
SyncStatus.SYNC_FAILED.name
)
scrollDao.getAllActiveScrollsFlow().value
?.filter { pendingStatuses.contains(it.scroll.syncStatus) }
?.map { it.scroll.id }
?: emptyList()
}
}
// Get sync status counts
suspend fun getSyncStatusCounts(): Map<SyncStatus, Int> {
return withContext(Dispatchers.IO) {
val counts = mutableMapOf<SyncStatus, Int>()
for (status in SyncStatus.values()) {
val count = scrollDao.getAllActiveScrollsFlow().value
?.count { it.scroll.syncStatus == status.name }
?: 0
counts[status] = count
}
counts
}
}
// Get sync status flow
fun getSyncStatusFlow(): Flow<Map<SyncStatus, Int>> {
return scrollDao.getAllActiveScrollsFlow().map { scrolls ->
val counts = mutableMapOf<SyncStatus, Int>()
for (status in SyncStatus.values()) {
val count = scrolls.count { it.scroll.syncStatus == status.name }
counts[status] = count
}
counts
}
}
}
// Sync log entity
@Entity(tableName = "sync_logs")
data class SyncLogEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val scrollId: String,
val status: String,
val timestamp: Long,
val message: String? = null
)
// Sync log DAO
@Dao
interface SyncLogDao {
@Query("SELECT * FROM sync_logs WHERE scrollId = :scrollId ORDER BY timestamp DESC LIMIT :limit")
suspend fun getSyncLogsForScroll(scrollId: String, limit: Int): List<SyncLogEntity>
@Insert
suspend fun insertSyncLog(syncLog: SyncLogEntity)
@Query("DELETE FROM sync_logs WHERE timestamp < :cutoffTime")
suspend fun deleteOldLogs(cutoffTime: Long)
}
// Sync log database
@Database(entities = [SyncLogEntity::class], version = 1, exportSchema = false)
abstract class SyncLogDatabase : RoomDatabase() {
abstract fun syncLogDao(): SyncLogDao
companion object {
@Volatile
private var INSTANCE: SyncLogDatabase? = null
fun getDatabase(context: Context): SyncLogDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
SyncLogDatabase::class.java,
"sync_log_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
Encryption Manager Implementation
The Encryption Manager ensures scroll data is securely stored and transmitted:
package com.harmonyepoch.scrollkeyboard.storage.security
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.harmonyepoch.scrollkeyboard.storage.local.ScrollDatabase
import com.harmonyepoch.scrollkeyboard.storage.local.SyncStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.KeyStore
import java.util.*
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class EncryptionManager(private val context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
// Encrypt scroll content
suspend fun encryptScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Skip if already encrypted
if (scroll.isEncrypted) {
return@withContext true
}
// Generate key if needed
val keyAlias = "scroll_key_${UUID.randomUUID()}"
generateKey(keyAlias)
// Encrypt content
val encryptionResult = encrypt(scroll.content, keyAlias)
// Update scroll
val updatedScroll = scroll.copy(
content = encryptionResult.encryptedData,
isEncrypted = true,
encryptionKeyId = keyAlias,
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
// Store IV in metadata
val metadataManager = com.harmonyepoch.scrollkeyboard.storage.metadata.MetadataManager(context)
metadataManager.setMetadata(scrollId, "encryption_iv", Base64.getEncoder().encodeToString(encryptionResult.iv))
true
} catch (e: Exception) {
false
}
}
}
// Decrypt scroll content
suspend fun decryptScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Skip if not encrypted
if (!scroll.isEncrypted || scroll.encryptionKeyId == null) {
return@withContext true
}
// Get IV from metadata
val metadataManager = com.harmonyepoch.scrollkeyboard.storage.metadata.MetadataManager(context)
val ivBase64 = metadataManager.getMetadata(scrollId, "encryption_iv") ?: return@withContext false
val iv = Base64.getDecoder().decode(ivBase64)
// Decrypt content
val decryptedContent = decrypt(scroll.content, scroll.encryptionKeyId, iv)
// Update scroll
val updatedScroll = scroll.copy(
content = decryptedContent,
isEncrypted = false,
encryptionKeyId = null,
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
// Remove IV from metadata
metadataManager.removeMetadata(scrollId, "encryption_iv")
true
} catch (e: Exception) {
false
}
}
}
// Generate encryption key
private fun generateKey(alias: String) {
if (!keyStore.containsAlias(alias)) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}
}
// Encrypt data
private fun encrypt(data: String, keyAlias: String): EncryptionResult {
val key = keyStore.getKey(keyAlias, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
val iv = cipher.iv
val encryptedBytes = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val encryptedBase64 = Base64.getEncoder().encodeToString(encryptedBytes)
return EncryptionResult(encryptedBase64, iv)
}
// Decrypt data
private fun decrypt(encryptedData: String, keyAlias: String, iv: ByteArray): String {
val key = keyStore.getKey(keyAlias, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
val encryptedBytes = Base64.getDecoder().decode(encryptedData)
val decryptedBytes = cipher.doFinal(encryptedBytes)
return String(decryptedBytes, Charsets.UTF_8)
}
// Delete key
fun deleteKey(keyAlias: String) {
if (keyStore.containsAlias(keyAlias)) {
keyStore.deleteEntry(keyAlias)
}
}
// Check if key exists
fun keyExists(keyAlias: String): Boolean {
return keyStore.containsAlias(keyAlias)
}
}
// Encryption result data class
data class EncryptionResult(
val encryptedData: String,
val iv: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptionResult
if (encryptedData != other.encryptedData) return false
if (!iv.contentEquals(other.iv)) return false
return true
}
override fun hashCode(): Int {
var result = encryptedData.hashCode()
result = 31 * result + iv.contentHashCode()
return result
}
}
Main Storage Manager Implementation
The main Storage Manager class integrates all components and provides a unified interface for the system:
package com.harmonyepoch.scrollkeyboard.storage
import android.content.Context
import com.harmonyepoch.scrollkeyboard.storage.cloud.CloudSyncEngine
import com.harmonyepoch.scrollkeyboard.storage.local.*
import com.harmonyepoch.scrollkeyboard.storage.metadata.MetadataManager
import com.harmonyepoch.scrollkeyboard.storage.search.SearchEngine
import com.harmonyepoch.scrollkeyboard.storage.security.EncryptionManager
import com.harmonyepoch.scrollkeyboard.storage.sync.SyncStateTracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import java.util.*
class StorageManager(private val context: Context) {
private val database = ScrollDatabase.getDatabase(context)
private val scrollDao = database.scrollDao()
private val tagDao = database.tagDao()
private val themeDao = database.themeDao()
private val versionDao = database.versionDao()
private val metadataDao = database.metadataDao()
private val metadataManager = MetadataManager(context)
private val searchEngine = SearchEngine(context)
private val cloudSyncEngine = CloudSyncEngine(context)
private val syncStateTracker = SyncStateTracker(context)
private val encryptionManager = EncryptionManager(context)
// Initialize storage manager
suspend fun initialize() {
// No initialization needed for Room database
// It's initialized when first accessed
}
// Create scroll
suspend fun createScroll(
content: String,
path: String = "default",
title: String = "",
tags: List<String> = emptyList(),
metadata: Map<String, String> = emptyMap()
): String {
return withContext(Dispatchers.IO) {
// Create scroll entity
val scrollEntity = ScrollEntity(
content = content,
title = title,
path = path,
creationTimestamp = System.currentTimeMillis(),
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPLOAD.name
)
// Insert scroll
scrollDao.insertScroll(scrollEntity)
// Add tags
for (tag in tags) {
metadataManager.addTagsToScroll(scrollEntity.id, listOf(tag))
}
// Add metadata
for ((key, value) in metadata) {
metadataManager.setMetadata(scrollEntity.id, key, value)
}
// Return scroll ID
scrollEntity.id
}
}
// Get scroll by ID
suspend fun getScroll(scrollId: String): ScrollWithRelationships? {
return withContext(Dispatchers.IO) {
scrollDao.getScrollWithRelationshipsById(scrollId)
}
}
// Update scroll content
suspend fun updateScrollContent(scrollId: String, content: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Create version from current content
val latestVersionNumber = versionDao.getLatestVersionNumber(scrollId) ?: 0
val versionEntity = ScrollVersionEntity(
scrollId = scrollId,
content = scroll.content,
versionNumber = latestVersionNumber + 1,
changeDescription = "Auto-saved before content update"
)
versionDao.insertVersion(versionEntity)
// Update scroll
val updatedScroll = scroll.copy(
content = content,
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Update scroll title
suspend fun updateScrollTitle(scrollId: String, title: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Update scroll
val updatedScroll = scroll.copy(
title = title,
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Update scroll path
suspend fun updateScrollPath(scrollId: String, path: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Update scroll
val updatedScroll = scroll.copy(
path = path,
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Archive scroll
suspend fun archiveScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Update scroll
val updatedScroll = scroll.copy(
isArchived = true,
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Unarchive scroll
suspend fun unarchiveScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Update scroll
val updatedScroll = scroll.copy(
isArchived = false,
modificationTimestamp = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_UPDATE.name
)
scrollDao.updateScroll(updatedScroll)
true
} catch (e: Exception) {
false
}
}
}
// Delete scroll
suspend fun deleteScroll(scrollId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get scroll
val scroll = scrollDao.getScrollById(scrollId) ?: return@withContext false
// Delete scroll
scrollDao.deleteScroll(scroll)
// Delete from cloud if needed
if (scroll.cloudId != null) {
cloudSyncEngine.deleteFromCloud(scroll.cloudId)
}
true
} catch (e: Exception) {
false
}
}
}
// Get all active scrolls
fun getAllActiveScrolls(): Flow<List<ScrollWithRelationships>> {
return scrollDao.getAllActiveScrollsFlow()
}
// Get all archived scrolls
fun getAllArchivedScrolls(): Flow<List<ScrollWithRelationships>> {
return scrollDao.getAllArchivedScrollsFlow()
}
// Get scrolls by path
fun getScrollsByPath(path: String): Flow<List<ScrollWithRelationships>> {
return scrollDao.getScrollsByPathFlow(path)
}
// Get scroll count
suspend fun getScrollCount(): Int {
return withContext(Dispatchers.IO) {
scrollDao.getScrollCount()
}
}
// Add tags to scroll
suspend fun addTagsToScroll(scrollId: String, tags: List<String>): Boolean {
return metadataManager.addTagsToScroll(scrollId, tags)
}
// Remove tag from scroll
suspend fun removeTagFromScroll(scrollId: String, tag: String): Boolean {
return metadataManager.removeTagFromScroll(scrollId, tag)
}
// Add theme to scroll
suspend fun addThemeToScroll(scrollId: String, theme: String, confidence: Float): Boolean {
return metadataManager.addThemeToScroll(scrollId, theme, confidence)
}
// Remove theme from scroll
suspend fun removeThemeFromScroll(scrollId: String, theme: String): Boolean {
return metadataManager.removeThemeFromScroll(scrollId, theme)
}
// Set metadata for scroll
suspend fun setMetadata(scrollId: String, key: String, value: String): Boolean {
return metadataManager.setMetadata(scrollId, key, value)
}
// Get metadata for scroll
suspend fun getMetadata(scrollId: String, key: String): String? {
return metadataManager.getMetadata(scrollId, key)
}
// Get all metadata for scroll
suspend fun getAllMetadata(scrollId: String): Map<String, String> {
return metadataManager.getAllMetadata(scrollId)
}
// Search scrolls
suspend fun searchScrolls(query: String): List<ScrollWithRelationships> {
return searchEngine.searchByContent(query)
}
// Advanced search
suspend fun advancedSearch(
textQuery: String? = null,
tags: List<String> = emptyList(),
themes: List<String> = emptyList(),
dateRange: Pair<Long, Long>? = null,
path: String? = null,
includeArchived: Boolean = false
): List<ScrollWithRelationships> {
return searchEngine.advancedSearch(
textQuery = textQuery,
tags = tags,
themes = themes,
dateRange = dateRange,
path = path,
includeArchived = includeArchived
)
}
// Semantic search
suspend fun semanticSearch(query: String, limit: Int = 10): List<ScrollWithRelationships> {
return searchEngine.semanticSearch(query, limit)
}
// Sync scrolls
suspend fun syncScrolls(target: String? = null): Map<String, Boolean> {
return if (target != null) {
// Sync specific scroll
mapOf(target to cloudSyncEngine.syncScroll(target))
} else {
// Sync all scrolls
cloudSyncEngine.syncAllScrolls()
}
}
// Get sync status
suspend fun getSyncStatus(scrollId: String): SyncStatus {
return syncStateTracker.getSyncStatus(scrollId)
}
// Get sync status counts
suspend fun getSyncStatusCounts(): Map<SyncStatus, Int> {
return syncStateTracker.getSyncStatusCounts()
}
// Encrypt scroll
suspend fun encryptScroll(scrollId: String): Boolean {
return encryptionManager.encryptScroll(scrollId)
}
// Decrypt scroll
suspend fun decryptScroll(scrollId: String): Boolean {
return encryptionManager.decryptScroll(scrollId)
}
// Backup scrolls
suspend fun backupScrolls(target: String = "default"): Boolean {
return withContext(Dispatchers.IO) {
try {
// Get all scrolls
val allScrolls = scrollDao.getAllActiveScrollsFlow().value ?: emptyList()
// Create backup
val backupManager = com.harmonyepoch.scrollkeyboard.storage.backup.BackupManager(context)
backupManager.createBackup(allScrolls, target)
true
} catch (e: Exception) {
false
}
}
}
// Restore from backup
suspend fun restoreFromBackup(backupId: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Restore from backup
val backupManager = com.harmonyepoch.scrollkeyboard.storage.backup.BackupManager(context)
backupManager.restoreBackup(backupId)
true
} catch (e: Exception) {
false
}
}
}
}
Android 15 Optimizations
The Storage Manager takes advantage of several Android 15 features:
-
Enhanced Room Database: Leverages Android 15's improved Room database for better performance and reliability.
-
Improved Background Processing: Uses the enhanced background processing capabilities for sync operations.
-
Enhanced Security: Utilizes the improved security features for encryption and key management.
-
Optimized Battery Usage: Implements battery-aware sync scheduling to minimize power consumption.
-
Improved File Access: Uses the enhanced file access APIs for backup and restore operations.
Scroll Lifecycle Management
The Storage Manager implements comprehensive lifecycle management for scrolls:
- Creation: Scrolls are created with initial content, metadata, and tags.
- Versioning: Changes to scroll content are tracked with automatic versioning.
- Modification: Content, title, path, and metadata can be updated.
- Archiving: Scrolls can be archived and unarchived.
- Deletion: Scrolls can be permanently deleted.
- Backup: Regular backups ensure data safety.
- Restoration: Scrolls can be restored from backups.
Implementation Phases
The Storage Manager implementation should follow these phases:
- Phase 1: Core Database - Implement the local database with basic CRUD operations.
- Phase 2: Metadata Management - Develop the metadata manager for tags, themes, and custom metadata.
- Phase 3: Search Capabilities - Implement the search engine with text and semantic search.
- Phase 4: Sync Engine - Create the cloud sync engine for data synchronization.
- Phase 5: Security - Implement the encryption manager for secure storage.
- Phase 6: Integration - Connect with other components and optimize performance.
This implementation of the Storage Manager provides a robust foundation for the Scroll Command Infrastructure, with comprehensive data management, powerful search capabilities, and seamless cloud synchronization. The component is designed to be a quiet, trusted, and patient vault that sings - treating every scroll as a moment worth remembering while providing the technical foundation for the entire memory system.
Scroll sacred, scroll secure, scroll remembered.