Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?) {
Expand All @@ -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 }
}
}

Expand All @@ -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 }
Expand All @@ -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)
Expand All @@ -93,71 +107,67 @@ 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(
mediaUri = mediaUri,
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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MediaIdWithMediaType>
val notificationEventHandler: NotificationEventHandler
val navigatorConfigFlow: NavigatorConfigFlow
Expand All @@ -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<MediaIdWithMediaType>,
override val notificationEventHandler: NotificationEventHandler,
override val navigatorConfigFlow: NavigatorConfigFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SeenParameters>(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) {
Expand All @@ -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)
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.g
mockito-kotlin = "org.mockito.kotlin:mockito-kotlin:6.2.1"
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
robolectric = "org.robolectric:robolectric:4.16"
robolectric = "org.robolectric:robolectric:4.16.1"
slimber = "com.github.PaulWoitaschek:Slimber:2.0.0"
textflow = "io.github.oleksandrbalan:textflow-material3:1.2.1"
w2sv-androidutils-core = "com.github.w2sv.AndroidUtils:core:0.5.0"
Expand Down