diff --git a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserver.kt b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserver.kt index f8d9899d..76aabbb5 100644 --- a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserver.kt +++ b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserver.kt @@ -7,6 +7,7 @@ import android.os.Handler import com.anggrayudi.storage.media.MediaType import com.w2sv.common.logging.log import com.w2sv.common.uri.MediaId +import com.w2sv.common.uri.MediaUri import com.w2sv.common.uri.mediaUri import com.w2sv.domain.model.filetype.FileAndSourceType import com.w2sv.domain.model.navigatorconfig.AutoMoveConfig @@ -26,8 +27,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import slimber.log.i +// TODO naming private data class MoveFileWithProcedureJob(val moveFile: MoveFile, val procedureJob: Job) +// TODO increase private const val CANCEL_PERIOD_MILLIS = 300L private enum class FileChangeOperation(private val flag: Int?) { @@ -36,11 +39,13 @@ private enum class FileChangeOperation(private val flag: Int?) { Update(ContentResolver.NOTIFY_UPDATE), Insert(ContentResolver.NOTIFY_INSERT), Delete(ContentResolver.NOTIFY_DELETE), + SyncToNetwork(ContentResolver.NOTIFY_SYNC_TO_NETWORK), Unclassified(null); companion object { - operator fun invoke(contentObserverOnChangeFlags: Int): FileChangeOperation = - entries.first { it.flag == null || it.flag and contentObserverOnChangeFlags != 0 } + // TODO what if flags are combined + operator fun invoke(notifyFlags: Int): FileChangeOperation = + entries.first { it.flag == null || it.flag and notifyFlags != 0 } } } @@ -55,6 +60,13 @@ internal abstract class FileObserver(val mediaType: MediaType, handler: Handler, get() = this::class.java.simpleName init { + collectBlacklistedMediaIds() + } + + final override fun deliverSelfNotifications(): Boolean = + false + + private fun collectBlacklistedMediaIds() { blacklistedMediaUris .filter { it.mediaType == mediaType } .map { it.mediaId } @@ -67,22 +79,24 @@ internal abstract class FileObserver(val mediaType: MediaType, handler: Handler, } } - final override fun deliverSelfNotifications(): Boolean = - false - private fun cancelAndResetMoveFileProcedureJob() { moveFileWithProcedureJob?.procedureJob?.cancel() moveFileWithProcedureJob = null } override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) { - when (FileChangeOperation(flags).also { emitOnChangeLog(uri, it) }) { - FileChangeOperation.Insert -> Unit - FileChangeOperation.Update, FileChangeOperation.Unclassified -> onChangeCore(uri) + // TODO log flags, not just parsed operation + val operation = FileChangeOperation(flags) + emitOnChangeLog(uri, operation) + + when (operation) { + FileChangeOperation.Update -> onChangeCore(uri) FileChangeOperation.Delete -> cancelAndResetMoveFileProcedureJob() + else -> Unit } } + // TODO needed? override fun onChange(selfChange: Boolean, uri: Uri?) { emitOnChangeLog(uri, FileChangeOperation.Unclassified) onChangeCore(uri) @@ -93,30 +107,17 @@ internal abstract class FileObserver(val mediaType: MediaType, handler: Handler, } private fun onChangeCore(uri: Uri?) { - val mediaUri = uri?.mediaUri ?: return - - val mediaId = mediaUri.id() ?: run { - i { "mediaId null; discarding" } - return - } - // Exit if in mediaUriBlacklist - if (mediaIdBlacklist.contains(mediaId)) { - i { "Found $mediaId in blacklist; discarding" } - return - } + val mediaUri = uri?.mediaUri ?: return discard("mediaUri null") + val mediaId = mediaUri.id() ?: return discard("mediaId null") + if (mediaIdBlacklist.contains(mediaId)) return discard("mediaId blacklisted") - val mediaStoreDataRetrievalResult = mediaStoreDataProducer( + val mediaStoreDataRetrievalResult = mediaStoreDataProvider( mediaUri = mediaUri, contentResolver = context.contentResolver ) .asSuccessOrNull ?: return - moveFileWithProcedureJob?.run { - val (moveFile, procedureJob) = this - if (mediaStoreDataRetrievalResult.isUpdateOfAlreadySeenFile && mediaUri == moveFile.mediaUri) { - procedureJob.cancel() - } - } + cancelExistingJobIfUpdate(isUpdateOfAlreadySeenFile = mediaStoreDataRetrievalResult.isUpdateOfSeenFile, mediaUri = mediaUri) matchingFileAndSourceTypeOrNull(mediaStoreDataRetrievalResult.data)?.let { fileAndSourceType -> val moveFile = MoveFile( @@ -124,40 +125,49 @@ internal abstract class FileObserver(val mediaType: MediaType, handler: Handler, mediaStoreData = mediaStoreDataRetrievalResult.data, fileAndSourceType = fileAndSourceType ) - .log { "Calling onMoveFile on $it" } - - scope.launch { - // TODO maybe cache via StateFlows - val enabledAutoMoveDestinationOrNull = navigatorConfigFlow - .first() - .autoMoveConfig( - fileType = moveFile.fileType, - sourceType = moveFile.sourceType - ) - .enabledDestinationOrNull - - moveFileWithProcedureJob = MoveFileWithProcedureJob( - moveFile = moveFile, - procedureJob = scope.launchDelayed(CANCEL_PERIOD_MILLIS) { - enabledAutoMoveDestinationOrNull?.let { - // TODO Why not perform the moving directly instead of starting a receiver with parceling - MoveBroadcastReceiver.sendBroadcast( - operation = MoveOperation.AutoMove( - file = moveFile, - destination = enabledAutoMoveDestinationOrNull, - destinationSelectionManner = DestinationSelectionManner.Auto - ), - context = context - ) - } ?: run { - notificationEventHandler(NotificationEvent.PostMoveFile(moveFile)) - } - } - ) + .log { "Created $it" } + scope.launch { onNewMoveFile(moveFile) } + } + } + + private fun discard(reason: String) { + i { "$logIdentifier: discarding because $reason" } + } + + private fun cancelExistingJobIfUpdate(isUpdateOfAlreadySeenFile: Boolean, mediaUri: MediaUri) { + moveFileWithProcedureJob?.run { + val (moveFile, procedureJob) = this + if (isUpdateOfAlreadySeenFile && mediaUri == moveFile.mediaUri) { + procedureJob.cancel() } } } + private suspend fun onNewMoveFile(moveFile: MoveFile) { + // TODO maybe cache via StateFlows + val autoMoveDestination = navigatorConfigFlow + .first() + .autoMoveConfig(moveFile.fileType, moveFile.sourceType) + .enabledDestinationOrNull + + moveFileWithProcedureJob = MoveFileWithProcedureJob( + moveFile = moveFile, + procedureJob = scope.launchDelayed(CANCEL_PERIOD_MILLIS) { + autoMoveDestination + // TODO Perform the moving directly instead of starting a receiver with parceling + ?.let { sendAutoMoveBroadcast(moveFile, it) } + ?: notificationEventHandler(NotificationEvent.PostMoveFile(moveFile)) + } + ) + } + + private fun sendAutoMoveBroadcast(moveFile: MoveFile, destination: MoveDestination.Directory) { + MoveBroadcastReceiver.sendBroadcast( + MoveOperation.AutoMove(moveFile, destination, DestinationSelectionManner.Auto), + context + ) + } + /** * This method determines whether the observer will fire for the received [mediaStoreFileData] or not. */ diff --git a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserverEnvironment.kt b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserverEnvironment.kt index a2a578f3..ba9551fc 100644 --- a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserverEnvironment.kt +++ b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserverEnvironment.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.SharedFlow internal interface FileObserverEnvironment { val context: Context val scope: CoroutineScope - val mediaStoreDataProducer: MediaStoreDataProducer + val mediaStoreDataProvider: MediaStoreDataProvider val blacklistedMediaUris: SharedFlow val notificationEventHandler: NotificationEventHandler val navigatorConfigFlow: NavigatorConfigFlow @@ -27,7 +27,7 @@ internal interface FileObserverEnvironment { internal class FileObserverEnvironmentImpl @Inject constructor( @ApplicationContext override val context: Context, @ApplicationIoScope override val scope: CoroutineScope, - override val mediaStoreDataProducer: MediaStoreDataProducer, + override val mediaStoreDataProvider: MediaStoreDataProvider, override val blacklistedMediaUris: SharedFlow, override val notificationEventHandler: NotificationEventHandler, override val navigatorConfigFlow: NavigatorConfigFlow diff --git a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProducer.kt b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProvider.kt similarity index 65% rename from core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProducer.kt rename to core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProvider.kt index 51777a43..4a5bfbfc 100644 --- a/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProducer.kt +++ b/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/MediaStoreDataProvider.kt @@ -5,36 +5,17 @@ import com.w2sv.common.uri.MediaUri import com.w2sv.navigator.domain.moving.MediaStoreFileData import com.w2sv.navigator.shared.discardedLog import javax.inject.Inject -import javax.inject.Singleton private const val SEEN_FILES_BUFFER_SIZE = 5 -@Singleton -internal class MediaStoreDataProducer @Inject constructor() { - - sealed interface Result { - data class Success(val data: MediaStoreFileData, val isUpdateOfAlreadySeenFile: Boolean) : Result - - sealed interface Failure : Result - - data object CouldntRetrieve : Failure - data object FileIsPending : Failure - data object FileIsTrashed : Failure - data object AlreadySeen : Failure - - val asSuccessOrNull: Success? - get() = this as? Success - } - - private data class SeenParameters(val uri: MediaUri, val fileSize: Long) +internal class MediaStoreDataProvider @Inject constructor() { private val seenParametersBuffer = RecentSet(SEEN_FILES_BUFFER_SIZE) operator fun invoke(mediaUri: MediaUri, contentResolver: ContentResolver): Result { - // Fetch MediaStoreColumnData; exit if impossible - val columnData = - MediaStoreFileData.queryFor(mediaUri, contentResolver) - ?: return Result.CouldntRetrieve + // Fetch MediaStoreFileData; exit if impossible + val columnData = MediaStoreFileData.queryFor(mediaUri, contentResolver) + ?: return Result.RetrievalUnsuccessful // Exit if file is pending or trashed if (columnData.isPending) { @@ -52,10 +33,24 @@ internal class MediaStoreDataProducer @Inject constructor() { return Result.AlreadySeen } - val isUpdateOfAlreadySeenFile = seenParametersBuffer.replaceIf( + val isUpdateOfSeenFile = seenParametersBuffer.replaceIf( predicate = { it.uri == seenParameters.uri }, element = seenParameters ) - return Result.Success(data = columnData, isUpdateOfAlreadySeenFile = isUpdateOfAlreadySeenFile) + return Result.Success(data = columnData, isUpdateOfSeenFile = isUpdateOfSeenFile) } + + sealed interface Result { + data class Success(val data: MediaStoreFileData, val isUpdateOfSeenFile: Boolean) : Result + + data object RetrievalUnsuccessful : Result + data object FileIsPending : Result + data object FileIsTrashed : Result + data object AlreadySeen : Result + + val asSuccessOrNull: Success? + get() = this as? Success + } + + private data class SeenParameters(val uri: MediaUri, val fileSize: Long) } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 19a6bdeb..37f78a6a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME