diff --git a/src/backend/src/main/kotlin/pt/ulisboa/ist/pharmacist/repository/medicines/MedicinesRepositoryMem.kt b/src/backend/src/main/kotlin/pt/ulisboa/ist/pharmacist/repository/medicines/MedicinesRepositoryMem.kt index 840635e..e4b31ea 100644 --- a/src/backend/src/main/kotlin/pt/ulisboa/ist/pharmacist/repository/medicines/MedicinesRepositoryMem.kt +++ b/src/backend/src/main/kotlin/pt/ulisboa/ist/pharmacist/repository/medicines/MedicinesRepositoryMem.kt @@ -29,7 +29,7 @@ class MedicinesRepositoryMem(private val dataSource: MemDataSource) : MedicinesR MedicineWithClosestPharmacyDto( medicine = MedicineDto(medicine), closestPharmacy = dataSource.pharmacies.values.filter { pharmacy -> - pharmacy.medicines.map { it.medicine.medicineId }.contains(medicine.medicineId) + pharmacy.medicines.any { it.medicine.medicineId == medicine.medicineId } }.minByOrNull { pharmacy -> if (location == null) return@minByOrNull 0.0 diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/DependenciesContainer.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/DependenciesContainer.kt index c9e2844..8822f6f 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/DependenciesContainer.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/DependenciesContainer.kt @@ -3,6 +3,7 @@ package pt.ulisboa.ist.pharmacist import com.google.gson.Gson import okhttp3.OkHttpClient import pt.ulisboa.ist.pharmacist.service.http.PharmacistService +import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdatesService import pt.ulisboa.ist.pharmacist.session.SessionManager /** @@ -15,6 +16,7 @@ import pt.ulisboa.ist.pharmacist.session.SessionManager interface DependenciesContainer { val jsonEncoder: Gson val sessionManager: SessionManager + val realTimeUpdatesService: RealTimeUpdatesService val pharmacistService: PharmacistService val httpClient: OkHttpClient } diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/PharmacistApplication.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/PharmacistApplication.kt index 48344e8..053f328 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/PharmacistApplication.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/PharmacistApplication.kt @@ -11,6 +11,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import okhttp3.OkHttpClient import pt.ulisboa.ist.pharmacist.service.http.PharmacistService +import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdatesService import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatesBackgroundService import pt.ulisboa.ist.pharmacist.session.SessionManager import pt.ulisboa.ist.pharmacist.session.SessionManagerSharedPrefs @@ -41,6 +42,11 @@ class PharmacistApplication : DependenciesContainer, Application() { sessionManager = sessionManager ) + override val realTimeUpdatesService = RealTimeUpdatesService( + apiEndpoint = API_ENDPOINT, + sessionManager = sessionManager, + httpClient = httpClient + ) override fun onCreate() { super.onCreate() diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/http/services/medicines/RealTimeUpdatesService.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/http/services/medicines/RealTimeUpdatesService.kt index 6ea99e5..cceb61c 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/http/services/medicines/RealTimeUpdatesService.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/http/services/medicines/RealTimeUpdatesService.kt @@ -4,7 +4,10 @@ import android.util.Log import com.google.gson.Gson import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -13,7 +16,15 @@ import okhttp3.WebSocketListener import okio.ByteString import pt.ulisboa.ist.pharmacist.service.http.utils.Uris import pt.ulisboa.ist.pharmacist.service.http.utils.fromJson -import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdate +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RTU +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdateMedicineNotificationPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdateNewPharmacyPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePharmacyGlobalRatingPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePharmacyMedicineStockPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePharmacyUserFavoritedPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePharmacyUserFlaggedPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePharmacyUserRatingPublishingData +import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatePublishingDto import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdatesBackgroundService.Companion.TAG import pt.ulisboa.ist.pharmacist.session.SessionManager import java.net.HttpURLConnection @@ -25,8 +36,25 @@ class RealTimeUpdatesService( ) { private var webSocket: WebSocket? = null + private val updateFlow = MutableSharedFlow() - fun getUpdateFlow(): Flow = callbackFlow { + init { + runBlocking { + while (true) { + if (sessionManager.isLoggedIn()) { + getWebSocketListenerFlow() + } + // TODO Synchronization issue? + sessionManager.logInFlow.collect { loggedIn -> + if (loggedIn && sessionManager.isLoggedIn()) { + getWebSocketListenerFlow() + } + } + } + } + } + + private fun getWebSocketListenerFlow(): Flow = callbackFlow { val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { super.onOpen(webSocket, response) @@ -39,7 +67,7 @@ class RealTimeUpdatesService( close() return } - val message = Gson().fromJson(text) + val message = Gson().fromJson(text) trySend(message) Log.d(TAG, "WebSocket message received") } @@ -77,17 +105,76 @@ class RealTimeUpdatesService( webSocket = httpClient.newWebSocket(request, listener) + launch { + sessionManager.logInFlow.collect { loggedIn -> + if (!loggedIn) { + close() + } + } + } + awaitClose { webSocket?.close(1000, null) Log.d(TAG, "WebSocket closed") } } -} -object RealTimeUpdateTypes { - const val PHARMACY = "pharmacy" - const val PHARMACY_MEDICINE_STOCK = "pharmacy-medicine-stock" - const val MEDICINE_NOTIFICATION = "medicine-notification" + + suspend fun listenForRealTimeUpdates( + onNewPharmacy: (RealTimeUpdateNewPharmacyPublishingData) -> Unit = {}, + onPharmacyUserRating: (RealTimeUpdatePharmacyUserRatingPublishingData) -> Unit = {}, + onPharmacyGlobalRating: (RealTimeUpdatePharmacyGlobalRatingPublishingData) -> Unit = {}, + onPharmacyUserFlagged: (RealTimeUpdatePharmacyUserFlaggedPublishingData) -> Unit = {}, + onPharmacyUserFavorited: (RealTimeUpdatePharmacyUserFavoritedPublishingData) -> Unit = {}, + onMedicineStock: (RealTimeUpdatePharmacyMedicineStockPublishingData) -> Unit = {}, + onMedicineNotification: (RealTimeUpdateMedicineNotificationPublishingData) -> Unit = {} + ) { + updateFlow.collect { realTimeUpdateDto -> + when (val realTimeUpdateClass = RTU.getType(realTimeUpdateDto.type)) { + RTU.NEW_PHARMACY -> onNewPharmacy(realTimeUpdateClass.parseJson(realTimeUpdateDto.data)) + RTU.PHARMACY_USER_RATING -> onPharmacyUserRating( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + RTU.PHARMACY_GLOBAL_RATING -> onPharmacyGlobalRating( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + RTU.PHARMACY_USER_FLAGGED -> onPharmacyUserFlagged( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + RTU.PHARMACY_USER_FAVORITED -> onPharmacyUserFavorited( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + RTU.PHARMACY_MEDICINE_STOCK -> onMedicineStock( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + RTU.MEDICINE_NOTIFICATION -> onMedicineNotification( + realTimeUpdateClass.parseJson( + realTimeUpdateDto.data + ) + ) + + null -> Log.e( + "RealTimeUpdatesService", + "Unknown real time update type: ${realTimeUpdateDto.type}" + ) + } + } + } } class JsonWebSocket(private val webSocket: WebSocket) { diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatePublishSubscribeData.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatePublishSubscribeData.kt index 94e474f..451d7e1 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatePublishSubscribeData.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatePublishSubscribeData.kt @@ -1,26 +1,66 @@ package pt.ulisboa.ist.pharmacist.service.real_time_updates +import com.google.gson.Gson +import org.jetbrains.annotations.Contract import pt.ulisboa.ist.pharmacist.domain.medicines.Medicine -import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdateTypes.MEDICINE_NOTIFICATION -import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdateTypes.PHARMACY -import pt.ulisboa.ist.pharmacist.service.real_time_updates.RealTimeUpdateTypes.PHARMACY_MEDICINE_STOCK +import pt.ulisboa.ist.pharmacist.domain.pharmacies.Location +import pt.ulisboa.ist.pharmacist.service.http.connection.APIResult +import pt.ulisboa.ist.pharmacist.service.http.connection.isSuccess +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract /* * Types */ -object RealTimeUpdateTypes { - const val PHARMACY = "pharmacy" - const val PHARMACY_MEDICINE_STOCK = "pharmacy-medicine-stock" - const val MEDICINE_NOTIFICATION = "medicine-notification" +enum class RTU(val type: String, val dataClass: Class) { + NEW_PHARMACY( + "new-pharmacy", + RealTimeUpdateNewPharmacyPublishingData::class.java + ), + PHARMACY_USER_RATING( + "pharmacy-user-rating", + RealTimeUpdatePharmacyUserRatingPublishingData::class.java + ), + PHARMACY_GLOBAL_RATING( + "pharmacy-global-rating", + RealTimeUpdatePharmacyGlobalRatingPublishingData::class.java + ), + PHARMACY_USER_FLAGGED( + "pharmacy-user-flagged", + RealTimeUpdatePharmacyUserFlaggedPublishingData::class.java + ), + PHARMACY_USER_FAVORITED( + "pharmacy-user-favorited", + RealTimeUpdatePharmacyUserFavoritedPublishingData::class.java + ), + PHARMACY_MEDICINE_STOCK( + "pharmacy-medicine-stock", + RealTimeUpdatePharmacyMedicineStockPublishingData::class.java + ), + MEDICINE_NOTIFICATION( + "medicine-notification", + RealTimeUpdateMedicineNotificationPublishingData::class.java + ); + + companion object { + fun getType(type: String): RTU? { + return entries.find { it.type == type } + } + } + + inline fun parseJson(data: String): T { + return Gson().fromJson( + data, + dataClass + ) as T + } } /* * Publishes */ -typealias RealTimeUpdate = RealTimeUpdatePublishingDto - data class RealTimeUpdatePublishingDto( val type: String, val data: String @@ -30,19 +70,69 @@ data class RealTimeUpdatePublishing( val type: String, val data: RealTimeUpdatePublishingData ) { + constructor(rtu: RTU, data: RealTimeUpdatePublishingData) : this(rtu.type, data) + companion object { - fun pharmacy(pharmacyId: Long, globalRatingSum: Double, numberOfRatings: List) = + + fun newPharmacy( + pharmacyId: Long, + name: String, + location: Location, + pictureUrl: String, + globalRating: Double?, + numberOfRatings: List + ) = + RealTimeUpdatePublishing( + RTU.NEW_PHARMACY, RealTimeUpdateNewPharmacyPublishingData( + pharmacyId = pharmacyId, + name = name, + location = location, + pictureUrl = pictureUrl, + globalRating = globalRating, + numberOfRatings = numberOfRatings + ) + ) + + fun pharmacyUserRating(pharmacyId: Long, userRating: Int) = + RealTimeUpdatePublishing( + RTU.PHARMACY_USER_RATING, RealTimeUpdatePharmacyUserRatingPublishingData( + pharmacyId = pharmacyId, + userRating = userRating + ) + ) + + fun pharmacyGlobalRating( + pharmacyId: Long, + globalRatingSum: Double, + numberOfRatings: List + ) = RealTimeUpdatePublishing( - PHARMACY, RealTimeUpdatePharmacyPublishingData( + RTU.PHARMACY_GLOBAL_RATING, RealTimeUpdatePharmacyGlobalRatingPublishingData( pharmacyId = pharmacyId, globalRatingSum = globalRatingSum, numberOfRatings = numberOfRatings ) ) + fun pharmacyUserFlagged(pharmacyId: Long, flagged: Boolean) = + RealTimeUpdatePublishing( + RTU.PHARMACY_USER_FLAGGED, RealTimeUpdatePharmacyUserFlaggedPublishingData( + pharmacyId = pharmacyId, + flagged = flagged + ) + ) + + fun pharmacyUserFavorited(pharmacyId: Long, favorited: Boolean) = + RealTimeUpdatePublishing( + RTU.PHARMACY_USER_FAVORITED, RealTimeUpdatePharmacyUserFavoritedPublishingData( + pharmacyId = pharmacyId, + favorited = favorited + ) + ) + fun pharmacyMedicineStock(pharmacyId: Long, medicineId: Long, stock: Long) = RealTimeUpdatePublishing( - PHARMACY_MEDICINE_STOCK, RealTimeUpdatePharmacyMedicineStockPublishingData( + RTU.PHARMACY_MEDICINE_STOCK, RealTimeUpdatePharmacyMedicineStockPublishingData( pharmacyId = pharmacyId, medicineId = medicineId, stock = stock @@ -54,7 +144,7 @@ data class RealTimeUpdatePublishing( medicineStock: RealTimeUpdateMedicineNotificationPublishingData.MedicineStock ) = RealTimeUpdatePublishing( - MEDICINE_NOTIFICATION, RealTimeUpdateMedicineNotificationPublishingData( + RTU.MEDICINE_NOTIFICATION, RealTimeUpdateMedicineNotificationPublishingData( pharmacy = pharmacy, medicineStock = medicineStock ) @@ -64,12 +154,36 @@ data class RealTimeUpdatePublishing( interface RealTimeUpdatePublishingData -data class RealTimeUpdatePharmacyPublishingData( +data class RealTimeUpdateNewPharmacyPublishingData( + var pharmacyId: Long, + val name: String, + val location: Location, + val pictureUrl: String, + val globalRating: Double?, + val numberOfRatings: List +) : RealTimeUpdatePublishingData + +data class RealTimeUpdatePharmacyUserRatingPublishingData( + val pharmacyId: Long, + val userRating: Int +) : RealTimeUpdatePublishingData + +data class RealTimeUpdatePharmacyGlobalRatingPublishingData( val pharmacyId: Long, val globalRatingSum: Double, val numberOfRatings: List ) : RealTimeUpdatePublishingData +data class RealTimeUpdatePharmacyUserFlaggedPublishingData( + var pharmacyId: Long, + val flagged: Boolean +) : RealTimeUpdatePublishingData + +data class RealTimeUpdatePharmacyUserFavoritedPublishingData( + var pharmacyId: Long, + val favorited: Boolean +) : RealTimeUpdatePublishingData + data class RealTimeUpdatePharmacyMedicineStockPublishingData( val pharmacyId: Long, val medicineId: Long, diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatesBackgroundService.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatesBackgroundService.kt index 0fcb54c..b6ba701 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatesBackgroundService.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/service/real_time_updates/RealTimeUpdatesBackgroundService.kt @@ -11,17 +11,9 @@ import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import com.google.gson.Gson -import kotlinx.coroutines.delay import pt.ulisboa.ist.pharmacist.DependenciesContainer import pt.ulisboa.ist.pharmacist.PharmacistApplication -import pt.ulisboa.ist.pharmacist.PharmacistApplication.Companion.API_ENDPOINT import pt.ulisboa.ist.pharmacist.R -import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdateTypes.MEDICINE_NOTIFICATION -import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdateTypes.PHARMACY -import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdateTypes.PHARMACY_MEDICINE_STOCK -import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdatesService -import pt.ulisboa.ist.pharmacist.service.http.utils.fromJson import pt.ulisboa.ist.pharmacist.service.utils.runNewBlocking import pt.ulisboa.ist.pharmacist.ui.screens.medicine.MedicineActivity @@ -30,29 +22,13 @@ class RealTimeUpdatesBackgroundService : Service() { (application as DependenciesContainer) } - //TODO: Check null pointer exception - private val realTimeUpdatesService by lazy { - RealTimeUpdatesService( - apiEndpoint = API_ENDPOINT, - sessionManager = dependenciesContainer.sessionManager, - httpClient = dependenciesContainer.httpClient, - ) - } - override fun onBind(intent: Intent?): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { runNewBlocking { - while (true) { - if (dependenciesContainer.sessionManager.isLoggedIn()) { - getUpdates() - } - - delay(5000) //TODO: Remove delay - Log.d(TAG, "Checking if user is logged in after delay") - } + getUpdates() } return START_STICKY @@ -62,23 +38,14 @@ class RealTimeUpdatesBackgroundService : Service() { * Get the real time updates from the server. */ private suspend fun getUpdates() { - Log.d(TAG, "Getting real time updates") - val flow = realTimeUpdatesService - .getUpdateFlow() - Log.d(TAG, "Real time updates flow started") - flow.collect { realTimeUpdate -> - when (realTimeUpdate.type) { - PHARMACY, PHARMACY_MEDICINE_STOCK -> {} - MEDICINE_NOTIFICATION -> { - if (checkNotificationPermission()) { - val medicineNotificationData = - Gson().fromJson(realTimeUpdate.data) - showMedicineNotification(medicineNotificationData) - } + dependenciesContainer.realTimeUpdatesService.listenForRealTimeUpdates( + onMedicineNotification = { medicineNotificationData -> + if (checkNotificationPermission()) { + showMedicineNotification(medicineNotificationData) } } - } + ) Log.d(TAG, "Real time updates flow ended") } diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManager.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManager.kt index 644ab34..409a2f4 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManager.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManager.kt @@ -1,5 +1,7 @@ package pt.ulisboa.ist.pharmacist.session +import kotlinx.coroutines.flow.SharedFlow + /** * Responsible for holding a user's session. * @@ -13,6 +15,7 @@ interface SessionManager { val accessToken: String? val username: String? val isGuest: Boolean + val logInFlow: SharedFlow /** * Checks if the user is logged in. diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManagerSharedPrefs.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManagerSharedPrefs.kt index b93c1a6..8f3b889 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManagerSharedPrefs.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/session/SessionManagerSharedPrefs.kt @@ -1,6 +1,7 @@ package pt.ulisboa.ist.pharmacist.session import android.content.Context +import kotlinx.coroutines.flow.MutableSharedFlow /** * Session manager that uses shared preferences to store the session. @@ -28,6 +29,8 @@ class SessionManagerSharedPrefs(private val context: Context) : SessionManager { override val isGuest: Boolean get() = prefs.getBoolean(IS_GUEST, false) + override val logInFlow: MutableSharedFlow = MutableSharedFlow() + override fun setSession( userId: Long, accessToken: String, @@ -40,6 +43,8 @@ class SessionManagerSharedPrefs(private val context: Context) : SessionManager { .putLong(USER_ID, userId) .putBoolean(IS_GUEST, isGuest) .apply() + + logInFlow.tryEmit(true) } override fun clearSession() { @@ -49,6 +54,8 @@ class SessionManagerSharedPrefs(private val context: Context) : SessionManager { .remove(USER_ID) .remove(IS_GUEST) .apply() + + logInFlow.tryEmit(false) } companion object { diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapActivity.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapActivity.kt index c200bfc..1e878c9 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapActivity.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapActivity.kt @@ -28,6 +28,7 @@ class PharmacyMapActivity : PharmacistActivity() { PharmacyMapViewModel( dependenciesContainer.pharmacistService, dependenciesContainer.sessionManager, + dependenciesContainer.realTimeUpdatesService, placesClient = Places.createClient(this), geoCoder = Geocoder(this) ) @@ -68,6 +69,9 @@ class PharmacyMapActivity : PharmacistActivity() { viewModel.hasLocationPermission = hasLocationPermission() viewModel.hasCameraPermission = hasCameraPermission() viewModel.loadPharmacyList() + lifecycleScope.launch { + viewModel.listenForRealTimeUpdates() + } setContent { PharmacyMapScreen( diff --git a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapViewModel.kt b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapViewModel.kt index ae6b828..f4ed162 100644 --- a/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapViewModel.kt +++ b/src/frontend/app/src/main/kotlin/pt/ulisboa/ist/pharmacist/ui/screens/pharmacyMap/PharmacyMapViewModel.kt @@ -5,6 +5,7 @@ import android.content.Context import android.location.Geocoder import android.util.Log import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap @@ -24,9 +25,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import okhttp3.MediaType import pt.ulisboa.ist.pharmacist.domain.pharmacies.Location +import pt.ulisboa.ist.pharmacist.domain.pharmacies.Pharmacy import pt.ulisboa.ist.pharmacist.service.LocationService import pt.ulisboa.ist.pharmacist.service.http.PharmacistService import pt.ulisboa.ist.pharmacist.service.http.connection.isSuccess +import pt.ulisboa.ist.pharmacist.service.http.services.medicines.RealTimeUpdatesService import pt.ulisboa.ist.pharmacist.service.http.services.pharmacies.models.getPharmacyById.PharmacyWithUserDataModel import pt.ulisboa.ist.pharmacist.session.SessionManager import pt.ulisboa.ist.pharmacist.ui.screens.PharmacistViewModel @@ -45,6 +48,7 @@ import pt.ulisboa.ist.pharmacist.ui.screens.shared.ImageHandlingUtils class PharmacyMapViewModel( pharmacistService: PharmacistService, sessionManager: SessionManager, + private val realTimeUpdatesService: RealTimeUpdatesService, val placesClient: PlacesClient, val geoCoder: Geocoder ) : PharmacistViewModel(pharmacistService, sessionManager) { @@ -57,8 +61,7 @@ class PharmacyMapViewModel( var hasLocationPermission by mutableStateOf(false) var hasCameraPermission by mutableStateOf(false) - var pharmacies by mutableStateOf>(emptyList()) - private set + val pharmacies = mutableStateMapOf() var cameraPositionState by mutableStateOf(CameraPositionState()) private set @@ -78,6 +81,52 @@ class PharmacyMapViewModel( var searchQuery by mutableStateOf("") private set + suspend fun listenForRealTimeUpdates() { + realTimeUpdatesService.listenForRealTimeUpdates( + onNewPharmacy = { newPharmacy -> + pharmacies[newPharmacy.pharmacyId] = + PharmacyWithUserDataModel( + Pharmacy( + pharmacyId = newPharmacy.pharmacyId, + name = newPharmacy.name, + location = newPharmacy.location, + pictureUrl = newPharmacy.pictureUrl, + globalRating = newPharmacy.globalRating, + numberOfRatings = newPharmacy.numberOfRatings.toTypedArray() + ), + userRating = null, + userMarkedAsFavorite = false, + userFlagged = false + ) + }, + onPharmacyUserRating = { pharmacyUserRatingData -> + pharmacies.compute(pharmacyUserRatingData.pharmacyId) { _, pharmacy -> + pharmacy?.copy(userRating = pharmacyUserRatingData.userRating) + } + }, + onPharmacyGlobalRating = { pharmacyGlobalRatingData -> + pharmacies.compute(pharmacyGlobalRatingData.pharmacyId) { _, pharmacyWithUserData -> + pharmacyWithUserData?.copy( + pharmacy = pharmacyWithUserData.pharmacy.copy( + globalRating = pharmacyGlobalRatingData.globalRatingSum, + numberOfRatings = pharmacyGlobalRatingData.numberOfRatings.toTypedArray() + ) + ) + } + }, + onPharmacyUserFlagged = { pharmacyUserFlaggedData -> + pharmacies.compute(pharmacyUserFlaggedData.pharmacyId) { _, pharmacyWithUserData -> + pharmacyWithUserData?.copy(userFlagged = pharmacyUserFlaggedData.flagged) + } + }, + onPharmacyUserFavorited = { pharmacyUserFavoritedData -> + pharmacies.compute(pharmacyUserFavoritedData.pharmacyId) { _, pharmacyWithUserData -> + pharmacyWithUserData?.copy(userMarkedAsFavorite = pharmacyUserFavoritedData.favorited) + } + } + ) + } + /** * Uploads the box photo to the server. *