diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c21045e4..4a913487 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,6 +152,7 @@ dependencies { implementation(platform("com.google.firebase:firebase-bom:32.7.0")) implementation("com.google.firebase:firebase-auth") implementation("com.google.android.gms:play-services-auth:20.7.0") + implementation("com.google.firebase:firebase-storage") // Crashlytics implementation("com.google.firebase:firebase-crashlytics") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53c2a7f5..4a574e33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,10 @@ android:name=".data.receiver.location.LocationUpdateReceiver" android:exported="false"/> + \ No newline at end of file diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt index 61080a26..c1047ef1 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt @@ -134,7 +134,8 @@ private fun EditProfileScreenContent(modifier: Modifier) { dismissProfileChooser = { viewModel.showProfileChooser(false) }, - state.showProfileChooser + state.showProfileChooser, + state.isImageUploadInProgress ) Spacer(modifier = Modifier.height(35.dp)) diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt index b43b335a..1a78cdb3 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt @@ -1,5 +1,6 @@ package com.canopas.yourspace.ui.flow.settings.profile +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.canopas.yourspace.data.models.user.ApiUser @@ -10,12 +11,12 @@ import com.canopas.yourspace.data.service.auth.AuthService import com.canopas.yourspace.data.utils.AppDispatcher import com.canopas.yourspace.ui.navigation.AppDestinations import com.canopas.yourspace.ui.navigation.AppNavigator +import com.google.firebase.storage.FirebaseStorage import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import timber.log.Timber -import java.io.File import javax.inject.Inject @HiltViewModel @@ -45,7 +46,7 @@ class EditProfileViewModel @Inject constructor( lastName = user?.last_name, email = user?.email, phone = user?.phone, - profileUrl = null, + profileUrl = user?.profile_image, enablePhone = user?.auth_type != LOGIN_TYPE_PHONE, enableEmail = user?.auth_type != LOGIN_TYPE_GOOGLE ) @@ -102,9 +103,41 @@ class EditProfileViewModel @Inject constructor( _state.value = _state.value.copy(showProfileChooser = show) } - fun onProfileImageChanged(profileUrl: File?) { - _state.value = _state.value.copy(profileUrl = profileUrl?.path) - onChange() + fun onProfileImageChanged(profileUri: Uri?) { + profileUri?.let { uri -> + uploadProfileImage(uri) + } ?: run { + _state.value = _state.value.copy(profileUrl = null) + onChange() + } + } + + private fun uploadProfileImage(uri: Uri) = viewModelScope.launch(appDispatcher.IO) { + try { + val storage = FirebaseStorage.getInstance() + val storageRef = storage.reference + val fileName = "IMG_${System.currentTimeMillis()}.jpg" + val imageRef = storageRef.child("profile_images/${user?.id}/$fileName") + val uploadTask = imageRef.putFile(uri) + uploadTask.addOnProgressListener { + _state.value = _state.value.copy(isImageUploadInProgress = true) + }.addOnSuccessListener { + imageRef.downloadUrl.addOnSuccessListener { uri -> + _state.value = _state.value.copy( + profileUrl = uri.toString(), + isImageUploadInProgress = false + ) + onChange() + } + }.addOnFailureListener { + Timber.e(it, "Failed to upload profile image") + _state.value = _state.value.copy(profileUrl = null, isImageUploadInProgress = false) + onChange() + } + } catch (e: Exception) { + Timber.e(e, "Failed to upload profile image") + _state.emit(_state.value.copy(isImageUploadInProgress = false, error = e.message)) + } } fun onFirstNameChanged(firstName: String) { @@ -167,5 +200,6 @@ data class EditProfileState( val lastName: String? = null, val email: String? = null, val phone: String? = null, - val profileUrl: String? = null + val profileUrl: String? = null, + val isImageUploadInProgress: Boolean = false ) diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt index bfef1bf0..700de323 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt @@ -2,6 +2,7 @@ package com.canopas.yourspace.ui.flow.settings.profile.component import android.graphics.Bitmap import android.net.Uri +import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.launch @@ -18,9 +19,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -28,7 +31,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -46,38 +48,32 @@ import com.canhub.cropper.CropImageContractOptions import com.canhub.cropper.CropImageOptions import com.canopas.yourspace.R import com.canopas.yourspace.data.models.user.ApiUser +import com.canopas.yourspace.ui.component.AppProgressIndicator import com.canopas.yourspace.ui.component.motionClickEvent import com.canopas.yourspace.ui.flow.settings.ProfileImageView import com.canopas.yourspace.ui.theme.AppTheme -import java.io.File -import java.io.FileOutputStream +import java.io.ByteArrayOutputStream @Composable fun UserProfileView( modifier: Modifier, profileUrl: String?, - onProfileChanged: (File?) -> Unit, + onProfileChanged: (Uri?) -> Unit, onProfileImageClicked: () -> Unit, dismissProfileChooser: () -> Unit, - showProfileChooser: Boolean = false + showProfileChooser: Boolean = false, + isImageUploading: Boolean = false ) { val context = LocalContext.current - val scope = rememberCoroutineScope() var imageUri by remember { mutableStateOf(null) } val imageCropLauncher = rememberLauncherForActivityResult(contract = CropImageContract()) { result -> - result.uriContent?.let { - imageUri = it - val fileName = "IMG_${System.currentTimeMillis()}.jpg" - val file = File(context.cacheDir, "images/$fileName") - val inputStream = context.contentResolver.openInputStream(imageUri!!) - - val outputStream = FileOutputStream(file) - inputStream?.copyTo(outputStream) - inputStream?.close() - outputStream.close() - onProfileChanged(file) + if (result.isSuccessful) { + result.uriContent?.let { + imageUri = it + onProfileChanged(imageUri) + } } } @@ -92,19 +88,16 @@ fun UserProfileView( val cameraLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicturePreview()) { bitmap -> - bitmap?.let { - val fileName = "IMG_${System.currentTimeMillis()}.jpg" - val file = File(context.cacheDir, "images/$fileName") - try { - val out = FileOutputStream(file) - it.compress(Bitmap.CompressFormat.JPEG, 100, out) - out.flush() - out.close() - } catch (e: Exception) { - e.printStackTrace() - } - onProfileChanged(file) + val bytes = ByteArrayOutputStream() + it.compress(Bitmap.CompressFormat.JPEG, 100, bytes) + val path: String = MediaStore.Images.Media.insertImage( + context.contentResolver, + it, + "Title", + null + ) + onProfileChanged(Uri.parse(path)) } } @@ -135,7 +128,9 @@ fun UserProfileView( painter = rememberAsyncImagePainter( ImageRequest.Builder(LocalContext.current).data(data = setProfile).build() ), - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) .border(1.dp, AppTheme.colorScheme.textPrimary, CircleShape) .background(AppTheme.colorScheme.containerHigh, CircleShape) .padding(if (profileUrl == null) 32.dp else 0.dp), @@ -143,14 +138,40 @@ fun UserProfileView( contentDescription = "ProfileImage" ) -// Image( -// painter = painterResource(id = R.drawable.ic_edit_user_profile), -// contentDescription = null, -// modifier = Modifier -// .align(Alignment.BottomEnd) -// .size(32.dp) -// .motionClickEvent { onProfileImageClicked() } -// ) + Box( + modifier = Modifier + .wrapContentSize() + .align(Alignment.BottomEnd) + .clip(CircleShape) + .border(1.dp, AppTheme.colorScheme.textPrimary, CircleShape) + .background(AppTheme.colorScheme.onPrimary, CircleShape) + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + modifier = Modifier + .align(Alignment.Center) + .padding(8.dp) + .size(20.dp) + .motionClickEvent { + if (!isImageUploading) { + onProfileImageClicked() + } + } + ) + } + + if (isImageUploading) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + .background(AppTheme.colorScheme.onPrimary.copy(0.5f), CircleShape), + contentAlignment = Alignment.Center + ) { + AppProgressIndicator() + } + } } }