Skip to content

Commit

Permalink
Merge pull request #339 from PeriodPals/feat/alert/fetch-profile-pict…
Browse files Browse the repository at this point in the history
…ure-of-other-users

Feat/alert/fetch profile picture of other users
  • Loading branch information
Harrish92 authored Dec 18, 2024
2 parents 55ab7f4 + 24f37a2 commit 79a35a1
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package com.android.periodpals.services

import android.Manifest
import androidx.activity.ComponentActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.android.periodpals.model.authentication.AuthenticationViewModel
import com.android.periodpals.model.location.Location
import com.android.periodpals.model.user.AuthenticationUserData
import com.android.periodpals.model.user.UserViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
Expand All @@ -17,6 +20,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`

@RunWith(AndroidJUnit4::class)
class GPSServiceImplInstrumentedTest {
Expand All @@ -31,6 +35,7 @@ class GPSServiceImplInstrumentedTest {
private lateinit var scenario: ActivityScenario<ComponentActivity>
private lateinit var activity: ComponentActivity
private lateinit var gpsService: GPSServiceImpl
private lateinit var authenticationViewModel: AuthenticationViewModel
private lateinit var userViewModel: UserViewModel

// Default location
Expand All @@ -39,6 +44,7 @@ class GPSServiceImplInstrumentedTest {

@Before
fun setup() {
authenticationViewModel = mock(AuthenticationViewModel::class.java)
userViewModel = mock(UserViewModel::class.java)

scenario = ActivityScenario.launch(ComponentActivity::class.java)
Expand All @@ -49,15 +55,20 @@ class GPSServiceImplInstrumentedTest {

scenario.onActivity { activity ->
this.activity = activity
gpsService = GPSServiceImpl(this.activity, userViewModel)
gpsService = GPSServiceImpl(this.activity, authenticationViewModel, userViewModel)
}

// Once the GPSService has been initialized, set its state to resumed
scenario.moveToState(Lifecycle.State.RESUMED)

`when`(authenticationViewModel.authUserData)
.thenReturn(mutableStateOf(AuthenticationUserData("test", "test")))
}

@After
fun tearDownService() {
`when`(authenticationViewModel.authUserData)
.thenReturn(mutableStateOf(AuthenticationUserData("test", "test")))
gpsService.cleanup()
}

Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/android/periodpals/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

gpsService = GPSServiceImpl(this, userViewModel)
pushNotificationsService = PushNotificationsServiceImpl(this, userViewModel)
gpsService = GPSServiceImpl(this, authenticationViewModel, userViewModel)
pushNotificationsService =
PushNotificationsServiceImpl(this, authenticationViewModel, userViewModel)
timerManager = TimerManager(this)
timerViewModel = TimerViewModel(timerModel, timerManager)

Expand Down
19 changes: 17 additions & 2 deletions app/src/main/java/com/android/periodpals/model/user/UserModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,25 @@ interface UserRepository {
/**
* Loads the user profile for the given user ID.
*
* @param idUser The ID of the user profile to be loaded.
* @param onSuccess callback to be called on successful call on this function returning the
* UserDto
* @param onFailure callback to be called when error is caught
*/
suspend fun loadUserProfile(onSuccess: (UserDto) -> Unit, onFailure: (Exception) -> Unit)
suspend fun loadUserProfile(
idUser: String,
onSuccess: (UserDto) -> Unit,
onFailure: (Exception) -> Unit
)

/**
* Loads all user profiles.
*
* @param onSuccess callback to be called on successful call on this function returning the list
* of UserDto
* @param onFailure callback to be called when error is caught
*/
suspend fun loadUserProfiles(onSuccess: (List<UserDto>) -> Unit, onFailure: (Exception) -> Unit)

/**
* Creates the user profile.
Expand All @@ -25,7 +39,8 @@ interface UserRepository {
* else create new.
*
* @param userDto The user profile to be checked
* @param onSuccess callback block
* @param onSuccess callback block to be called on success
* @param onFailure callback block to be called when exception is caught
*/
suspend fun upsertUserProfile(
userDto: UserDto,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ private const val USERS = "users"
class UserRepositorySupabase(private val supabase: SupabaseClient) : UserRepository {

override suspend fun loadUserProfile(
idUser: String,
onSuccess: (UserDto) -> Unit,
onFailure: (Exception) -> Unit,
) {
try {
val result =
withContext(Dispatchers.Main) {
supabase.postgrest[USERS]
.select {}
.select { filter { eq("user_id", idUser) } }
.decodeSingle<UserDto>() // RLS rules only allows user to check their own line
}
Log.d(TAG, "loadUserProfile: Success")
Expand All @@ -36,6 +37,23 @@ class UserRepositorySupabase(private val supabase: SupabaseClient) : UserReposit
}
}

override suspend fun loadUserProfiles(
onSuccess: (List<UserDto>) -> Unit,
onFailure: (Exception) -> Unit,
) {
try {
val result =
withContext(Dispatchers.Main) {
supabase.postgrest[USERS].select {}.decodeList<UserDto>()
}
Log.d(TAG, "loadUserProfiles: Success")
onSuccess(result)
} catch (e: Exception) {
Log.d(TAG, "loadUserProfiles: fail to load user profile: ${e.message}")
onFailure(e)
}
}

override suspend fun createUserProfile(
user: User,
onSuccess: () -> Unit,
Expand Down Expand Up @@ -122,7 +140,7 @@ class UserRepositorySupabase(private val supabase: SupabaseClient) : UserReposit
) {
try {
withContext(Dispatchers.Main) {
val file = supabase.storage.from("avatars").downloadAuthenticated("$filePath.jpg")
val file = supabase.storage.from("avatars").downloadPublic("$filePath.jpg")
Log.d(TAG, "downloadFile: Success")
onSuccess(file)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class UserViewModel(private val userRepository: UserRepositorySupabase) : ViewMo

private val _user = mutableStateOf<User?>(null)
val user: State<User?> = _user
private val _users = mutableStateOf<List<User>?>(null)
val users: State<List<User>?> = _users
private val _avatar = mutableStateOf<ByteArray?>(null)
val avatar: State<ByteArray?> = _avatar

Expand All @@ -76,48 +78,24 @@ class UserViewModel(private val userRepository: UserRepositorySupabase) : ViewMo
name = PROFILE_IMAGE_STATE_NAME, validators = profileImageValidators),
))

/**
* Initializes the user profile.
*
* @param onSuccess Callback function to be called when the user profile is successfully loaded.
* @param onFailure Callback function to be called when there is an error loading the user
* profile.
*/
fun init(
onSuccess: () -> Unit = { Log.d(TAG, "init success callback") },
onFailure: (Exception) -> Unit = { e: Exception ->
Log.d(TAG, "init failure callback: ${e.message}")
},
) {
loadUser(
onSuccess = {
user.value?.let {
downloadFile(
it.imageUrl,
onSuccess = { onSuccess() },
onFailure = { e: Exception -> onFailure(Exception(e)) },
)
}
},
onFailure = { e: Exception -> onFailure(Exception(e)) },
)
}

/**
* Loads the user profile and updates the user state.
*
* @param idUser The ID of the user profile to be loaded.
* @param onSuccess Callback function to be called when the user profile is successfully loaded.
* @param onFailure Callback function to be called when there is an error loading the user
* profile.
*/
fun loadUser(
idUser: String,
onSuccess: () -> Unit = { Log.d(TAG, "loadUser success callback") },
onFailure: (Exception) -> Unit = { e: Exception ->
Log.d(TAG, "loadUser failure callback: ${e.message}")
},
) {
viewModelScope.launch {
userRepository.loadUserProfile(
idUser,
onSuccess = { userDto ->
Log.d(TAG, "loadUserProfile: Successful")
_user.value = userDto.asUser()
Expand All @@ -132,6 +110,35 @@ class UserViewModel(private val userRepository: UserRepositorySupabase) : ViewMo
}
}

/**
* Loads all user profiles and updates the user state.
*
* @param onSuccess Callback function to be called when the user profiles are successfully loaded.
* @param onFailure Callback function to be called when there is an error loading the user
* profiles.
*/
fun loadUsers(
onSuccess: () -> Unit = { Log.d(TAG, "loadUsers success callback") },
onFailure: (Exception) -> Unit = { e: Exception ->
Log.d(TAG, "loadUsers failure callback: ${e.message}")
},
) {
viewModelScope.launch {
userRepository.loadUserProfiles(
onSuccess = { userDtos ->
Log.d(TAG, "loadUsers: Successful")
_users.value = userDtos.map { it.asUser() }
onSuccess()
},
onFailure = { e: Exception ->
Log.d(TAG, "loadUsers: fail to load user profiles: ${e.message}")
_users.value = null
onFailure(e)
},
)
}
}

/**
* Saves the user profile.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import com.android.periodpals.model.authentication.AuthenticationViewModel
import com.android.periodpals.model.location.Location
import com.android.periodpals.model.location.parseLocationGIS
import com.android.periodpals.model.user.UserViewModel
Expand Down Expand Up @@ -49,6 +50,7 @@ private enum class REQUEST_TYPE {
*/
class GPSServiceImpl(
private val activity: ComponentActivity,
private val authenticationViewModel: AuthenticationViewModel,
private val userViewModel: UserViewModel,
) : GPSService {
private var _location = MutableStateFlow(Location.DEFAULT_LOCATION)
Expand Down Expand Up @@ -196,7 +198,10 @@ class GPSServiceImpl(
*/
private fun uploadUserLocation() {
Log.d(TAG_UPLOAD_LOCATION, "Uploading user location")
authenticationViewModel.loadAuthenticationUserData(
onFailure = { Log.d(TAG_UPLOAD_LOCATION, "Authentication data is null") })
userViewModel.loadUser(
authenticationViewModel.authUserData.value!!.uid,
onSuccess = {
val newUser =
userViewModel.user.value?.copy(locationGIS = parseLocationGIS(_location.value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.android.periodpals.R
import com.android.periodpals.model.authentication.AuthenticationViewModel
import com.android.periodpals.model.user.UserViewModel
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
Expand All @@ -37,6 +38,7 @@ private const val TIMEOUT = 1000L
*/
class PushNotificationsServiceImpl(
private val activity: ComponentActivity,
private val authenticationViewModel: AuthenticationViewModel?,
private val userViewModel: UserViewModel?,
) : FirebaseMessagingService(), PushNotificationsService {

Expand All @@ -50,7 +52,7 @@ class PushNotificationsServiceImpl(
handlePermissionResult(it)
}

constructor() : this(ComponentActivity(), null) {
constructor() : this(ComponentActivity(), null, null) {
Log.e(TAG, "went through empty constructor")
}

Expand Down Expand Up @@ -229,7 +231,10 @@ class PushNotificationsServiceImpl(
Log.e(TAG, "UserViewModel not available")
return
}
authenticationViewModel?.loadAuthenticationUserData(
onFailure = { Log.d(TAG, "Authentication data is null") })
userViewModel.loadUser(
authenticationViewModel?.authUserData?.value!!.uid,
onSuccess = {
Log.d(TAG, "Uploading token to server")
val newUser = userViewModel.user.value?.copy(fcmToken = token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ fun CreateAlertScreen(
Log.d(TAG, "Authentication data is null")
})
userViewModel.loadUser(
authenticationViewModel.authUserData.value!!.uid,
onFailure = {
Handler(Looper.getMainLooper()).post { // used to show the Toast in the main thread
Toast.makeText(context, "Error loading your data! Try again later.", Toast.LENGTH_SHORT)
Expand Down
Loading

0 comments on commit 79a35a1

Please sign in to comment.