diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index 5f7bc37d26..e7c390d834 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -48,6 +48,7 @@ import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.getTypeOfObject import com.anytypeio.anytype.feature_chats.BuildConfig +import com.anytypeio.anytype.feature_chats.tools.ClearChatsTempFolder import com.anytypeio.anytype.feature_chats.tools.DummyMessageGenerator import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.confgs.ChatConfig @@ -101,7 +102,8 @@ class ChatViewModel @Inject constructor( private val generateSpaceInviteLink: GenerateSpaceInviteLink, private val makeSpaceShareable: MakeSpaceShareable, private val getSpaceInviteLink: GetSpaceInviteLink, - private val revokeSpaceInviteLink: RevokeSpaceInviteLink + private val revokeSpaceInviteLink: RevokeSpaceInviteLink, + private val clearChatsTempFolder: ClearChatsTempFolder ) : BaseViewModel(), ExitToVaultDelegate by exitToVaultDelegate { private val visibleRangeUpdates = MutableSharedFlow>( @@ -547,6 +549,8 @@ class ChatViewModel @Inject constructor( val normalizedMarkup = (markup + parsedUrls).sortedBy { it.range.first } + var shouldClearChatTempFolder = false + chatBoxMode.value = chatBoxMode.value.updateIsSendingBlocked(isBlocked = true) val attachments = buildList { val currAttachments = chatBoxAttachments.value @@ -594,6 +598,7 @@ class ChatViewModel @Inject constructor( ) } val path = if (attachment.capturedByCamera) { + shouldClearChatTempFolder = true withContext(dispatchers.io) { copyFileToCacheDirectory.copy(attachment.uri) }.orEmpty() @@ -610,6 +615,14 @@ class ChatViewModel @Inject constructor( Block.Content.File.Type.IMAGE ) ).onSuccess { file -> + withContext(dispatchers.io) { + val isDeleted = copyFileToCacheDirectory.delete(path) + if (isDeleted) { + Timber.d("DROID-2966 Successfully deleted temp file: ${attachment.uri}") + } else { + Timber.w("DROID-2966 Error while deleting temp file: ${attachment.uri}") + } + } add( Chat.Message.Attachment( target = file.id, @@ -688,7 +701,7 @@ class ChatViewModel @Inject constructor( type = Block.Content.File.Type.NONE ) ).onSuccess { file -> - // TODO delete file. + copyFileToCacheDirectory.delete(path) add( Chat.Message.Attachment( target = file.id, @@ -792,6 +805,12 @@ class ChatViewModel @Inject constructor( // Do nothing. } } + + if (shouldClearChatTempFolder) { + withContext(dispatchers.io) { + clearChatsTempFolder() + } + } } } diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt index bd002308fd..79d0df454e 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt @@ -22,6 +22,7 @@ import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.notifications.NotificationBuilder import com.anytypeio.anytype.domain.objects.CreateObjectFromUrl import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.feature_chats.tools.ClearChatsTempFolder import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManager import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory import com.anytypeio.anytype.presentation.vault.ExitToVaultDelegate @@ -51,7 +52,8 @@ class ChatViewModelFactory @Inject constructor( private val generateSpaceInviteLink: GenerateSpaceInviteLink, private val makeSpaceShareable: MakeSpaceShareable, private val getSpaceInviteLink: GetSpaceInviteLink, - private val revokeSpaceInviteLink: RevokeSpaceInviteLink + private val revokeSpaceInviteLink: RevokeSpaceInviteLink, + private val clearChatsTempFolder: ClearChatsTempFolder ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = ChatViewModel( @@ -78,6 +80,7 @@ class ChatViewModelFactory @Inject constructor( generateSpaceInviteLink = generateSpaceInviteLink, makeSpaceShareable = makeSpaceShareable, getSpaceInviteLink = getSpaceInviteLink, - revokeSpaceInviteLink = revokeSpaceInviteLink + revokeSpaceInviteLink = revokeSpaceInviteLink, + clearChatsTempFolder = clearChatsTempFolder ) as T } \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/tools/Media.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/tools/Media.kt index 922e1a78ce..5f6724c3e2 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/tools/Media.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/tools/Media.kt @@ -5,6 +5,8 @@ import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher import androidx.core.content.FileProvider import java.io.File +import javax.inject.Inject +import timber.log.Timber fun launchCamera( @@ -12,7 +14,13 @@ fun launchCamera( launcher: ManagedActivityResultLauncher, onUriReceived: (Uri) -> Unit ) { - val photoFile = File.createTempFile("IMG_", ".jpg", context.cacheDir).apply { + val tempDir = File(context.cacheDir, CHATS_TEMP_FOLDER_NAME) + if (!tempDir.exists()) { + val created = tempDir.mkdirs() + Timber.d("Created camera temp dir: $created at ${tempDir.absolutePath}") + } + + val photoFile = File.createTempFile("IMG_", ".jpg", tempDir).apply { createNewFile() deleteOnExit() } @@ -23,7 +31,29 @@ fun launchCamera( photoFile ) - onUriReceived(uri) + Timber.d("Launching camera with URI: $uri (path: ${photoFile.absolutePath})") + onUriReceived(uri) launcher.launch(uri) +} + +const val CHATS_TEMP_FOLDER_NAME = "chats_temp_folder" + +class ClearChatsTempFolder @Inject constructor( + private val context: Context +) { + companion object { + private const val CHATS_TEMP_FOLDER_NAME = "chats_temp_folder" + } + + operator fun invoke(): Boolean { + val folder = File(context.cacheDir, CHATS_TEMP_FOLDER_NAME) + return if (folder.exists()) { + val deleted = folder.deleteRecursively() + // Optional: log if needed + deleted + } else { + false + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt index bcfceeeef0..21ee65f104 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt @@ -4,6 +4,9 @@ import android.content.Context import android.net.Uri import android.provider.OpenableColumns import com.anytypeio.anytype.core_utils.ext.msg +import java.io.File +import java.io.FileOutputStream +import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -11,9 +14,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.File -import java.io.FileOutputStream -import java.lang.ref.WeakReference /** * Interface defining the contract for copying files to a cache directory. @@ -42,6 +42,8 @@ interface CopyFileToCacheDirectory { * @return `true` if the operation is active, `false` otherwise. */ fun isActive(): Boolean + + fun delete(uri: String): Boolean } /** @@ -212,6 +214,37 @@ class DefaultCopyFileToCacheDirectory(context: Context) : CopyFileToCacheDirecto } return result } + + override fun delete(uri: String): Boolean { + val context = mContext?.get() ?: return false + return try { + val path = Uri.parse(uri).path ?: return false + val file = File(path) + + // Optional: check if file is in cache or external files dir + val allowedRoots = listOfNotNull( + context.cacheDir?.absolutePath, + context.getExternalFilesDir(null)?.absolutePath + ) + + if (allowedRoots.any { file.absolutePath.startsWith(it) }) { + if (!file.exists()) { + Timber.w("File does not exist: $path") + return false + } + + val deleted = file.delete() + Timber.d("Attempting to delete file: $path → deleted=$deleted") + deleted + } else { + Timber.w("Blocked delete attempt outside allowed folders: $path") + false + } + } catch (e: Exception) { + Timber.e(e, "Error deleting file at $uri") + false + } + } } /** @@ -330,6 +363,19 @@ class NetworkModeCopyFileToCacheDirectory(context: Context) : CopyFileToCacheDir return result } + override fun delete(uri: String): Boolean { + val context = mContext?.get() ?: return false + return try { + val file = File(Uri.parse(uri).path ?: return false) + val deleted = file.delete() + Timber.d("Attempting to delete file by uri: $uri → $deleted") + deleted + } catch (e: Exception) { + Timber.e(e, "Error deleting file by uri: $uri") + false + } + } + companion object { const val CONFIG_FILE_NAME = "configCustom.txt" }