From cbda62312e00b0e25fb04d22d97df45160fadd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20N=C3=A9risson?= Date: Fri, 20 Dec 2024 03:42:33 +0100 Subject: [PATCH 1/3] feat: add data from real travels storage --- app/build.gradle.kts | 3 +- .../com/github/se/travelpouch/MainActivity.kt | 2 +- .../github/se/travelpouch/di/AppModules.kt | 22 ++++- .../model/documents/DocumentContainer.kt | 12 +++ .../model/documents/DocumentRepository.kt | 7 +- .../model/documents/DocumentViewModel.kt | 22 ++++- .../model/documents/DocumentsManager.kt | 54 ++++++++--- .../model/home/StorageDashboardViewModel.kt | 97 ++++++++++++++++--- .../model/travels/ListTravelViewModel.kt | 17 +++- .../travelpouch/ui/authentication/SignIn.kt | 2 +- .../travelpouch/ui/home/StorageDashboard.kt | 43 ++++---- cloud-functions/functions/src/index.ts | 2 +- gradle/libs.versions.toml | 2 + 13 files changed, 225 insertions(+), 60 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 29d0a66a..35d054ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -199,8 +199,9 @@ dependencies { implementation(libs.play.services.location) implementation(libs.firebase.messaging.ktx) implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.datastore.core.android) - testImplementation(libs.junit) + testImplementation(libs.junit) globalTestImplementation(libs.androidx.junit) globalTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt index ffbad4a2..5eeaf0f5 100644 --- a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt @@ -85,7 +85,7 @@ class MainActivity : ComponentActivity() { val calendarViewModel: CalendarViewModel = viewModel(factory = CalendarViewModel.Factory(activityModelView)) val locationViewModel: LocationViewModel = viewModel(factory = LocationViewModel.Factory) - val storageDashboardViewModel = viewModel() + val storageDashboardViewModel = hiltViewModel() NavHost(navController = navController, startDestination = Route.DEFAULT) { navigation( diff --git a/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt b/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt index ae43094b..d5c0ec52 100644 --- a/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt +++ b/app/src/main/java/com/github/se/travelpouch/di/AppModules.kt @@ -13,6 +13,7 @@ import com.github.se.travelpouch.model.authentication.AuthenticationService import com.github.se.travelpouch.model.authentication.FirebaseAuthenticationService import com.github.se.travelpouch.model.documents.DocumentRepository import com.github.se.travelpouch.model.documents.DocumentRepositoryFirestore +import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.model.documents.DocumentsManager import com.github.se.travelpouch.model.events.EventRepository import com.github.se.travelpouch.model.events.EventRepositoryFirebase @@ -20,6 +21,7 @@ import com.github.se.travelpouch.model.notifications.NotificationRepository import com.github.se.travelpouch.model.notifications.NotificationRepositoryFirestore import com.github.se.travelpouch.model.profile.ProfileRepository import com.github.se.travelpouch.model.profile.ProfileRepositoryFirebase +import com.github.se.travelpouch.model.travels.ListTravelViewModel import com.github.se.travelpouch.model.travels.TravelRepository import com.github.se.travelpouch.model.travels.TravelRepositoryFirestore import com.google.firebase.Firebase @@ -101,7 +103,7 @@ object AppModule { @Provides @Singleton - fun provideFileDownloader( + fun provideFileManager( @ApplicationContext context: Context, storage: FirebaseStorage, functions: FirebaseFunctions, @@ -139,4 +141,22 @@ object AppModule { return PreferenceDataStoreFactory.create( produceFile = { context.preferencesDataStoreFile("documents") }) } + + @Provides + @Singleton + fun provideDocumentViewModel( + repository: DocumentRepository, + documentsManager: DocumentsManager, + dataStore: DataStore + ): DocumentViewModel { + return DocumentViewModel(repository, documentsManager, dataStore) + } + + @Provides + @Singleton + fun provideListTravelViewModel( + repository: TravelRepository + ): ListTravelViewModel { + return ListTravelViewModel(repository) + } } diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentContainer.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentContainer.kt index 47ff74f1..c57c8784 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentContainer.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentContainer.kt @@ -1,6 +1,7 @@ // Portions of this code were generated and or inspired by the help of GitHub Copilot or Chatgpt package com.github.se.travelpouch.model.documents +import android.net.Uri import com.google.firebase.Timestamp import com.google.firebase.firestore.DocumentReference @@ -81,3 +82,14 @@ enum class DocumentVisibility { ORGANIZERS, PARTICIPANTS } + +/** + * Data class representing a document container. + * + * @property document the document container + * @property uri the uri of the document + */ +data class OfflineDocumentContainer( + val document: DocumentContainer, + val uri: Uri? +) diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt index 16ea6cb1..1b72b7f2 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt @@ -16,7 +16,7 @@ import com.google.firebase.storage.FirebaseStorage interface DocumentRepository { fun setIdTravel(onSuccess: () -> Unit, travelId: String) - fun getDocuments(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) + fun getDocuments(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit, collectionPathOverride: String? = null) fun deleteDocumentById( document: DocumentContainer, @@ -63,9 +63,10 @@ class DocumentRepositoryFirestore( */ override fun getDocuments( onSuccess: (List) -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + collectionPathOverride: String? ) { - db.collection(collectionPath).get().addOnCompleteListener { task -> + db.collection(collectionPathOverride ?: collectionPath).get().addOnCompleteListener { task -> if (task.isSuccessful) { val documents = task.result?.documents?.mapNotNull { document -> fromSnapshot(document) } ?: emptyList() diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt index a9bb557e..0db65cfe 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModel import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.travels.TravelContainer import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CompletableDeferred import java.io.ByteArrayOutputStream import java.io.InputStream import javax.inject.Inject @@ -64,11 +65,24 @@ constructor( repository.setIdTravel({ getDocuments() }, travelId) } - /** Gets all Documents. */ - fun getDocuments() { + /** + * Retrieves all documents from the repository and updates the documents state. + * + * @param collectionPathOverride The path to the collection to get documents from. + * @return A deferred object that completes when the documents are retrieved. + */ + fun getDocuments(collectionPathOverride: String? = null): CompletableDeferred> { + val ret = CompletableDeferred>() repository.getDocuments( - onSuccess = { _documents.value = it }, - onFailure = { Log.e("DocumentsViewModel", "Failed to get Documents", it) }) + onSuccess = { + ret.complete(it) + _documents.value = it }, + onFailure = { + Log.e("DocumentsViewModel", "Failed to get Documents", it) + ret.completeExceptionally(it) + }, + collectionPathOverride = collectionPathOverride) + return ret } /** diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt index f14c4d05..d07acc94 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt @@ -6,7 +6,6 @@ import android.net.Uri import android.util.Log import androidx.core.net.toUri import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.documentfile.provider.DocumentFile @@ -23,13 +22,13 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await -/** Helper class to download files from Firbase storage to local files */ +/** Helper class to download files from Firebase storage to local files */ open class DocumentsManager( - private val contentResolver: ContentResolver, - private val storage: FirebaseStorage, - private val functions: FirebaseFunctions, - private val dataStore: DataStore, - private val thumbsDirectory: File + private val contentResolver: ContentResolver, + private val storage: FirebaseStorage, + private val functions: FirebaseFunctions, + private val dataStore: DataStore, + private val thumbsDirectory: File ) { private val tag = DocumentsManager::class.java.simpleName @@ -90,17 +89,48 @@ open class DocumentsManager( val pathFlow: Flow = dataStore.data.map { preferences -> preferences[documentUid] } return try { pathFlow.first()?.let { - Uri.parse(it).takeIf { - val afd = contentResolver.openAssetFileDescriptor(it, "r") - afd?.close() - afd != null - } + parseUriAndCheckFile(it) } } catch (_: Exception) { null } } + /** + * Parse the string into a Uri and check if the file exists. + * + * @param string The string to parse + * @return The Uri if the file exists, null otherwise + */ + private fun parseUriAndCheckFile(string: String?): Uri? { + return Uri.parse(string).takeIf { + val afd = contentResolver.openAssetFileDescriptor(it, "r") + afd?.close() + afd != null + } + } + + /** + * Return a list of cached documents from the list of documents. + * + * @param documents The list of documents to find in the cache + * @return The list of cached documents + */ + suspend fun getCachedDocuments(documents: List): List { + val preferences = try { + dataStore.data.first() + } catch (e: Exception) { + Log.e(tag, "Failed to find cached documents", e) + return emptyList() + } + + return documents.map { document -> + val path = preferences[stringPreferencesKey(document.ref.id)] + val uri = if (path != null) Uri.parse(path) ?: null else null + OfflineDocumentContainer(document, uri) + } + } + /** * Add an entry in the cache. * diff --git a/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt index b0ab59fa..0fd2dc96 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt @@ -2,6 +2,11 @@ package com.github.se.travelpouch.model.home import androidx.lifecycle.ViewModel +import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.documents.DocumentViewModel +import com.github.se.travelpouch.model.documents.DocumentsManager +import com.github.se.travelpouch.model.travels.ListTravelViewModel +import com.github.se.travelpouch.model.travels.TravelContainer import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.math.max @@ -10,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -fun formatStorageUnit(number: Long): String { +fun formatStorageUnit(number: Long, hideUnit: Boolean? = null): String { val units = arrayOf("B", "KiB", "MiB", "GiB") var size = number.toDouble() var unitIndex = 0 @@ -19,10 +24,52 @@ fun formatStorageUnit(number: Long): String { size /= 1024 unitIndex++ } - + if (hideUnit == true) { + return String.format(null, "%.1f", size) + } return String.format(null, "%.1f %s", size, units[unitIndex]) } +data class TravelStorageStats( + val travel: TravelContainer, + val storageUsed: Long, + val storageMax: Long, + val storageReclaimable: Long, +) { + /** + * Merges the storage stats with another storage stats object. + * + * @param other The other storage stats object to merge with. + * @return The merged storage stats object. + */ + fun merge(other: TravelStorageStats): TravelStorageStats { + return TravelStorageStats( + travel = travel, + storageUsed = storageUsed + other.storageUsed, + storageMax = storageMax + other.storageMax, + storageReclaimable = storageReclaimable + other.storageReclaimable, + ) + } + + /** + * Converts the storage stats to a storage dashboard stats object. + * + * @param storageLimit The storage limit for the travel. + * @return The storage dashboard stats object. + */ + fun toStorageDashboardStats(storageLimit: Long?): StorageDashboardStats { + return StorageDashboardStats( + storageLimit = storageLimit, + storageUsed = storageUsed, + storageReclaimable = storageReclaimable, + ) + } + + fun storageUsageToString(): String { + return "${formatStorageUnit(storageUsed, hideUnit = true)} / ${formatStorageUnit(storageMax)}" + } +} + data class StorageDashboardStats( var storageLimit: Long?, val storageUsed: Long, @@ -65,24 +112,44 @@ data class StorageDashboardStats( } @HiltViewModel -open class StorageDashboardViewModel @Inject constructor() : ViewModel() { +open class StorageDashboardViewModel @Inject constructor(private val documentsManager: DocumentsManager, private val documentViewModel: DocumentViewModel, private val travelViewModel: ListTravelViewModel) : ViewModel() { private val _isLoading = MutableStateFlow(true) open val isLoading: StateFlow = _isLoading.asStateFlow() - private val _storageStats = MutableStateFlow(null) - open val storageStats: StateFlow = _storageStats.asStateFlow() + private val _globalStorageStats = MutableStateFlow(null) + open val globalStorageStats: StateFlow = _globalStorageStats.asStateFlow() + private val _travelStorageStats = MutableStateFlow>(emptyList()) + open val travelStorageStats: StateFlow> = _travelStorageStats.asStateFlow() - open fun setStorageStats(storageStats: StorageDashboardStats?) { - _storageStats.value = storageStats + open fun setStorageStats(travelStorageStats: List, storageLimit: Long? = null) { + _travelStorageStats.value = travelStorageStats + _globalStorageStats.value = travelStorageStats.reduce(TravelStorageStats::merge).toStorageDashboardStats(storageLimit) _isLoading.value = false } - // Temporary function to update storage stats - fun updateStorageStats() { - setStorageStats( - StorageDashboardStats( - storageLimit = 500 * 1024L * 1024L, - storageUsed = 400 * 1024L * 1024L, - storageReclaimable = 50 * 1024L * 1024L, - )) + open fun setStorageLimit(storageLimit: Long) { + _globalStorageStats.value = _globalStorageStats.value?.copy(storageLimit = storageLimit) + } + + suspend fun updateStorageStats() { + travelViewModel.getTravels().await().map { travel -> + getTravelStorageStats(travel) + }.let { setStorageStats(it) } + } + + private suspend fun getTravelStorageStats(travel: TravelContainer): TravelStorageStats { + val collectionPath = FirebasePaths.constructPath(FirebasePaths.TravelsSuperCollection, travel.fsUid, FirebasePaths.documents) + val documents = documentViewModel.getDocuments(collectionPath).await() + val offlineDocuments = documentsManager.getCachedDocuments(documents) + + val storageUsed: Long = offlineDocuments.filter { it.uri != null } + .foldRight(0L) { document, acc -> acc + document.document.fileSize } + val storageMax: Long = offlineDocuments.foldRight(0L) { document, acc -> acc + document.document.fileSize } + + return TravelStorageStats( + travel = travel, + storageUsed = storageUsed, + storageMax = storageMax, + storageReclaimable = (0.1f * storageUsed).toLong(), + ) } } diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt index 71d703da..8b9a0ccc 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.github.se.travelpouch.model.profile.Profile import com.google.firebase.firestore.DocumentReference import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CompletableDeferred import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -109,20 +110,32 @@ open class ListTravelViewModel @Inject constructor(private val repository: Trave }) } - /** Gets all Travel documents. */ - fun getTravels() { + /** + * Fetches all travel documents. + * + * This function queries the repository to fetch all travel documents. If the fetch is successful, + * the `travels_` state is updated with the fetched travel documents. If an error occurs, the + * `isLoading` state is updated to `false`, and an error message is logged. + * + * @return A `CompletableDeferred` object that completes with the list of fetched travel documents. + */ + fun getTravels(): CompletableDeferred> { + val ret = CompletableDeferred>() Log.d("ListTravelViewModel", "Getting travels") _isLoading.value = true repository.getTravels( onSuccess = { Log.d("ListTravelViewModel", "Successfully got travels") + ret.complete(it) travels_.value = it _isLoading.value = false }, onFailure = { + ret.completeExceptionally(it) _isLoading.value = false Log.e("ListTravelViewModel", "Failed to get travels", it) }) + return ret } /** diff --git a/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt index 7314277e..957378c2 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/authentication/SignIn.kt @@ -203,7 +203,7 @@ fun SignInScreen( isLoading.value = true methodChosen.value = true - currentUser.getIdToken(true).await() // We shouldn't continue until this passes + // currentUser.getIdToken(true).await() // We shouldn't continue until this passes Log.d( "SignInScreen", diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt index b90b5a77..5155a7a4 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt @@ -70,7 +70,9 @@ import androidx.compose.ui.unit.em import androidx.compose.ui.window.Dialog import com.github.se.travelpouch.model.home.StorageDashboardStats import com.github.se.travelpouch.model.home.StorageDashboardViewModel +import com.github.se.travelpouch.model.home.TravelStorageStats import com.github.se.travelpouch.model.home.formatStorageUnit +import com.github.se.travelpouch.model.travels.TravelContainer import com.github.se.travelpouch.ui.navigation.NavigationActions import kotlin.math.min import kotlin.math.roundToLong @@ -85,12 +87,13 @@ fun StorageDashboard( navigationActions: NavigationActions ) { val isLoading by storageDashboardViewModel.isLoading.collectAsState() - val storageStats by storageDashboardViewModel.storageStats.collectAsState() + val travelStorageStats by storageDashboardViewModel.travelStorageStats.collectAsState() + val globalStorageStats by storageDashboardViewModel.globalStorageStats.collectAsState() val storageLimitDialogOpened = remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(1500) - if (storageStats == null) { + if (travelStorageStats.isEmpty()) { storageDashboardViewModel.updateStorageStats() } } @@ -122,11 +125,10 @@ fun StorageDashboard( if (storageLimitDialogOpened.value) { StorageLimitDialog( onDismissRequest = { storageLimitDialogOpened.value = false }, - currentLimit = storageStats?.storageLimit ?: 0, - usedStorage = storageStats?.storageUsed ?: 0, + currentLimit = globalStorageStats?.storageLimit ?: 0, + usedStorage = globalStorageStats?.storageUsed ?: 0, onUpdate = { - storageDashboardViewModel.setStorageStats( - storageStats!!.copy(storageLimit = it)) + storageDashboardViewModel.setStorageLimit(it) storageLimitDialogOpened.value = false }) } @@ -136,7 +138,7 @@ fun StorageDashboard( AnimatedVisibility(!isLoading, enter = fadeIn(), exit = fadeOut()) { Row(modifier = Modifier.padding(8f.dp)) { Text( - "Storage limit: ${storageStats?.storageLimitToString() ?: "..."}", + "Storage limit: ${globalStorageStats?.storageLimitToString() ?: "..."}", modifier = Modifier.testTag("storageLimitCardText")) } } @@ -144,7 +146,7 @@ fun StorageDashboard( } Spacer(Modifier.height(10.dp)) Row(modifier = Modifier.fillMaxWidth(0.6f).align(Alignment.CenterHorizontally)) { - CircularStorageDiagram(storageStats) + CircularStorageDiagram(globalStorageStats) } Spacer(Modifier.height(20.dp)) @@ -160,10 +162,9 @@ fun StorageDashboard( } } } - - items(if (storageStats == null) 0 else 10) { index -> - TravelCard(index) - + items(travelStorageStats.size) { index -> + val stats = travelStorageStats[index] + TravelStorageCard(stats) Spacer(Modifier.height(10.dp)) } } @@ -241,7 +242,7 @@ fun CircularStorageDiagram( style = Stroke(strokeWidth, cap = StrokeCap.Round)) val valueAngle = currentValue.value * 360f - val indicatorAngle = currentIndicator.value * 360f + val indicatorAngle = if (currentIndicator.value > 0.1) currentIndicator.value * 360f else 0f val effectiveIndicatorAngle = min(indicatorAngle, valueAngle) drawArc( @@ -274,21 +275,25 @@ fun CircularStorageDiagram( fontSize = 7.em, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center) - Text( - "${formatStorageUnit(((stats?.storageAvailable() ?: 0) * currentValueScale.value).roundToLong())} available", + val availableBytes = stats?.storageAvailable() ?: -1 + if (availableBytes >= 0) { + Text( + "${formatStorageUnit((availableBytes * currentValueScale.value).roundToLong())} available", fontSize = 4.em, - textAlign = TextAlign.Center) + textAlign = TextAlign.Center + ) + } } } } @Composable -fun TravelCard(index: Int) { +fun TravelStorageCard(stats: TravelStorageStats) { Card(Modifier.fillMaxWidth()) { Row(Modifier.padding(8.dp)) { - Text("Travel $index", Modifier.align(Alignment.CenterVertically)) + Text(stats.travel.title, Modifier.align(Alignment.CenterVertically)) Spacer(Modifier.fillMaxWidth().weight(0.1f)) - Text("${(index+1) * 244918795735983 % 27471} MiB", Modifier.align(Alignment.CenterVertically)) + Text(stats.storageUsageToString(), Modifier.align(Alignment.CenterVertically)) IconButton(onClick = {}, modifier = Modifier.testTag("editLimitButton")) { Icon(imageVector = Icons.Default.Delete, contentDescription = null) } diff --git a/cloud-functions/functions/src/index.ts b/cloud-functions/functions/src/index.ts index 442df197..85343fb7 100644 --- a/cloud-functions/functions/src/index.ts +++ b/cloud-functions/functions/src/index.ts @@ -57,7 +57,7 @@ export const storeDocument = onCall( ); export const generateThumbnailCall = onCall( - {region: "europe-west9", memory: "1GiB"}, + {region: "europe-west9", memory: "4GiB"}, async (req) => { if (!req.data.travelId || !req.data.documentId || !req.data.width) { throw new HttpsError("invalid-argument", "Missing parameters"); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8aaa622a..231ca5da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,6 +78,7 @@ playServicesMlkitDocumentScanner = "16.0.0-beta1" coil = "2.1.0" bouquet = "1.1.2" playServicesLocation = "21.3.0" +datastoreCoreAndroid = "1.1.1" [libraries] androidx-concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrentFutures" } @@ -169,6 +170,7 @@ play-services-location = { group = "com.google.android.gms", name = "play-servic #Permissions accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version = "0.30.0" } +androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastoreCoreAndroid" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From b6cb1598c785c63413f62fed48fa34521dddd0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20N=C3=A9risson?= Date: Fri, 20 Dec 2024 08:18:40 +0100 Subject: [PATCH 2/3] feat: add batch downloading in storage dashboard --- .../com/github/se/travelpouch/MainActivity.kt | 2 +- .../model/documents/DocumentRepository.kt | 2 + .../model/documents/DocumentViewModel.kt | 14 +- .../model/documents/DocumentsManager.kt | 44 ++++ .../model/home/StorageDashboardViewModel.kt | 31 +++ .../travelpouch/ui/home/StorageDashboard.kt | 194 +++++++++++++++--- 6 files changed, 255 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt index 1f9f79fc..fe3f708b 100644 --- a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt @@ -187,7 +187,7 @@ class MainActivity : ComponentActivity() { } composable(Screen.STORAGE) { - StorageDashboard(storageDashboardViewModel, navigationActions) + StorageDashboard(storageDashboardViewModel, documentViewModel, navigationActions) } composable(Screen.ONBOARDING) { OnboardingScreen(navigationActions, profileModelView) } } diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt index 1b72b7f2..96693d27 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt @@ -1,6 +1,8 @@ // Portions of this code were generated and or inspired by the help of GitHub Copilot or Chatgpt package com.github.se.travelpouch.model.documents +import android.util.Base64InputStream +import android.util.Base64OutputStream import android.util.Log import com.github.se.travelpouch.model.FirebasePaths import com.github.se.travelpouch.model.activity.Activity diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt index 0db65cfe..ff776ca4 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt @@ -86,12 +86,13 @@ constructor( } /** - * Downloads a Document from Firebase store adn store it in the folder pointed by documentFile + * Downloads a Document from Firebase store and store it in the folder pointed by documentFile * * @param documentFile The folder in which to create the file */ @OptIn(ExperimentalCoroutinesApi::class) - fun getSelectedDocument(documentFile: DocumentFile) { + fun getSelectedDocument(documentFile: DocumentFile): CompletableDeferred { + val ret = CompletableDeferred() _documentUri.value = null val mimeType = selectedDocument.value?.fileFormat?.mimeType val title = selectedDocument.value?.title @@ -105,11 +106,20 @@ constructor( result.invokeOnCompletion { if (it != null) { Log.e("DocumentViewModel", "Failed to download document", it) + ret.completeExceptionally(it) } else { + ret.complete(result.getCompleted()) _documentUri.value = result.getCompleted() Log.d("DocumentViewModel", "Document retrieved as ${result.getCompleted()}") } } + return ret + } + + fun removeDocumentFromCache(documentUid: String, uri: Uri): Deferred { + return CoroutineScope(Dispatchers.IO).async { + documentsManager.deleteDocumentFromCache(documentUid, uri) + } } /** diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt index d07acc94..5a943c6b 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt @@ -9,6 +9,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.documentfile.provider.DocumentFile +import com.github.se.travelpouch.model.travels.TravelContainer import com.google.firebase.functions.FirebaseFunctions import com.google.firebase.storage.FirebaseStorage import com.google.firebase.storage.StreamDownloadTask @@ -78,6 +79,49 @@ open class DocumentsManager( } } + /** + * Delete a document from the cache. + * @param documentUid The reference of the document to delete + * @return true if the document was deleted, false otherwise + */ + suspend fun deleteDocumentFromCache(documentUid: String, uri: Uri): Boolean { + val documentUri = documentInCacheOrNull(documentUid) + if (documentUri == null) { + Log.e(tag, "Document not found in cache, deletion aborted!") + return false + } +// if (documentUri != uri) { +// Log.e(tag, "Discrepancy between cache and storage, deletion aborted!") +// return false +// } + + val deletedRows = contentResolver.delete(documentUri, null, null) + if (deletedRows > 0) { + dataStore.edit { preferences -> preferences.remove(stringPreferencesKey(documentUid)) } + } + return deletedRows > 0 + } + + /** + * Delete all documents from the cache for this travel. + * @param travel The reference of the travel to delete the locally stored documents + * @return true if the document was deleted, false otherwise + */ +// suspend fun deleteTravelDocumentsFromCache(travel: TravelContainer): Boolean { +// +// +// val travelDocuments = preferences.filterKeys { it.contains(travel) } +// travelDocuments.forEach { (key, value) -> +// deleteDocumentFromCache(key, Uri.parse(value)) +// } +// +// return true +// } + + suspend fun downloadAllDocumentsForTravel(travel: TravelContainer) { + + } + /** * Return a Uri pointing to the file in the local storage if it is already present. * diff --git a/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt index 0fd2dc96..2ca691c6 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/home/StorageDashboardViewModel.kt @@ -3,8 +3,10 @@ package com.github.se.travelpouch.model.home import androidx.lifecycle.ViewModel import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.documents.DocumentContainer import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.model.documents.DocumentsManager +import com.github.se.travelpouch.model.documents.OfflineDocumentContainer import com.github.se.travelpouch.model.travels.ListTravelViewModel import com.github.se.travelpouch.model.travels.TravelContainer import dagger.hilt.android.lifecycle.HiltViewModel @@ -32,6 +34,7 @@ fun formatStorageUnit(number: Long, hideUnit: Boolean? = null): String { data class TravelStorageStats( val travel: TravelContainer, + val documents: List, val storageUsed: Long, val storageMax: Long, val storageReclaimable: Long, @@ -45,6 +48,7 @@ data class TravelStorageStats( fun merge(other: TravelStorageStats): TravelStorageStats { return TravelStorageStats( travel = travel, + documents = documents + other.documents, storageUsed = storageUsed + other.storageUsed, storageMax = storageMax + other.storageMax, storageReclaimable = storageReclaimable + other.storageReclaimable, @@ -66,8 +70,28 @@ data class TravelStorageStats( } fun storageUsageToString(): String { + if (storageUsed == storageMax) { + return formatStorageUnit(storageUsed) + } return "${formatStorageUnit(storageUsed, hideUnit = true)} / ${formatStorageUnit(storageMax)}" } + + fun documentsCountToString(): String { + val offlineDocs = documents.filter { it.uri != null }.size + val totalDocs = documents.size + if (offlineDocs == totalDocs) { + return "$offlineDocs docs" + } + return "$offlineDocs/$totalDocs docs" + } + + fun getMissingDocuments(): List { + return documents.filter { it.uri == null }.map { it.document } + } + + fun getDownloadedDocuments(): List { + return documents.filter { it.uri != null }.map { it.document } + } } data class StorageDashboardStats( @@ -147,9 +171,16 @@ open class StorageDashboardViewModel @Inject constructor(private val documentsMa return TravelStorageStats( travel = travel, + documents = offlineDocuments, storageUsed = storageUsed, storageMax = storageMax, storageReclaimable = (0.1f * storageUsed).toLong(), ) } + +// open fun downloadAllDocumentsForTravel(travel: TravelContainer) { +// documentsManager.downloadAllDocumentsForTravel(travel).onSuccess { +// updateStorageStats() +// } +// } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt index 5155a7a4..ab293c74 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt @@ -2,6 +2,12 @@ package com.github.se.travelpouch.ui.home import android.annotation.SuppressLint +import android.content.Intent +import android.util.Log +import android.widget.ProgressBar +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing @@ -30,11 +36,16 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.DownloadDone +import androidx.compose.material.icons.filled.DownloadForOffline +import androidx.compose.material.icons.filled.Downloading import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton @@ -61,6 +72,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -68,12 +80,18 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.window.Dialog +import androidx.datastore.preferences.core.edit +import androidx.documentfile.provider.DocumentFile +import com.github.se.travelpouch.model.documents.DocumentContainer +import com.github.se.travelpouch.model.documents.DocumentViewModel import com.github.se.travelpouch.model.home.StorageDashboardStats import com.github.se.travelpouch.model.home.StorageDashboardViewModel import com.github.se.travelpouch.model.home.TravelStorageStats import com.github.se.travelpouch.model.home.formatStorageUnit import com.github.se.travelpouch.model.travels.TravelContainer import com.github.se.travelpouch.ui.navigation.NavigationActions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlin.math.min import kotlin.math.roundToLong import kotlinx.coroutines.delay @@ -83,19 +101,38 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun StorageDashboard( - storageDashboardViewModel: StorageDashboardViewModel, - navigationActions: NavigationActions + storageDashboardViewModel: StorageDashboardViewModel, + documentViewModel: DocumentViewModel, + navigationActions: NavigationActions ) { val isLoading by storageDashboardViewModel.isLoading.collectAsState() val travelStorageStats by storageDashboardViewModel.travelStorageStats.collectAsState() val globalStorageStats by storageDashboardViewModel.globalStorageStats.collectAsState() val storageLimitDialogOpened = remember { mutableStateOf(false) } + val context = LocalContext.current + val openDirectoryLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocumentTree()) { + if (it != null) { + val flagsPermission: Int = + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + try { + context.contentResolver.takePersistableUriPermission(it, flagsPermission) + } catch (e: Exception) { + Toast.makeText(context, "Failed to access directory", Toast.LENGTH_SHORT).show() + return@rememberLauncherForActivityResult + } + val documentFile = DocumentFile.fromTreeUri(context, it) + if (documentFile != null) { + documentViewModel.setSaveDocumentFolder(it) + documentViewModel.getSelectedDocument(documentFile) + } + } + } + LaunchedEffect(Unit) { delay(1500) - if (travelStorageStats.isEmpty()) { - storageDashboardViewModel.updateStorageStats() - } + storageDashboardViewModel.updateStorageStats() } Scaffold( @@ -114,14 +151,23 @@ fun StorageDashboard( }) }, ) { pd -> - LazyColumn(Modifier.fillMaxSize().padding(pd).padding(horizontal = 10.dp)) { + LazyColumn( + Modifier + .fillMaxSize() + .padding(pd) + .padding(horizontal = 10.dp)) { item { Column( - modifier = Modifier.fillMaxSize().animateItem(), + modifier = Modifier + .fillMaxSize() + .animateItem(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, ) { - Row(Modifier.padding(10.dp).align(Alignment.End)) { + Row( + Modifier + .padding(10.dp) + .align(Alignment.End)) { if (storageLimitDialogOpened.value) { StorageLimitDialog( onDismissRequest = { storageLimitDialogOpened.value = false }, @@ -145,13 +191,18 @@ fun StorageDashboard( } } Spacer(Modifier.height(10.dp)) - Row(modifier = Modifier.fillMaxWidth(0.6f).align(Alignment.CenterHorizontally)) { + Row(modifier = Modifier + .fillMaxWidth(0.6f) + .align(Alignment.CenterHorizontally)) { CircularStorageDiagram(globalStorageStats) } Spacer(Modifier.height(20.dp)) AnimatedVisibility(!isLoading, enter = fadeIn(), exit = fadeOut()) { - Row(Modifier.align(Alignment.Start).padding(10.dp)) { + Row( + Modifier + .align(Alignment.Start) + .padding(10.dp)) { Text( "Storage usage by travel", fontSize = 5.em, @@ -164,7 +215,48 @@ fun StorageDashboard( } items(travelStorageStats.size) { index -> val stats = travelStorageStats[index] - TravelStorageCard(stats) + TravelStorageCard(stats, { + // download all + CoroutineScope(Dispatchers.IO).launch { + Log.d("StorageDashboard", "Downloading all documents for travel ${stats.travel.title}") + val documentFileUri = documentViewModel.getSaveDocumentFolder().await() + if (documentFileUri == null) { + openDirectoryLauncher.launch(null) + return@launch + } + stats.getMissingDocuments().forEach { document -> + Log.d("StorageDashboard", "Downloading document ${document.title}") + documentViewModel.selectDocument(document) + val documentFile = DocumentFile.fromTreeUri(context, documentFileUri) + if (documentFile != null) { + Log.d("StorageDashboard", "Downloading document ${document.title} to $documentFileUri") + documentViewModel.getSelectedDocument(documentFile).await() + Log.d("StorageDashboard", "Downloaded document ${document.title}") + } + } + storageDashboardViewModel.updateStorageStats() + } + }, { + // delete all + Toast.makeText(context, "Feature not yet implemented", Toast.LENGTH_SHORT).show() +// CoroutineScope(Dispatchers.IO).launch { +// Log.d("StorageDashboard", "Deleting all documents for travel ${stats.travel.title}") +// val documentFileUri = documentViewModel.getSaveDocumentFolder().await() +// if (documentFileUri == null) { +// openDirectoryLauncher.launch(null) +// return@launch +// } +// stats.getDownloadedDocuments().forEach { document -> +// Log.d("StorageDashboard", "Deleting document ${document.title}") +// val documentFile = DocumentFile.fromTreeUri(context, documentFileUri) +// if (documentFile != null) { +// Log.d("StorageDashboard", "Deleting document ${document.title} from $documentFileUri") +// documentViewModel.removeDocumentFromCache(document.ref.id, documentFile.uri).await() +// Log.d("StorageDashboard", "Deleted document ${document.title}") +// } +// } +// } + }) Spacer(Modifier.height(10.dp)) } } @@ -225,7 +317,9 @@ fun CircularStorageDiagram( Box { Column(modifier = Modifier.align(Alignment.Center)) { - Canvas(modifier = Modifier.aspectRatio(1f).fillMaxSize()) { + Canvas(modifier = Modifier + .aspectRatio(1f) + .fillMaxSize()) { // Define the size of the circle and the padding for text val size = size.minDimension val strokeWidth = size * 0.10f // Stroke width of the arcs @@ -288,15 +382,52 @@ fun CircularStorageDiagram( } @Composable -fun TravelStorageCard(stats: TravelStorageStats) { +fun TravelStorageCard(stats: TravelStorageStats, onDownloadAll: (TravelContainer) -> Unit, onDeleteAll: (TravelContainer) -> Unit) { + val ratio = stats.storageUsed.toFloat() / stats.storageMax.toFloat() + val currentProgressValue = remember { Animatable(0f) } + val isLoading = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + launch { + val v = stats.storageUsed.toFloat() / stats.storageMax.toFloat() + if (v.isNaN()) { + isLoading.value = true + } + currentProgressValue.animateTo( + targetValue = if (v.isNaN()) 0f else v, + animationSpec = tween(durationMillis = 1500, easing = FastOutSlowInEasing)) + } + } + Card(Modifier.fillMaxWidth()) { Row(Modifier.padding(8.dp)) { - Text(stats.travel.title, Modifier.align(Alignment.CenterVertically)) - Spacer(Modifier.fillMaxWidth().weight(0.1f)) - Text(stats.storageUsageToString(), Modifier.align(Alignment.CenterVertically)) - IconButton(onClick = {}, modifier = Modifier.testTag("editLimitButton")) { + Column { + Text(stats.travel.title, Modifier.align(Alignment.Start), fontWeight = FontWeight.Bold, fontSize = 4.em) + Text("${stats.storageUsageToString()} - ${stats.documentsCountToString()}", Modifier.align(Alignment.Start)) + } + + Spacer( + Modifier + .fillMaxWidth() + .weight(0.1f)) + + IconButton(onClick = { + onDeleteAll(stats.travel) + }, modifier = Modifier.testTag("travelStorageCardDeleteButton"), enabled = ratio > 0) { Icon(imageVector = Icons.Default.Delete, contentDescription = null) } + IconButton(onClick = { + onDownloadAll(stats.travel) + }, modifier = Modifier.testTag("travelStorageCardDownloadButton"), enabled = ratio < 1) { + Icon(imageVector = Icons.Default.DownloadForOffline, contentDescription = null) + } + } + Row { + LinearProgressIndicator( + progress = { if (isLoading.value) 0f else currentProgressValue.value }, + modifier = Modifier + .fillMaxWidth() + .height(5.dp), + ) } } } @@ -320,12 +451,15 @@ fun StorageLimitDialog( LaunchedEffect(Unit) { focusRequester.requestFocus() } Box( modifier = - Modifier.fillMaxWidth(1f) - .height(250.dp) - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.surface) - .testTag("storageLimitDialogBox")) { - Column(modifier = Modifier.padding(10.dp).fillMaxSize()) { + Modifier + .fillMaxWidth(1f) + .height(250.dp) + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.surface) + .testTag("storageLimitDialogBox")) { + Column(modifier = Modifier + .padding(10.dp) + .fillMaxSize()) { Text( "Set storage limit", Modifier.align(Alignment.CenterHorizontally), @@ -340,9 +474,10 @@ fun StorageLimitDialog( TextField( value = text.value, modifier = - Modifier.weight(0.5f) - .focusRequester(focusRequester) - .testTag("storageLimitDialogTextField"), + Modifier + .weight(0.5f) + .focusRequester(focusRequester) + .testTag("storageLimitDialogTextField"), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), onValueChange = { text.value = it }, label = { Text("New limit") }) @@ -370,9 +505,10 @@ fun StorageLimitDialog( }, enabled = validInput, modifier = - Modifier.fillMaxWidth() - .padding(10.dp) - .testTag("storageLimitDialogSaveButton")) { + Modifier + .fillMaxWidth() + .padding(10.dp) + .testTag("storageLimitDialogSaveButton")) { Text("Update") } } From 1a94034573572ba8dd88465f33a1c8518542e413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20N=C3=A9risson?= Date: Fri, 20 Dec 2024 08:27:44 +0100 Subject: [PATCH 3/3] chore: fix merge conflict --- .../model/documents/DocumentViewModel.kt | 15 +++++------- .../model/documents/DocumentsManager.kt | 24 +------------------ 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt index e5b1ed3d..5715d078 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt @@ -108,7 +108,7 @@ constructor( result.invokeOnCompletion { when (it) { null -> { - ret.completeExceptionally(it) + ret.complete(result.getCompleted()) _documentUri.value = result.getCompleted() Log.d("DocumentViewModel", "Document retrieved as ${result.getCompleted()}") } @@ -116,21 +116,18 @@ constructor( Log.i( "DocumentViewModel", "Failed to create document file in same directory. Re-asking permission") - ret.complete(result.getCompleted()) + ret.completeExceptionally(it) resetSaveDocumentFolder().invokeOnCompletion { _needReload.value = true } } - else -> Log.e("DocumentViewModel", "Failed to download document", it) + else -> { + Log.e("DocumentViewModel", "Failed to download document", it) + ret.completeExceptionally(it) + } } } return ret } - fun removeDocumentFromCache(documentUid: String, uri: Uri): Deferred { - return CoroutineScope(Dispatchers.IO).async { - documentsManager.deleteDocumentFromCache(documentUid, uri) - } - } - /** * Deletes a Document. * diff --git a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt index 3149c9c0..4d86c21d 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/documents/DocumentsManager.kt @@ -84,16 +84,12 @@ open class DocumentsManager( * @param documentUid The reference of the document to delete * @return true if the document was deleted, false otherwise */ - suspend fun deleteDocumentFromCache(documentUid: String, uri: Uri): Boolean { + suspend fun deleteDocumentFromCache(documentUid: String): Boolean { val documentUri = documentInCacheOrNull(documentUid) if (documentUri == null) { Log.e(tag, "Document not found in cache, deletion aborted!") return false } -// if (documentUri != uri) { -// Log.e(tag, "Discrepancy between cache and storage, deletion aborted!") -// return false -// } val deletedRows = contentResolver.delete(documentUri, null, null) if (deletedRows > 0) { @@ -102,25 +98,7 @@ open class DocumentsManager( return deletedRows > 0 } - /** - * Delete all documents from the cache for this travel. - * @param travel The reference of the travel to delete the locally stored documents - * @return true if the document was deleted, false otherwise - */ -// suspend fun deleteTravelDocumentsFromCache(travel: TravelContainer): Boolean { -// -// -// val travelDocuments = preferences.filterKeys { it.contains(travel) } -// travelDocuments.forEach { (key, value) -> -// deleteDocumentFromCache(key, Uri.parse(value)) -// } -// -// return true -// } - - suspend fun downloadAllDocumentsForTravel(travel: TravelContainer) { - } /** * Return a Uri pointing to the file in the local storage if it is already present.