-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #249 from SwEnt-Group13/feat/manage-pictures
Feat/manage pictures
- Loading branch information
Showing
7 changed files
with
347 additions
and
16 deletions.
There are no files selected for viewing
82 changes: 82 additions & 0 deletions
82
...c/androidTest/java/com/android/unio/components/authentication/PictureSelectionToolTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
app/src/main/java/com/android/unio/ui/components/PictureSelectionTool.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.