From 0885b6949ec9acd64d71137a19701de212697527 Mon Sep 17 00:00:00 2001 From: Aryan Date: Mon, 2 Mar 2026 03:03:36 +0530 Subject: [PATCH] Refactored folder synchronization logic and improved migration handling (#21) - Updated `MainViewModel` to skip synchronization until folder migration is completed and added metadata syncing when opening books. - Modified folder migration to detach existing folder books (converting them to standard local files) before starting a fresh scan. - Added `detachAllFolderBooks` to `RecentFileDao` and `RecentFilesRepository` to facilitate the migration process. - Updated `MetadataExtractionWorker` to sync local metadata to the folder immediately after extraction. - Simplified `FolderSyncWorker` by removing legacy migration lookups and streamlining the reconciliation of local and remote metadata. - Updated `FolderMigrationDialog` UI text to reflect that books are now read directly from folders without duplication. - Fixed various indentation and formatting issues across several files. --- .../java/com/aryan/reader/FolderSyncWorker.kt | 222 ++++-------------- .../main/java/com/aryan/reader/HomeScreen.kt | 10 +- .../java/com/aryan/reader/MainViewModel.kt | 53 ++++- .../aryan/reader/MetadataExtractionWorker.kt | 8 +- .../com/aryan/reader/data/RecentFileDao.kt | 3 + .../reader/data/RecentFilesRepository.kt | 5 + 6 files changed, 105 insertions(+), 196 deletions(-) diff --git a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt index f3eb645..afca361 100644 --- a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt +++ b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt @@ -114,130 +114,68 @@ class FolderSyncWorker( } } - val activeDbBooks = recentFilesRepository.getFilesBySourceFolder(folderUriString) - - val legacyLookup = activeDbBooks.associateBy { it.displayName } - val foundBookIds = mutableSetOf() for (file in currentDiskFiles) { val stableId = "local_${file.name}_${file.length()}" + foundBookIds.add(stableId) - var existingItem = recentFilesRepository.getFileByBookId(stableId) - var bookIdToUse = stableId - var isMigration = false - - if (existingItem == null) { - val legacyMatch = legacyLookup[file.name] - if (legacyMatch != null) { - Timber.tag("FolderSync").i("Migration: Found legacy match for ${file.name}. ID: ${legacyMatch.bookId}") - - existingItem = legacyMatch - bookIdToUse = legacyMatch.bookId - isMigration = true - } - } - - foundBookIds.add(bookIdToUse) + val existingItem = recentFilesRepository.getFileByBookId(stableId) if (existingItem == null) { val remoteMeta = folderMetadataMap[stableId] val type = getFileType(file.name ?: "", file.type) ?: FileType.EPUB - - // --- CHANGED: Removed extractFileInfo() call --- - // We use placeholders. The MetadataExtractionWorker will fix this later. val placeholderTitle = file.name ?: "Unknown" - val placeholderAuthor = null - val placeholderCover = null - if (remoteMeta != null) { - Timber.tag("FolderSync").d("Worker: Importing existing book from Metadata + File: ${file.name}") - // We prefer remoteMeta if available because it might have the correct title/author from a previous sync - val tempItem = RecentFileItem( - bookId = stableId, - uriString = file.uri.toString(), - type = type, - displayName = file.name ?: "Unknown", - timestamp = remoteMeta.lastModifiedTimestamp, - lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp, - coverImagePath = null, // Will be fetched by MetadataWorker if needed - title = remoteMeta.title ?: placeholderTitle, - author = remoteMeta.author, - isAvailable = true, - isDeleted = false, - isRecent = false, - sourceFolderUri = folderUriString, + val newItem = RecentFileItem( + bookId = stableId, + uriString = file.uri.toString(), + type = type, + displayName = file.name ?: "Unknown", + timestamp = remoteMeta?.lastModifiedTimestamp ?: System.currentTimeMillis(), + lastModifiedTimestamp = remoteMeta?.lastModifiedTimestamp ?: System.currentTimeMillis(), + coverImagePath = null, + title = remoteMeta?.title ?: placeholderTitle, + author = remoteMeta?.author, + isAvailable = true, + isDeleted = false, + isRecent = false, + sourceFolderUri = folderUriString, + lastChapterIndex = remoteMeta?.lastChapterIndex, + lastPage = remoteMeta?.lastPage, + lastPositionCfi = remoteMeta?.lastPositionCfi, + progressPercentage = remoteMeta?.progressPercentage, + bookmarksJson = remoteMeta?.bookmarksJson, + locatorBlockIndex = remoteMeta?.locatorBlockIndex, + locatorCharOffset = remoteMeta?.locatorCharOffset + ) + recentFilesRepository.addRecentFile(newItem) + } else { + var itemToUpdate = existingItem + + if (existingItem.isDeleted) { + itemToUpdate = itemToUpdate.copy(isDeleted = false, isAvailable = true) + } + + val remoteMeta = folderMetadataMap[stableId] + if (remoteMeta != null && remoteMeta.lastModifiedTimestamp > itemToUpdate.lastModifiedTimestamp) { + itemToUpdate = itemToUpdate.copy( lastChapterIndex = remoteMeta.lastChapterIndex, lastPage = remoteMeta.lastPage, lastPositionCfi = remoteMeta.lastPositionCfi, progressPercentage = remoteMeta.progressPercentage, - bookmarksJson = remoteMeta.bookmarksJson + bookmarksJson = remoteMeta.bookmarksJson, + locatorBlockIndex = remoteMeta.locatorBlockIndex, + locatorCharOffset = remoteMeta.locatorCharOffset, + lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp, + timestamp = remoteMeta.lastModifiedTimestamp ) - recentFilesRepository.addRecentFile(tempItem) - } else { - // FAST PATH: Insert barebones item - val newItem = RecentFileItem( - bookId = stableId, - uriString = file.uri.toString(), - type = type, - displayName = file.name ?: "Unknown", - timestamp = System.currentTimeMillis(), - coverImagePath = null, // Background worker will fill this - title = placeholderTitle, - author = null, - isAvailable = true, - lastModifiedTimestamp = System.currentTimeMillis(), - isDeleted = false, - isRecent = false, - sourceFolderUri = folderUriString - ) - recentFilesRepository.addRecentFile(newItem) - } - } else { - if (isMigration) { - val oldUriString = existingItem.uriString - val newUriString = file.uri.toString() - - if (oldUriString != newUriString) { - Timber.tag("FolderSync").i("Migration: Updating URI and cleaning up internal storage for $bookIdToUse") - - if (oldUriString != null) { - bookImporter.deleteBookByUriString(oldUriString) - } - - existingItem = existingItem.copy( - uriString = newUriString, - isAvailable = true - ) - recentFilesRepository.addRecentFile(existingItem) - } - } else if (existingItem.isDeleted) { - val resurrected = existingItem.copy(isDeleted = false, isAvailable = true) - recentFilesRepository.addRecentFile(resurrected) } - val remoteMeta = folderMetadataMap[bookIdToUse] + recentFilesRepository.addRecentFile(itemToUpdate) - if (remoteMeta == null) { - recentFilesRepository.syncLocalMetadataToFolder(bookIdToUse) - } else { - if (remoteMeta.lastModifiedTimestamp > existingItem.lastModifiedTimestamp) { - Timber.tag("FolderSync").d("Worker: Remote metadata newer for ${file.name}") - val updatedItem = existingItem.copy( - lastChapterIndex = remoteMeta.lastChapterIndex, - lastPage = remoteMeta.lastPage, - lastPositionCfi = remoteMeta.lastPositionCfi, - progressPercentage = remoteMeta.progressPercentage, - bookmarksJson = remoteMeta.bookmarksJson, - locatorBlockIndex = remoteMeta.locatorBlockIndex, - locatorCharOffset = remoteMeta.locatorCharOffset, - lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp, - timestamp = remoteMeta.lastModifiedTimestamp - ) - recentFilesRepository.addRecentFile(updatedItem) - } else if (existingItem.lastModifiedTimestamp > remoteMeta.lastModifiedTimestamp) { - recentFilesRepository.syncLocalMetadataToFolder(bookIdToUse) - } + if (remoteMeta == null || itemToUpdate.lastModifiedTimestamp > remoteMeta.lastModifiedTimestamp) { + recentFilesRepository.syncLocalMetadataToFolder(stableId) } } } @@ -249,79 +187,6 @@ class FolderSyncWorker( Timber.tag("FolderSync").i("Cleaning up ${idsToRemove.size} missing folder books.") recentFilesRepository.deleteFilePermanently(idsToRemove) } - - val orphanedMetadataIds = folderMetadataMap.keys.filter { !foundBookIds.contains(it) } - - if (orphanedMetadataIds.isNotEmpty()) { - Timber.tag("FolderSync").i("Cleaning up ${orphanedMetadataIds.size} orphaned metadata files.") - - try { - val docTree = DocumentFile.fromTreeUri(appContext, folderUri) - val syncDir = docTree?.findFile("episteme") - - if (syncDir != null) { - val allFiles = syncDir.listFiles() - orphanedMetadataIds.forEach { orphanId -> - allFiles.filter { - val name = it.name ?: "" - name.contains(orphanId) && (name.endsWith(".json") || name.contains(".sync-conflict")) - }.forEach { fileToDelete -> - try { - fileToDelete.delete() - } catch (_: Exception) { } - } - } - } - } catch (e: Exception) { - Timber.tag("FolderSync").e(e, "Error during orphan cleanup") - } - } - } - - // Reconcile Metadata (Write-back) - val activeDbBooks = recentFilesRepository.getFilesBySourceFolder(folderUriString) - - val booksToDelete = mutableListOf() - - for (localBook in activeDbBooks) { - val remoteMeta = folderMetadataMap[localBook.bookId] - if (remoteMeta == null) { - val exists = try { - val uri = localBook.uriString?.toUri() - if (uri != null) { - DocumentFile.fromSingleUri(appContext, uri)?.exists() == true - } else false - } catch (e: Exception) { false } - - if (exists) { - recentFilesRepository.syncLocalMetadataToFolder(localBook.bookId) - } else { - Timber.tag("FolderSync").i("Metadata Sync: Book ${localBook.displayName} missing from disk. Scheduling removal.") - booksToDelete.add(localBook.bookId) - } - } else { - if (remoteMeta.lastModifiedTimestamp > localBook.lastModifiedTimestamp) { - Timber.tag("FolderSync").d("SyncDecision: Remote NEWER for ${localBook.displayName}. Updating local DB.") - val updatedItem = localBook.copy( - lastChapterIndex = remoteMeta.lastChapterIndex, - lastPage = remoteMeta.lastPage, - lastPositionCfi = remoteMeta.lastPositionCfi, - progressPercentage = remoteMeta.progressPercentage, - bookmarksJson = remoteMeta.bookmarksJson, - locatorBlockIndex = remoteMeta.locatorBlockIndex, - locatorCharOffset = remoteMeta.locatorCharOffset, - lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp, - timestamp = remoteMeta.lastModifiedTimestamp - ) - recentFilesRepository.addRecentFile(updatedItem) - } else if (localBook.lastModifiedTimestamp > remoteMeta.lastModifiedTimestamp) { - recentFilesRepository.syncLocalMetadataToFolder(localBook.bookId) - } - } - } - - if (booksToDelete.isNotEmpty()) { - recentFilesRepository.deleteFilePermanently(booksToDelete) } prefs.edit { putLong(MainViewModel.KEY_LAST_FOLDER_SCAN_TIME, System.currentTimeMillis()) } @@ -335,6 +200,7 @@ class FolderSyncWorker( ) return Result.success() + } catch (e: Exception) { Timber.tag("FolderSync").e(e, "Error during folder sync worker execution.") return Result.failure() diff --git a/app/src/main/java/com/aryan/reader/HomeScreen.kt b/app/src/main/java/com/aryan/reader/HomeScreen.kt index 8f282e4..1270f8b 100644 --- a/app/src/main/java/com/aryan/reader/HomeScreen.kt +++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt @@ -1140,17 +1140,17 @@ fun FpsMonitor(modifier: Modifier = Modifier) { @Composable private fun FolderMigrationDialog(onConfirm: () -> Unit) { AlertDialog( - onDismissRequest = { }, // Force acknowledgment + onDismissRequest = { }, icon = { Icon(Icons.Default.FolderSpecial, contentDescription = null) }, - title = { Text("Folder Sync Refactored") }, + title = { Text("Folder Sync Update") }, text = { Column { Text( - "We've completely rebuilt how Folder Sync works! Books from the folder are now read directly from your folder instead of being copied to app storage." + "We've improved Folder Sync! Books are now read directly from your folder without duplicating files." ) Spacer(modifier = Modifier.height(12.dp)) Text( - "We'll now perform a one-time scan to migrate your existing progress and bookmarks. This will also free up internal storage space on your device.", + "To keep your reading progress safe, your previously synced books have been converted to standard local books. You may see duplicates once the folder resyncs; you can safely delete the old copies at your convenience.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -1158,7 +1158,7 @@ private fun FolderMigrationDialog(onConfirm: () -> Unit) { }, confirmButton = { TextButton(onClick = onConfirm) { - Text("Start Migration") + Text("Got it") } } ) diff --git a/app/src/main/java/com/aryan/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt index ffc6366..b52940c 100644 --- a/app/src/main/java/com/aryan/reader/MainViewModel.kt +++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt @@ -529,9 +529,15 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio remoteConfigRepository.init() + val isMigrationCompleted = prefs.getBoolean(KEY_FOLDER_MIGRATION_COMPLETED, false) + if (_internalState.value.syncedFolderUri != null) { - Timber.d("App Start: Triggering local folder metadata-only sync.") - syncFolderMetadata() + if (isMigrationCompleted) { + Timber.d("App Start: Triggering local folder metadata-only sync.") + syncFolderMetadata() + } else { + Timber.d("App Start: Skipping sync. Waiting for migration/detachment logic.") + } } viewModelScope.launch { billingClientWrapper.initializeConnection() } @@ -593,11 +599,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } fun completeFolderMigration() { - Timber.tag("FolderSync").d("User accepted migration. Marking completed and starting scan.") - prefs.edit { putBoolean(KEY_FOLDER_MIGRATION_COMPLETED, true) } - _internalState.update { it.copy(showFolderMigrationDialog = false) } + Timber.tag("FolderSync").d("User acknowledged update. Detaching old books and starting fresh scan.") - scanSyncedFolder() + viewModelScope.launch { + recentFilesRepository.detachAllFolderBooks() + + prefs.edit { putBoolean(KEY_FOLDER_MIGRATION_COMPLETED, true) } + _internalState.update { it.copy(showFolderMigrationDialog = false) } + + scanSyncedFolder() + } } private val fontsRepository = FontsRepository(appContext) @@ -1330,8 +1341,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (workInfo != null) { when (workInfo.state) { WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> { - val msg = if (metadataOnly) "Folder Sync: Updating metadata..." else "Folder Sync: Scanning files..." - _internalState.update { it.copy(isLoading = true, bannerMessage = BannerMessage(msg)) } + val msg = if (metadataOnly) "Folder Sync: Updating metadata..." else "Scanning folder for new books..." + _internalState.update { it.copy( + isLoading = false, + isRefreshing = true, + bannerMessage = BannerMessage(msg) + ) } } WorkInfo.State.SUCCEEDED -> { _internalState.update { it.copy( @@ -1342,7 +1357,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio ) } } WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { - _internalState.update { it.copy(isLoading = false, isRefreshing = false, errorMessage = "Sync failed.") } // ADD isRefreshing = false + _internalState.update { it.copy( + isLoading = false, + isRefreshing = false, + errorMessage = "Sync failed." + ) } } else -> Unit } @@ -2255,9 +2274,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio if (type == FileType.PDF) { viewModelScope.launch { val recentItem = recentFilesRepository.getFileByBookId(bookId) - Timber.d( - "openBook: Loading PDF. bookId=$bookId, initialBookmarksJson from DB: ${recentItem?.bookmarksJson}" - ) + + if (recentItem?.sourceFolderUri != null) { + launch(Dispatchers.IO) { + recentFilesRepository.syncLocalMetadataToFolder(bookId) + } + } + + Timber.d("openBook: Loading PDF. bookId=$bookId ...") _internalState.update { it.copy( selectedPdfUri = uri, @@ -2278,6 +2302,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio } else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.MD || type == FileType.TXT || type == FileType.HTML) { viewModelScope.launch { val recentItem = recentFilesRepository.getFileByBookId(bookId) + if (recentItem?.sourceFolderUri != null) { + launch(Dispatchers.IO) { + recentFilesRepository.syncLocalMetadataToFolder(bookId) + } + } val locator = if (recentItem?.lastChapterIndex != null && recentItem.locatorBlockIndex != null && recentItem.locatorCharOffset != null) { Locator( diff --git a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt index 1c68dfd..d48a19b 100644 --- a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt +++ b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt @@ -90,7 +90,6 @@ class MetadataExtractionWorker( } } - // Only update if we actually found something useful if (coverPath != null || title != null || author != null) { val updatedItem = item.copy( coverImagePath = coverPath ?: item.coverImagePath, @@ -99,6 +98,13 @@ class MetadataExtractionWorker( ) recentFilesRepository.addRecentFile(updatedItem) Timber.tag("MetadataWorker").d("Updated metadata for: ${item.displayName}") + + try { + recentFilesRepository.syncLocalMetadataToFolder(updatedItem.bookId) + Timber.tag("MetadataWorker").d("Created/Updated JSON for: ${item.displayName}") + } catch (_: Exception) { + Timber.tag("MetadataWorker").w("Failed to save JSON during extraction for ${item.displayName}") + } } } catch (e: Exception) { diff --git a/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt index 8fae6dc..10087b1 100644 --- a/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt +++ b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt @@ -87,4 +87,7 @@ interface RecentFileDao { @Query("SELECT * FROM recent_files WHERE sourceFolderUri IS NOT NULL AND coverImagePath IS NULL AND isDeleted = 0") suspend fun getFolderBooksWithoutCovers(): List + + @Query("UPDATE recent_files SET sourceFolderUri = NULL WHERE sourceFolderUri IS NOT NULL") + suspend fun detachAllFolderBooks() } \ No newline at end of file diff --git a/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt index 9bfe398..89ca4e5 100644 --- a/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt +++ b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt @@ -172,6 +172,11 @@ class RecentFilesRepository(private val context: Context) { return@withContext recentFileDao.getFolderBooksWithoutCovers().map { it.toRecentFileItem() } } + suspend fun detachAllFolderBooks() = withContext(Dispatchers.IO) { + recentFileDao.detachAllFolderBooks() + Timber.d("Detached all folder books. They are now standard local files.") + } + suspend fun updateBookmarks(bookId: String, bookmarksJson: String) = withContext(Dispatchers.IO) { val currentTime = System.currentTimeMillis() recentFileDao.updateBookmarks(bookId, bookmarksJson, currentTime)