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

user profile picture in the create profile screen and the hamburger menu #358

Merged
merged 11 commits into from
May 31, 2024

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.swent.echo.compose.components

import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
Expand All @@ -8,7 +9,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Close
Expand All @@ -26,6 +29,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
Expand All @@ -48,6 +54,7 @@ fun HamburgerMenuDrawerSheet(
scope: CoroutineScope,
profileName: String,
profileClass: String,
profilePicture: Bitmap? = null,
onSignOutPressed: () -> Unit,
onToggle: () -> Unit
) {
Expand Down Expand Up @@ -109,8 +116,13 @@ fun HamburgerMenuDrawerSheet(
modifier = Modifier.align(Alignment.TopStart).padding(8.dp).testTag("profile_sheet")
) {
Image(
modifier = Modifier.testTag("profile_picture"),
painter = painterResource(id = R.drawable.ic_launcher_foreground),
modifier = Modifier.testTag("profile_picture").size(150.dp).clip(CircleShape),
painter =
if (profilePicture != null) {
BitmapPainter(profilePicture!!.asImageBitmap())
} else {
painterResource(R.drawable.echologoround)
},
contentDescription = "profile picture"
)
Row(modifier = Modifier.padding(8.dp).testTag("profile_info")) {
Expand All @@ -127,20 +139,11 @@ fun HamburgerMenuDrawerSheet(
)
}
}
// Close button for the hamburger menu
IconButton(
onClick = { scope.launch { drawerState.close() } },
modifier =
Modifier.align(Alignment.TopEnd)
.padding(8.dp)
.testTag("close_button_hamburger_menu")
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close button hamburger menu"
)

// button to toggle the theme
Box(modifier = Modifier.align(Alignment.TopEnd).padding(8.dp)) {
ThemeToggleButton(onToggle = onToggle)
}
ThemeToggleButton(onToggle = onToggle)
}
// Display the navigation items
items.forEachIndexed { index, item ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ fun HomeScreen(
// Profile information for the hamburger menu
val profileName by homeScreenViewModel.profileName.collectAsState()
val profileClass by homeScreenViewModel.profileClass.collectAsState()
val profilePicture by homeScreenViewModel.profilePicture.collectAsState()

// Search mode for displaying events
val searchMode by homeScreenViewModel.searchMode.collectAsState()
Expand All @@ -62,6 +63,7 @@ fun HomeScreen(
scope,
profileName,
profileClass,
profilePicture,
{
homeScreenViewModel.signOut()
navActions.navigateTo(Routes.LOGIN)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.swent.echo.viewmodels

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.swent.echo.R
Expand Down Expand Up @@ -144,6 +146,8 @@ constructor(
val initialPage = _initialPage.asStateFlow()
// Flow to observe the network status
val isOnline = networkService.isOnline
private val _profilePicture = MutableStateFlow<Bitmap?>(null)
val profilePicture = _profilePicture.asStateFlow()
// Flow to observe the followed associations
private val _followedAssociations = MutableStateFlow<List<String>>(listOf())
val followedAssociations = _followedAssociations.asStateFlow()
Expand All @@ -169,6 +173,11 @@ constructor(
followedTagFilter = getTagsAndSubTags(_followedTags.value).toList()
sectionTags = repository.getSubTags(sectionTagId)
semesterTags = repository.getSubTags(semesterTagId)
val pictureByteArray = repository.getUserProfilePicture(userId)
if (pictureByteArray != null) {
_profilePicture.value =
BitmapFactory.decodeByteArray(pictureByteArray, 0, pictureByteArray.size)
}
val followedAssociationIds =
repository.getUserProfile(userId)?.associationsSubscriptions?.map {
it.associationId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.swent.echo.viewmodels.authentication

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -12,6 +14,7 @@ import com.github.swent.echo.data.model.Tag
import com.github.swent.echo.data.model.UserProfile
import com.github.swent.echo.data.repository.Repository
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.ByteArrayOutputStream
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -56,6 +59,9 @@ constructor(
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage = _errorMessage.asStateFlow()

private val _picture = MutableStateFlow<Bitmap?>(null)
val picture = _picture.asStateFlow()

// functions to change the name of the private variables from outside the class. (setters)

fun setFirstName(firstName: String) {
Expand Down Expand Up @@ -94,6 +100,15 @@ constructor(
_tagList.value = userProfile.tags
_committeeMember.value = userProfile.committeeMember
_associationSubscriptions.value = userProfile.associationsSubscriptions
val pictureByteArray = repository.getUserProfilePicture(userId)
if (pictureByteArray != null) {
_picture.value =
BitmapFactory.decodeByteArray(
pictureByteArray,
0,
pictureByteArray.size
)
}
} else {
_isEditing.value = false
}
Expand Down Expand Up @@ -124,6 +139,13 @@ constructor(
associationsSubscriptions = _associationSubscriptions.value
)
)
if (picture.value == null) {
repository.deleteUserProfilePicture(userId)
} else {
val pictureStream = ByteArrayOutputStream()
picture.value!!.compress(Bitmap.CompressFormat.JPEG, 100, pictureStream)
repository.setUserProfilePicture(userId, pictureStream.toByteArray())
}
}
}
}
Expand All @@ -136,4 +158,8 @@ constructor(
fun removeTag(tag: Tag) {
_tagList.value -= tag
}

fun setPicture(picture: Bitmap?) {
_picture.value = picture
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
<string name="profile_creation_section">Section</string>
<string name="profile_creation_semester">Semester</string>
<string name="profile_creation_tags">Tags</string>
<string name="profile_creation_edit_picture">Edit picture</string>
<string name="profile_creation_delete_picture">Delete picture</string>
<string name="profile_creation_center_picture">Adjust the zoom</string>
<string name="event_successfully_joined">Event successfully joined!</string>
<string name="event_info_sheet_modify_event">Modify Event</string>
<string name="list_drawer_event_status">Event Status</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
package com.github.swent.echo.compose.authentication

import android.graphics.Bitmap
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipe
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.swent.echo.authentication.AuthenticationService
import com.github.swent.echo.connectivity.NetworkService
import com.github.swent.echo.data.model.SectionEPFL
import com.github.swent.echo.data.model.SemesterEPFL
import com.github.swent.echo.data.model.Tag
import com.github.swent.echo.data.repository.SimpleRepository
import com.github.swent.echo.viewmodels.authentication.CreateProfileViewModel
import io.mockk.every
import io.mockk.mockk
import java.io.ByteArrayOutputStream
import junit.framework.TestCase
import junit.framework.TestCase.assertNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -39,7 +58,9 @@ class CreateProfileTest {
onSecChange = {},
onSemChange = {},
isEditing = true,
isOnline = true
isOnline = true,
picture = null,
onPictureChange = {}
)
}
// Assert that certain elements are present on the screen
Expand All @@ -62,4 +83,79 @@ class CreateProfileTest {
composeTestRule.onNodeWithTag("BA1").assertExists()
composeTestRule.onNodeWithTag("BA2").assertExists()
}

@Test
fun profileCreationPictureIsCorrect() {
val picture = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
var changedPicture: Bitmap? = picture
composeTestRule.setContent {
ProfilePictureEdit(picture = picture, onPictureChange = { changedPicture = it })
}
composeTestRule
.onNodeWithTag("profile-picture-image")
.assertIsDisplayed()
.assertHasClickAction()
composeTestRule
.onNodeWithTag("profile-picture-delete")
.assertIsDisplayed()
.assertHasClickAction()
composeTestRule.onNodeWithTag("profile-picture-delete").performClick()
assertNull(changedPicture)
composeTestRule.onNodeWithTag("profile-picture-image").performClick()
}

@Test
fun profileCreationPictureTransformerCallbacksTest() {
val picture = Bitmap.createBitmap(300, 1000, Bitmap.Config.ARGB_8888)
var changedPicture: Bitmap = picture
var canceled = false
composeTestRule.setContent {
PictureTransformer(
picture = picture,
onConfirm = { changedPicture = it },
onCancel = { canceled = true }
)
}
composeTestRule.onNodeWithTag("profile-picture-transformer").assertIsDisplayed()
composeTestRule.onNodeWithTag("profile-picture-transformer-confirm").performClick()
assertTrue(changedPicture != picture)
assertTrue(changedPicture.height == changedPicture.width)
composeTestRule.onNodeWithTag("profile-picture-transformer-cancel").performClick()
assertTrue(canceled)
}

@Test
fun profileCreationPictureTransformerGestureTest() {
val picture = Bitmap.createBitmap(300, 1000, Bitmap.Config.ARGB_8888)
var changedPicture: Bitmap = picture
composeTestRule.setContent {
PictureTransformer(
picture = picture,
onConfirm = { changedPicture = it },
onCancel = {}
)
}
val image = composeTestRule.onNodeWithTag("profile-picture-image")
image.assertIsDisplayed()
image.performTouchInput { this.swipe(Offset.Zero, Offset(50f, 50f)) }
image.assertPositionInRootIsEqualTo(0.dp, 0.dp)
}

@Test
fun setProfilePictureViewModelWithBitmapTest() {
val authenticationService: AuthenticationService = mockk(relaxed = true)
val repository = SimpleRepository(authenticationService)
val mockedNetworkService = mockk<NetworkService>()
every { mockedNetworkService.isOnline } returns MutableStateFlow(true)
var viewModel =
CreateProfileViewModel(authenticationService, repository, mockedNetworkService)
val picture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
viewModel.setPicture(picture)
TestCase.assertEquals(picture, viewModel.picture.value)
val outputStream = ByteArrayOutputStream()
picture.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
runBlocking { repository.setUserProfilePicture("", outputStream.toByteArray()) }
viewModel = CreateProfileViewModel(authenticationService, repository, mockedNetworkService)
TestCase.assertEquals(picture.byteCount, viewModel.picture.value?.byteCount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,6 @@ class HomeScreenTest {
composeTestRule.onNodeWithTag("profile_class").assertExists()
}

@Test
fun shouldShowCloseButtonWhenMenuButtonClicked() {
composeTestRule.onNodeWithTag("menu_button").performClick()
composeTestRule.onNodeWithTag("close_button_hamburger_menu").assertExists()
}

// Change number of items to check according to the number of button in hamburger menu

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.swent.echo.viewmodels

import android.graphics.BitmapFactory
import com.github.swent.echo.compose.map.MAP_CENTER
import com.github.swent.echo.connectivity.NetworkService
import com.github.swent.echo.data.model.AssociationHeader
Expand All @@ -14,6 +15,7 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import java.time.ZonedDateTime
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -73,7 +75,10 @@ class HomeScreenViewModelTest {
coEvery { mockedRepository.getAllEvents() } returns eventList
coEvery { mockedRepository.getAllTags() } returns tagSet
coEvery { mockedRepository.getUserProfile("u0") } returns userProfile
coEvery { mockedRepository.getUserProfilePicture("u0") } returns null
every { mockedNetworkService.isOnline } returns isOnline
mockkStatic(BitmapFactory::class)
every { BitmapFactory.decodeStream(any()) } returns null
runBlocking {
homeScreenViewModel =
HomeScreenViewModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -37,6 +38,7 @@ class CreateProfileViewModelTest {
Dispatchers.setMain(UnconfinedTestDispatcher())
authenticationService.userID = "test_user_id"
every { mockedNetworkService.isOnline } returns MutableStateFlow(true)
coEvery { repository.getUserProfilePicture(any()) } returns null
viewModel = CreateProfileViewModel(authenticationService, repository, mockedNetworkService)
}

Expand Down Expand Up @@ -127,4 +129,10 @@ class CreateProfileViewModelTest {
val tagList1 = viewModel.tagList.value
assertFalse(tagList1.contains(tag))
}

@Test
fun setNullProfilePictureTest() {
viewModel.setPicture(null)
assertNull(viewModel.picture.value)
}
}