Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: data integration of the storage dashboard #277

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/github/se/travelpouch/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<StorageDashboardViewModel>()
val storageDashboardViewModel = hiltViewModel<StorageDashboardViewModel>()

NavHost(navController = navController, startDestination = Route.DEFAULT) {
navigation(
Expand Down Expand Up @@ -187,7 +187,7 @@ class MainActivity : ComponentActivity() {
}

composable(Screen.STORAGE) {
StorageDashboard(storageDashboardViewModel, navigationActions)
StorageDashboard(storageDashboardViewModel, documentViewModel, navigationActions)
}
composable(Screen.ONBOARDING) { OnboardingScreen(navigationActions, profileModelView) }
}
Expand Down
22 changes: 21 additions & 1 deletion app/src/main/java/com/github/se/travelpouch/di/AppModules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ 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
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
Expand Down Expand Up @@ -101,7 +103,7 @@ object AppModule {

@Provides
@Singleton
fun provideFileDownloader(
fun provideFileManager(
@ApplicationContext context: Context,
storage: FirebaseStorage,
functions: FirebaseFunctions,
Expand Down Expand Up @@ -139,4 +141,22 @@ object AppModule {
return PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile("documents") })
}

@Provides
@Singleton
fun provideDocumentViewModel(
repository: DocumentRepository,
documentsManager: DocumentsManager,
dataStore: DataStore<Preferences>
): DocumentViewModel {
return DocumentViewModel(repository, documentsManager, dataStore)
}

@Provides
@Singleton
fun provideListTravelViewModel(
repository: TravelRepository
): ListTravelViewModel {
return ListTravelViewModel(repository)
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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?
)
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,7 +18,7 @@ import com.google.firebase.storage.FirebaseStorage
interface DocumentRepository {
fun setIdTravel(onSuccess: () -> Unit, travelId: String)

fun getDocuments(onSuccess: (List<DocumentContainer>) -> Unit, onFailure: (Exception) -> Unit)
fun getDocuments(onSuccess: (List<DocumentContainer>) -> Unit, onFailure: (Exception) -> Unit, collectionPathOverride: String? = null)

fun deleteDocumentById(
document: DocumentContainer,
Expand Down Expand Up @@ -63,9 +65,10 @@ class DocumentRepositoryFirestore(
*/
override fun getDocuments(
onSuccess: (List<DocumentContainer>) -> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,20 +67,34 @@ 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<List<DocumentContainer>> {
val ret = CompletableDeferred<List<DocumentContainer>>()
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
}

/**
* 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<Uri> {
val ret = CompletableDeferred<Uri>()
_documentUri.value = null
val mimeType = selectedDocument.value?.fileFormat?.mimeType
val title = selectedDocument.value?.title
Expand All @@ -93,18 +108,24 @@ constructor(
result.invokeOnCompletion {
when (it) {
null -> {
ret.complete(result.getCompleted())
_documentUri.value = result.getCompleted()
Log.d("DocumentViewModel", "Document retrieved as ${result.getCompleted()}")
}
is DocumentsManager.FileCreationException -> {
Log.i(
"DocumentViewModel",
"Failed to create document file in same directory. Re-asking permission")
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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ 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
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
Expand All @@ -23,13 +23,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<Preferences>,
private val thumbsDirectory: File
private val contentResolver: ContentResolver,
private val storage: FirebaseStorage,
private val functions: FirebaseFunctions,
private val dataStore: DataStore<androidx.datastore.preferences.core.Preferences>,
private val thumbsDirectory: File
) {
private val tag = DocumentsManager::class.java.simpleName

Expand Down Expand Up @@ -79,6 +79,27 @@ 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): Boolean {
val documentUri = documentInCacheOrNull(documentUid)
if (documentUri == null) {
Log.e(tag, "Document not found in cache, deletion aborted!")
return false
}

val deletedRows = contentResolver.delete(documentUri, null, null)
if (deletedRows > 0) {
dataStore.edit { preferences -> preferences.remove(stringPreferencesKey(documentUid)) }
}
return deletedRows > 0
}



/**
* Return a Uri pointing to the file in the local storage if it is already present.
*
Expand All @@ -90,17 +111,48 @@ open class DocumentsManager(
val pathFlow: Flow<String?> = 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<DocumentContainer>): List<OfflineDocumentContainer> {
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.
*
Expand Down
Loading
Loading