Skip to content

Commit

Permalink
Merge pull request #249 from SwEnt-Group13/feat/manage-pictures
Browse files Browse the repository at this point in the history
Feat/manage pictures
  • Loading branch information
Aurelien9Code authored Dec 5, 2024
2 parents df33c92 + dba4fad commit 662b7a8
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.android.unio.components.authentication

import android.net.Uri
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.filters.LargeTest
import com.android.unio.TearDown
import com.android.unio.model.strings.test_tags.PictureSelectionToolTestTags
import com.android.unio.ui.components.PictureSelectionTool
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.auth.internal.zzac
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@LargeTest
@HiltAndroidTest
class PictureSelectionToolTest : TearDown() {

@MockK private lateinit var firebaseAuth: FirebaseAuth
@MockK private lateinit var mockFirebaseUser: zzac

@get:Rule val composeTestRule = createComposeRule()
@get:Rule val hiltRule = HiltAndroidRule(this)

@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)

// Mocking Firebase.auth and its behavior
mockkStatic(FirebaseAuth::class)
every { Firebase.auth } returns firebaseAuth
every { firebaseAuth.currentUser } returns mockFirebaseUser
every { mockFirebaseUser.uid } returns "mocked-uid"
}

@Test
fun testInitialUIState() {
composeTestRule.setContent {
PictureSelectionTool(
maxPictures = 3,
allowGallery = true,
allowCamera = true,
onValidate = {},
onCancel = {},
initialSelectedPictures = emptyList())
}
// Verify that initial UI elements are displayed
composeTestRule.onNodeWithTag(PictureSelectionToolTestTags.GALLERY_ADD).assertIsDisplayed()
composeTestRule.onNodeWithTag(PictureSelectionToolTestTags.CAMERA_ADD).assertIsDisplayed()
composeTestRule.onNodeWithTag(PictureSelectionToolTestTags.CANCEL_BUTTON).assertIsDisplayed()
}

@Test
fun testHavingPictures() {
val mockUri1 = mockk<Uri>(relaxed = true)

composeTestRule.setContent {
PictureSelectionTool(
maxPictures = 3,
allowGallery = true,
allowCamera = true,
onValidate = {},
onCancel = {},
initialSelectedPictures = listOf(mockUri1))
}
// Verify that the selected pictures are displayed
composeTestRule.onNodeWithTag(PictureSelectionToolTestTags.SELECTED_PICTURE).assertIsDisplayed()
// Ensure that the Validate button is now visible
composeTestRule.onNodeWithTag(PictureSelectionToolTestTags.VALIDATE_BUTTON).assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class UserAccountCreationTest : EndToEndTest() {
composeTestRule.onNodeWithTag(InterestsOverlayTestTags.SAVE_BUTTON).assertIsDisplayed()
composeTestRule.onNodeWithTag(InterestsOverlayTestTags.SAVE_BUTTON).performClick()

composeTestRule.onNodeWithTag(AccountDetailsTestTags.PROFILE_PICTURE_ICON).assertIsDisplayed()
composeTestRule.onNodeWithTag(AccountDetailsTestTags.PROFILE_PICTURE_ICON).performClick()

composeTestRule
.onNodeWithTag(AccountDetailsTestTags.CONTINUE_BUTTON)
.assertDisplayComponentInScroll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,14 @@ object WelcomeTestTags {
const val BUTTON = "welcomeButton"
const val FORGOT_PASSWORD = "welcomeForgotPassword"
}

object PictureSelectionToolTestTags {
const val SELECTED_PICTURE = "pictureSelectionToolSelectedPicture"
const val REMOVE_PICTURE = "pictureSelectionToolRemovePicture"
const val GALLERY_ADD = "pictureSelectionToolGalleryAdd"
const val CAMERA_ADD = "pictureSelectionToolCameraAdd"
const val VALIDATE_BUTTON = "pictureSelectionToolValidateButton"
const val CANCEL_BUTTON = "pictureSelectionToolCancelButton"
const val NEW_PROFILE_PICTURE = "new_profile_picture.jpg"
const val IMAGE_JPEG = "image/jpeg"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.android.unio.ui.components

import android.content.ContentValues
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.android.unio.R
import com.android.unio.model.strings.test_tags.PictureSelectionToolTestTags

/**
* A composable function to select and display pictures from the gallery or camera. It allows the
* user to add pictures, see selected pictures in a grid, and validate the selection.
*
* @param maxPictures The maximum number of pictures that can be selected.
* @param allowGallery Boolean to specify if selecting from the gallery is allowed.
* @param allowCamera Boolean to specify if taking pictures with the camera is allowed.
* @param onValidate Lambda to handle the selected pictures after validation.
* @param onCancel Lambda to handle the cancellation of the selection.
*/
@Composable
fun PictureSelectionTool(
maxPictures: Int,
allowGallery: Boolean,
allowCamera: Boolean,
onValidate: (List<Uri>) -> Unit,
onCancel: () -> Unit,
initialSelectedPictures: List<Uri>
) {
val context = LocalContext.current

val selectedPictures = remember { mutableStateListOf<Uri>() }

// Initialize selectedPictures with initialSelectedPictures
LaunchedEffect(initialSelectedPictures) {
selectedPictures.clear() // Clear existing pictures, if any
selectedPictures.addAll(initialSelectedPictures) // Add the initial selected pictures
}

// Launcher for selecting multiple images from the gallery
val pickMediaLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
if (uris != null) {
selectedPictures.addAll(uris.take(maxPictures - selectedPictures.size))
}
}

val cameraImageUri = remember { mutableStateOf<Uri?>(null) }
// Launcher for taking a picture with the camera
val takePictureLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success && cameraImageUri.value != null) {
selectedPictures.add(cameraImageUri.value!!)
}
}

/**
* Creates a temporary URI for storing an image taken by the camera.
*
* @return A URI pointing to the location where the new image will be stored.
*/
fun createImageUri(): Uri {
val resolver = context.contentResolver
val contentValues =
ContentValues().apply {
put(
MediaStore.Images.Media.DISPLAY_NAME,
PictureSelectionToolTestTags.NEW_PROFILE_PICTURE)
put(MediaStore.Images.Media.MIME_TYPE, PictureSelectionToolTestTags.IMAGE_JPEG)
}
return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
}

// User interface for the picture selection tool
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp)) {
Text(
context.getString(R.string.selection_tool_selected_picture_text) +
": ${selectedPictures.size}/$maxPictures")

// Display selected pictures in a scrollable grid with a fixed number of columns
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
contentPadding = PaddingValues(4.dp)) {
items(selectedPictures.size) { index ->
val uri = selectedPictures[index]
Box(modifier = Modifier.size(100.dp).clip(CircleShape).padding(4.dp)) {
// Display the selected image
Image(
painter = rememberAsyncImagePainter(uri),
contentDescription =
context.getString(R.string.selection_tool_selected_picture_text),
contentScale = ContentScale.Crop,
modifier =
Modifier.fillMaxSize().testTag(PictureSelectionToolTestTags.SELECTED_PICTURE))
Icon(
Icons.Default.Close,
contentDescription = context.getString(R.string.selection_tool_remove_picture),
modifier =
Modifier.align(Alignment.TopEnd)
.clickable { selectedPictures.remove(uri) }
.padding(4.dp)
.testTag(PictureSelectionToolTestTags.REMOVE_PICTURE))
}
}
}

Spacer(modifier = Modifier.height(16.dp))

// Buttons to add pictures from the gallery or camera
Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) {
// Add picture from the gallery if allowed and the max limit isn't reached
if (allowGallery && selectedPictures.size < maxPictures) {
OutlinedButton(
onClick = {
pickMediaLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
},
modifier = Modifier.testTag(PictureSelectionToolTestTags.GALLERY_ADD)) {
Icon(
Icons.Default.Add,
contentDescription =
context.getString(R.string.selection_tool_add_gallery_content_description))
Text(context.getString(R.string.selection_tool_gallery_text))
}
}

// Take a picture with the camera if allowed and the max limit isn't reached
if (allowCamera && selectedPictures.size < maxPictures) {
OutlinedButton(
onClick = {
val uri = createImageUri() // Local stable reference
cameraImageUri.value = uri
takePictureLauncher.launch(uri)
},
modifier = Modifier.testTag(PictureSelectionToolTestTags.CAMERA_ADD)) {
Icon(
Icons.Default.Add,
contentDescription = context.getString(R.string.selection_tool_take_picture))
Text(context.getString(R.string.selection_tool_camera_text))
}
}
}

Spacer(modifier = Modifier.height(16.dp))

// Buttons for validating or canceling the picture selection
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Button(
onClick = { onValidate(selectedPictures) },
modifier = Modifier.testTag(PictureSelectionToolTestTags.VALIDATE_BUTTON)) {
Icon(
Icons.Default.Check,
contentDescription = context.getString(R.string.selection_tool_validate_text))
Text(context.getString(R.string.selection_tool_validate_text))
}

OutlinedButton(
onClick = onCancel,
modifier = Modifier.testTag(PictureSelectionToolTestTags.CANCEL_BUTTON)) {
Text(context.getString(R.string.selection_tool_cancel_text))
}
}
}
}
Loading

0 comments on commit 662b7a8

Please sign in to comment.