Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
23defe7
refactor: add string res for WorkBookScreen
Mar 25, 2022
7833cdb
refactor: create WorkBookScreen
Mar 25, 2022
d69578d
refactor(TakeTestScrenNavigation): add nav controller param & WorkBoo…
Mar 25, 2022
c11408a
fix(TakeTestScreenNavigation): pass the mcq list associated with curr…
Mar 25, 2022
03b18a2
refactor: add call to moveToNextWorkBook() after navigating to workBo…
Mar 26, 2022
1fbf797
refactor(WorkBookScreen): use string res for footer button
Mar 26, 2022
708d9dc
refactor(WorkBookScreen): add nav arrow icon to listFooter button
Mar 26, 2022
33fe155
feat(TakeTestScreenNavigation): add logic to not allow user to use ba…
Mar 26, 2022
6ab2bca
refactor(WorkBookScreen)!: make listFooter param as an internal compo…
Mar 26, 2022
1e65523
refactor(WorkBookScreen): add buttonTextValue optional enum param
Mar 26, 2022
2ed8d7f
refactor(WorkBookScreen): use string res for ButtonTextValue.FINISH_TEST
Mar 26, 2022
2f787a8
refactor(WorkBookScreen): add alignment to button content
Mar 27, 2022
66ef49f
refactor(WorkBookScreen): use mcq obj as key for currentlySelectedIndex
Mar 27, 2022
d2721cb
refactor(WorkBookScreen)!: add onFooterButtonClick param
Mar 27, 2022
da6a8a4
refactor: pop backstack when footerbutton of WorkBookScreen is clicked
Mar 27, 2022
c81ffd2
refactor: add UserAnsers domain object
Mar 27, 2022
f4d7078
refactor!: pass workbook id as nav arg when navigating to WorkBookScreen
Mar 27, 2022
94437a3
refactor(UserAnswers): create/add IndexOfChosenOption value class
Mar 28, 2022
e3ee1db
refactor: create UserAnswersDTO
Mar 28, 2022
c715e6f
refactor(UserAnswers): add toUserAnswersDTO extension method
Mar 28, 2022
82d1206
refactor(IndexOfChosenOption): override toString of value class
Mar 28, 2022
8545067
refactor: rename 'id' to 'multichoiceId' when converting UserAnswers-…
Mar 28, 2022
72aa64f
refactor: add room dependecies
Mar 28, 2022
d4c8434
refactor: add UserAnswersEntity
Mar 28, 2022
6bc8a71
refactor: create UserAnswersEntityDao
Mar 28, 2022
269be57
refactor: create ExamerDatabase abstract class
Mar 28, 2022
dd24e2a
refactor(UserAnswersEntityDao): rename getUserAnswersEntityList->getA…
Mar 28, 2022
4e6948c
refactor(AppContainer): create database and get the dao
Mar 28, 2022
f38a7a1
refactor(Repository)!: add userAnswersEntityDao contructor param
Mar 28, 2022
7d33b94
fix: use kapt instead of annotationProcessor for Room
Mar 29, 2022
1bd0249
fix: change Uri/LocalDateTime serilizers to object
Mar 29, 2022
00a2c0d
fix(UserAnswersEntity): change 'id' to room supported primary key dty…
Mar 29, 2022
504629b
fix: add room-ktx dependency to add coroutines support
Mar 29, 2022
2ba0500
Merge pull request #20 from t3chkid/room-db
technophilist Mar 29, 2022
a97cecb
refactor(UserAnswers): add toUserAnswersEntityList extension method
Mar 29, 2022
5666f66
refactor(Repository)!: add/impl saveUserAnswers method
Mar 29, 2022
fc8dfe1
revert!: all commits releated to local db
Mar 30, 2022
ace8e10
refactor(FirebaseRemoteDatabase): replace scheduledTestsCollectionPat…
Mar 30, 2022
9a65ef1
refactor(FirebaseRemoteDatabase): use whereIn query to filter complet…
Mar 30, 2022
de8df1f
Merge branch 'firestore-collection-rewrite' into create-workbook-screen
Mar 30, 2022
7707034
refactor(FirebaseRemoteDatabase): add 'runOnCollection' optional para…
Mar 30, 2022
97f71a5
refactor(RemoteDatabase)!: add/impl saveUserAnswers()
Mar 30, 2022
5e0e9ef
refactor(Repository)!: add/impl saveUserAnswersForUser()
Mar 30, 2022
1749794
refactor: create ExamerWorkBookViewModel & assoc factory
Mar 30, 2022
dd67a07
refactor!: pass testDetailsId when navigating to WorkBookScreen
Mar 30, 2022
873a970
refactor: add workBookViewModelFactory to AppContainer
Mar 30, 2022
39ceb44
refactor: add workBookViewModelFactory param to workBookScreenComposable
Mar 30, 2022
55081ec
fix: test_details_id_arg const having the same value as another const
Mar 30, 2022
a503991
refactor: add work-manager dependency
Mar 30, 2022
ca35ee1
refactor(WorkBookViewModel)!: add 'application' constructor param
Mar 30, 2022
a251224
refactor(AppContainer): make repository public
Mar 30, 2022
1bb685b
refactor: create SaveUserAnswersWorker
Mar 30, 2022
119d14a
refactor(WorkBookViewModel): use SaveUserAnswersWorker to save user a…
Mar 30, 2022
d9e1cc8
refactor(WorkBookViewModel)!: remove redundant constructor params
Mar 30, 2022
174c286
fix: set allowStructuredMapKeys to true when encoding userAnswers object
Apr 1, 2022
1b684a7
fix: mark UserAnswers/IndexOfChosenOption classes with @Serializable
Apr 1, 2022
672f5ac
fix(SaveUserAnswersWorker):change constructor param of type "Applicat…
Apr 1, 2022
804ce55
refactor(WorkBookViewModel): remove call to setExpedited() for WorkRe…
Apr 1, 2022
30f1da5
fix(SaveUserAnswersWorker): set allowStructuredMapKeys to true while …
Apr 1, 2022
d1c9c7c
Revert "fix(SaveUserAnswersWorker): set allowStructuredMapKeys to tru…
Apr 1, 2022
28be318
fix(SaveUserAnswersWorker): set allowStructuredMapKeys to true while …
Apr 1, 2022
4637944
refactor(WorkBookScreen): change dtype Int->IndexOfChosenOption in on…
Apr 1, 2022
488507f
feat(TakeTestNavigation): use workbook viewmodel to save user answers
Apr 1, 2022
1b89b66
feat(WorkBookScreen): conditionally enable footer button
Apr 1, 2022
79d59e3
fix(WorkBookScreen): add additional 8dp padding to top of footer button
Apr 1, 2022
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,7 @@ dependencies {
implementation "androidx.security:security-crypto:1.0.0"
// serialization
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0"
// work manager
implementation "androidx.work:work-runtime-ktx:2.7.1"

}
20 changes: 16 additions & 4 deletions app/src/main/java/com/example/examer/data/Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.net.toUri
import com.example.examer.data.domain.ExamerAudioFile
import com.example.examer.data.domain.ExamerUser
import com.example.examer.data.domain.TestDetails
import com.example.examer.data.domain.WorkBook
import com.example.examer.data.domain.*
import com.example.examer.data.dto.AudioFileDTO
import com.example.examer.data.dto.WorkBookDTO
import com.example.examer.data.dto.toMultiChoiceQuestion
Expand All @@ -27,6 +24,12 @@ interface Repository {
user: ExamerUser,
testDetails: TestDetails
): Result<List<WorkBook>>

suspend fun saveUserAnswersForUser(
user: ExamerUser,
userAnswers: UserAnswers,
testDetailId: String
)
}

class ExamerRepository(
Expand Down Expand Up @@ -75,6 +78,15 @@ class ExamerRepository(
Result.failure(exception)
}

override suspend fun saveUserAnswersForUser(
user: ExamerUser,
userAnswers: UserAnswers,
testDetailId: String
) {
// todo exception handling
remoteDatabase.saveUserAnswers(user, userAnswers, testDetailId)
}

private fun AudioFileDTO.toExamerAudioFile(localAudioFileUri: Uri) = ExamerAudioFile(
localAudioFileUri = localAudioFileUri,
numberOfRepeatsAllowedForAudioFile = numberOfRepeatsAllowedForAudioFile
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/com/example/examer/data/domain/UserAnswers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.examer.data.domain

import com.example.examer.data.dto.UserAnswersDTO
import kotlinx.serialization.Serializable

@Serializable
@JvmInline
value class IndexOfChosenOption(val index: Int) {
override fun toString(): String = index.toString()
}

@Serializable
data class UserAnswers(
val associatedWorkBookId: String,
val answers: Map<MultiChoiceQuestion, IndexOfChosenOption>
)

fun UserAnswers.toUserAnswersDTO() = UserAnswersDTO(
associatedWorkBookId = associatedWorkBookId,
answersDetailsMap = answers.keys.map {
mapOf(
"multiChoiceQuestionId" to it.id,
"indexOfCorrectOption" to it.indexOfCorrectOption.toString(),
"indexOfChosenOption" to answers[it].toString()
)
}
)
19 changes: 19 additions & 0 deletions app/src/main/java/com/example/examer/data/dto/UserAnswersDTO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.examer.data.dto

import com.example.examer.data.domain.UserAnswers

/**
* A DTO object for [UserAnswers].
* @param associatedWorkBookId the id of the workbook associated
* with the [UserAnswers] object.
* @param answersDetailsMap a map with custom objects as keys are not
* supported. Only strings are supported. Therefore, it is not
* possible to use Map<MultiChoiceQuestion, IndexOfChosenOption>
* for answers. To accommodate for that, a list of maps are used to
* store the details of the each answer. A map in the list consists
* of the details of a particular mcq question.
*/
data class UserAnswersDTO(
val associatedWorkBookId: String,
val answersDetailsMap: List<Map<String, String>>
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import android.net.Uri
import com.example.examer.data.domain.*
import com.example.examer.data.dto.AudioFileDTO
import com.example.examer.data.dto.MultiChoiceQuestionListDTO
import com.example.examer.data.dto.UserAnswersDTO
import com.example.examer.data.dto.WorkBookDTO
import com.example.examer.data.dto.toMultiChoiceQuestion
import com.example.examer.di.DispatcherProvider
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.QuerySnapshot
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.ktx.storage
Expand All @@ -27,7 +30,10 @@ class FirebaseRemoteDatabase(private val dispatcherProvider: DispatcherProvider)

override suspend fun fetchScheduledTestListForUser(user: ExamerUser): List<TestDetails> =
withContext(dispatcherProvider.io) {
val scheduledTestsCollection = fetchCollection(getCollectionPathForScheduledTests(user))
val scheduledTestsCollection = fetchCollection(
collectionPath = getCollectionPathForTests(user),
runOnCollectionReference = { whereEqualTo("testStatus", "scheduled") }
)
// if no collection exists for the user, which likely indicates
// that the user is a newly registered user, an empty list will
// be returned.
Expand All @@ -36,7 +42,10 @@ class FirebaseRemoteDatabase(private val dispatcherProvider: DispatcherProvider)

override suspend fun fetchPreviousTestListForUser(user: ExamerUser): List<TestDetails> =
withContext(dispatcherProvider.io) {
val previousTestsCollection = fetchCollection(getCollectionPathForPreviousTests(user))
val previousTestsCollection = fetchCollection(
collectionPath = getCollectionPathForTests(user),
runOnCollectionReference = { whereIn("testStatus", listOf("completed", "missed")) }
)
// if no collection exists for the user, which likely indicates
// that the user is a newly registered user, an empty list will
// be returned.
Expand Down Expand Up @@ -85,6 +94,20 @@ class FirebaseRemoteDatabase(private val dispatcherProvider: DispatcherProvider)
}
}

override suspend fun saveUserAnswers(
user: ExamerUser,
userAnswers: UserAnswers,
testDetailsId: String
) {
withContext(dispatcherProvider.io) {
Firebase.firestore
.collection(getCollectionPathForUserAnswers(user, testDetailsId))
.document()
.set(userAnswers.toUserAnswersDTO())
.await() // throws exception
}
}

private fun DocumentSnapshot.toWorkBookDTO(): WorkBookDTO {
val examerAudioFile = AudioFileDTO(
audioFileUrl = URL(get("audioFileDownloadUrl").toString()),
Expand Down Expand Up @@ -119,21 +142,31 @@ class FirebaseRemoteDatabase(private val dispatcherProvider: DispatcherProvider)
.atZone(ZoneId.systemDefault())
.toLocalDateTime()

private suspend fun fetchCollection(collectionPath: String) = Firebase.firestore
//TODO add doc
private suspend fun fetchCollection(
collectionPath: String,
runOnCollectionReference: (CollectionReference.() -> Query)? = null
): QuerySnapshot = Firebase
.firestore
.collection(collectionPath)
.run {
// CollectionReference is a subclass of Query.
// if the block is not null, run the block
// and return the Query object returned by the block.
// if block is null, return the collection reference.
runOnCollectionReference?.invoke(this) ?: this
}
.get()
.await()

companion object {
private const val PROFILE_PICTURES_FOLDER_NAME = "profile_pics"
private fun getCollectionPathForScheduledTests(user: ExamerUser) =
"users/${user.id}/scheduledTests"

private fun getCollectionPathForPreviousTests(user: ExamerUser) =
"users/${user.id}/previousTests"

private fun getCollectionPathForTests(user: ExamerUser) = "users/${user.id}/tests"
private fun getCollectionPathForWorkBooks(user: ExamerUser, testDetails: TestDetails) =
"${getCollectionPathForScheduledTests(user)}/${testDetails.id}/workbooks"
"${getCollectionPathForTests(user)}/${testDetails.id}/workbooks"

private fun getCollectionPathForUserAnswers(user: ExamerUser, testDetailsId: String) =
"${getCollectionPathForTests(user)}/${testDetailsId}/answersForEachWorkBook"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.graphics.Bitmap
import android.net.Uri
import com.example.examer.data.domain.ExamerUser
import com.example.examer.data.domain.TestDetails
import com.example.examer.data.domain.UserAnswers
import com.example.examer.data.domain.WorkBook
import com.example.examer.data.dto.WorkBookDTO

Expand All @@ -16,4 +17,9 @@ interface RemoteDatabase {
): Result<List<WorkBookDTO>>

suspend fun saveBitmap(bitmap: Bitmap, fileName: String): Result<Uri>
suspend fun saveUserAnswers(
user: ExamerUser,
userAnswers: UserAnswers,
testDetailsId: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.examer.data.workers

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.examer.data.domain.UserAnswers
import com.example.examer.di.ExamerApplication
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

class SaveUserAnswersWorker(
private val appContext: Context,
workerParameters: WorkerParameters
) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork(): Result = try {
// deserialize user answers object
val userAnswersJsonString = inputData.getString(KEY_USER_ANSWERS_JSON_STRING_ARG)!!
val format = Json { allowStructuredMapKeys = true }
val userAnswers = format.decodeFromString<UserAnswers>(userAnswersJsonString)
// get testDetailsId
val testDetailsId = inputData.getString(KEY_TEST_DETAILS_ID_ARG)!!
// use the repository to save the UserAnswers object
val appContainer = ((appContext) as ExamerApplication).appContainer
val currentlyLoggedInUser = appContainer.authenticationService.currentUser.value!!
val repository = appContainer.repository
repository.saveUserAnswersForUser(currentlyLoggedInUser, userAnswers, testDetailsId)
Result.success()
} catch (exception: Exception) {
if (exception is CancellationException) throw exception
Result.failure()
}

companion object {
const val KEY_USER_ANSWERS_JSON_STRING_ARG =
"com.example.examer.data.workers.SaveUserAnswersWorker.KEY_USER_ANSWERS_ARG"
const val KEY_TEST_DETAILS_ID_ARG =
"com.example.examer.data.workers.SaveUserAnswersWorker.KEY_TEST_DETAILS_ID_ARG"
}
}
3 changes: 2 additions & 1 deletion app/src/main/java/com/example/examer/di/AppContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AppContainer(application: Application) {
FirebaseRemoteDatabase(StandardDispatchersProvider()) as RemoteDatabase
private val passwordManager = ExamerPasswordManager(application) as PasswordManager
val authenticationService = FirebaseAuthenticationService()
private val repository = ExamerRepository(
val repository = ExamerRepository(
context = application,
remoteDatabase = remoteDatabase,
updateProfilePhotoUriUseCase = UpdateProfilePhotoUriUseCaseImpl(
Expand Down Expand Up @@ -51,6 +51,7 @@ class AppContainer(application: Application) {
testDetailsListType = TestDetailsListType.PREVIOUS_TESTS
)

val workBookViewModelFactory = WorkBookViewModelFactory(application = application)
fun getTestSessionViewModelFactory(
testDetails: TestDetails,
workBookList: List<WorkBook>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
package com.example.examer.ui.navigation

import com.example.examer.data.domain.MultiChoiceQuestion
import com.example.examer.data.domain.WorkBook
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber

sealed class TakeTestScreenDestinations(val route: String) {
object ListenToAudioScreen : TakeTestScreenDestinations("com.example.examer.ui.navigation")
object ListenToAudioScreen :
TakeTestScreenDestinations("com.example.examer.ui.navigation.WorkBookScreen")

object WorkBookScreen :
TakeTestScreenDestinations("com.example.examer.ui.navigation.WorkBookScreen/{testDetailsId}/{workBookId}/{questionsList}/") {
const val WORKBOOK_ID_ARG = "workBookId"
const val QUESTIONS_LIST_ARG = "questionsList"
const val TEST_DETAILS_ID_ARG = "testDetailsId"
fun buildRoute(
testDetailsId: String,
workBookId: String,
multiChoiceQuestionList: List<MultiChoiceQuestion>
): String {
val jsonString = Json.encodeToString(multiChoiceQuestionList)
return "com.example.examer.ui.navigation.WorkBookScreen/${testDetailsId}/$workBookId/$jsonString/"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ fun LoggedInScreen(

takeTestScreenNavigation(
route = ExamerDestinations.TakeTestScreen.route,
navController = loggedInNavController,
appContainer = appContainer
)
}
Expand Down
Loading