guess-It-SDP/guessIt

View on GitHub
app/src/main/java/com/github/freeman/bootcamp/EditProfileActivity.kt

Summary

Maintainability
F
5 days
Test Coverage
B
89%
package com.github.freeman.bootcamp

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import coil.compose.rememberAsyncImagePainter
import com.github.freeman.bootcamp.EditProfileActivity.Companion.CHOOSE_USERNAME
import com.github.freeman.bootcamp.EditProfileActivity.Companion.DONE
import com.github.freeman.bootcamp.EditProfileActivity.Companion.EMPTY_ERROR
import com.github.freeman.bootcamp.EditProfileActivity.Companion.ENTER_VALUE
import com.github.freeman.bootcamp.EditProfileActivity.Companion.SET_VALUE
import com.github.freeman.bootcamp.EditProfileActivity.Companion.TOPBAR_TEXT
import com.github.freeman.bootcamp.EditProfileActivity.Companion.USER_NAME
import com.github.freeman.bootcamp.ui.theme.BootcampComposeTheme
import com.github.freeman.bootcamp.utilities.firebase.FirebaseUtilities
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.ktx.storage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException

/**
 * Activity where you can edit your profile information
 */
class EditProfileActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val dbRef = Firebase.database.reference
            val storageRef = Firebase.storage.reference
            val userId = Firebase.auth.currentUser?.uid
            val dbUserRef = dbRef.child(getString(R.string.profiles_path))
                .child(userId.toString())
            val storageUserRef = storageRef.child(getString(R.string.profiles_path))
                .child(userId.toString())

            val displayName = remember { mutableStateOf("") }
            val profilePicBitmap = remember { mutableStateOf<Bitmap?>(null) }

            // get name from database
            FirebaseUtilities.databaseGet(dbUserRef.child(getString(R.string.username_path)))
                .thenAccept {
                    displayName.value = it
                }

            // get User's image from firebase storage
            LaunchedEffect(Unit) {
                FirebaseUtilities.storageGet(storageUserRef.child(getString(R.string.picture_path)))
                    .thenAccept {
                        profilePicBitmap.value = it
                    }
            }

            BootcampComposeTheme {
                Surface {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        TopAppbarEditProfile()
                        EditUserDetails(displayName = displayName, profilePic = profilePicBitmap)
                    }
                }
            }

        }
    }

    companion object {
        const val TOPBAR_TEXT = "Profile"
        const val SET_VALUE = "Set value"
        const val ENTER_VALUE = "Enter value"
        const val DONE = "Enter value"
        const val EMPTY_ERROR = "Field can not be empty"
        const val USER_NAME = "NAME"
        const val CHOOSE_USERNAME = "Choose username"
    }

}

// list of editable option available global to the activity
private val editablesList: ArrayList<EditableData> = ArrayList()

@Composable
fun EditUserDetails(context: Context = LocalContext.current, displayName: MutableState<String>, profilePic: MutableState<Bitmap?>) {
    val dbRef = Firebase.database.reference
    val storageRef = Firebase.storage.reference
    val userId = Firebase.auth.currentUser?.uid
    val showNameDialog = remember { mutableStateOf(false) }

    // stores data for images chosen in phone storage
    var imageUri by remember { mutableStateOf<Uri?>(null) }
    val image = remember { mutableStateOf(byteArrayOf()) }

    // This indicates if the optionsList has data or not
    // Initially, the list is empty. So, its value is false.
    var listPrepared by remember {
        mutableStateOf(false)
    }
    LaunchedEffect(Unit) {
        withContext(Dispatchers.Default) {
            editablesList.clear()

            // Add the data to editablesList
            prepareEditableItemsData(displayName, showNameDialog)

            listPrepared = true
        }
    }

    if (listPrepared) {
        // Edit name dialog
        if (showNameDialog.value) {
            EditDialog(
                text = displayName,
                setValue = CHOOSE_USERNAME,
                updateData = { name ->
                    dbRef.child(context.getString(R.string.profiles_path))
                        .child(userId.toString())
                        .child(context.getString(R.string.username_path))
                        .setValue(name)
                },
                show = showNameDialog
            )
        }

        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
                .testTag("editUserDetails"),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {

            item {


                if (profilePic.value != null) {

                    // Enables choosing an image in the phone storage and sens it to the database
                    val launcher = rememberLauncherForActivityResult(contract =
                    ActivityResultContracts.GetContent()) { uri: Uri? ->
                        imageUri = uri
                        image.value = readBytes(context, imageUri!!)!!
                        val uploadTask = storageRef
                            .child(context.getString(R.string.profiles_path))
                            .child(userId.toString())
                            .child(context.getString(R.string.picture_path))
                            .putBytes(image.value)
                        uploadTask.addOnFailureListener {
                            // Handle unsuccessful uploads
                        }.addOnSuccessListener {
                            // taskSnapshot.metadata contains file metadata such as size, content-type, etc.
                        }

                    }

                    // Actual user image
                    Image(
                        painter = rememberAsyncImagePainter(if (imageUri != null) imageUri else profilePic.value),
                        contentScale = ContentScale.Crop,
                        contentDescription = null,
                        modifier = Modifier
                            .size(200.dp)
                            .clip(CircleShape)
                            .clickable {
                                launcher.launch("image/*")
                            }
                    )
                }
            }

            // Show the editable options
            items(editablesList) { item ->
                EditableItemStyle(
                    item = item
                )
            }
        }
    }
}

@Composable
fun TopAppbarEditProfile(context: Context = LocalContext.current) {

    TopAppBar(
        modifier = Modifier.testTag("topAppbarEditProfile"),
        title = {
            Text(
                modifier = Modifier.testTag("topAppbarEditProfileTitle"),
                text = TOPBAR_TEXT,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.primary,
                fontSize = 20.sp
            )
        },
        backgroundColor = MaterialTheme.colorScheme.background,
        elevation = 4.dp,
        navigationIcon = {
            IconButton(
                modifier = Modifier
                    .testTag("appBarBack"),
                onClick = {
                val activity = (context as? Activity)
                activity?.finish()

            }) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = "Go back",
                    tint = MaterialTheme.colorScheme.primary
                )
            }
        }
    )
}

// Dialog that edits any field given in argument
@Composable
fun EditDialog(
    text: MutableState<String>,
    setValue: String = SET_VALUE,
    enterValue: String = ENTER_VALUE,
    keyboardType: KeyboardType = KeyboardType.Text,
    show: MutableState<Boolean>,
    updateData: (String) -> Unit
) {

    val txtFieldError = remember { mutableStateOf("") }
    val txtField = remember { mutableStateOf(text.value) }


    Dialog(
        onDismissRequest = { show.value = false }
    ) {
        Surface(
            modifier = Modifier.testTag("customDialog"),
            shape = RoundedCornerShape(16.dp),
            color = MaterialTheme.colorScheme.primaryContainer
        ) {
            Box(
                contentAlignment = Alignment.Center
            ) {
                Column(modifier = Modifier.padding(20.dp)) {

                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.SpaceBetween,
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Text(
                            text = setValue,
                            style = TextStyle(
                                fontSize = 24.sp,
                                fontFamily = FontFamily.Default,
                                fontWeight = FontWeight.Bold
                            ),
                            color = MaterialTheme.colorScheme.onPrimaryContainer
                        )
                        Icon(
                            imageVector = Icons.Rounded.Delete,
                            contentDescription = "",
                            tint = MaterialTheme.colorScheme.onPrimaryContainer,
                            modifier = Modifier
                                .width(30.dp)
                                .height(30.dp)
                                .clickable { show.value = false }
                        )
                    }

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

                    TextField(
                        keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
                        modifier = Modifier
                            .testTag("dialogTextField")
                            .fillMaxWidth()
                            .border(
                                BorderStroke(
                                    width = 2.dp,
                                    color =
                                    if (txtFieldError.value.isEmpty())
                                        MaterialTheme.colorScheme.primary
                                    else Color.Red
                                ),
                                shape = RoundedCornerShape(50)
                            ),
                        colors = TextFieldDefaults.textFieldColors(
                            textColor = MaterialTheme.colorScheme.primary,
                            disabledTextColor =  MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
                            backgroundColor = Color.Transparent,
                            focusedIndicatorColor = Color.Transparent,
                            unfocusedIndicatorColor = Color.Transparent
                        ),
                        leadingIcon = {
                            Icon(
                                imageVector = Icons.Rounded.Person,
                                contentDescription = "",
                                tint = MaterialTheme.colorScheme.primary,
                                modifier = Modifier
                                    .width(20.dp)
                                    .height(20.dp)
                            )
                        },
                        placeholder = {
                            Text(text = enterValue)
                        },
                        value = txtField.value,
                        onValueChange = {
                            txtField.value = it
                        }
                    )

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

                    Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
                        Button(
                            onClick = {
                                if (txtField.value.isEmpty()) {
                                    txtFieldError.value = EMPTY_ERROR
                                    return@Button
                                }
                                updateData(txtField.value)
                                text.value = txtField.value
                                show.value = false
                            },
                            shape = RoundedCornerShape(50.dp),
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(50.dp)
                                .testTag("doneButton"),
                            colors = ButtonDefaults.buttonColors(
                                backgroundColor = MaterialTheme.colorScheme.primary,
                                contentColor = MaterialTheme.colorScheme.primaryContainer
                            ),
                            content = {
                                Text(
                                    text = DONE,
                                    color = MaterialTheme.colorScheme.primaryContainer
                                )
                            }
                        )
                    }
                }
            }
        }

    }
}

@Composable
private fun EditableItemStyle(item: EditableData) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(enabled = true) {
                item.clickAction()
            }
            .padding(all = 16.dp)
            .testTag("editOptionsItemStyle"),
        verticalAlignment = Alignment.CenterVertically
    ) {

        Icon(
            modifier = Modifier
                .size(32.dp),
            imageVector = item.icon,
            contentDescription = item.title,
            tint = MaterialTheme.colorScheme.primary
        )

        Row(
            modifier = Modifier
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Column(
                modifier = Modifier
                    .weight(weight = 3f, fill = false)
                    .padding(start = 16.dp)
            ) {

                // Title
                Text(
                    text = item.title,
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.primary,
                    fontSize = 18.sp
                )

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

                // editable data
                Text(
                    text = item.subTitle.value,
                    style = MaterialTheme.typography.titleSmall,
                    color = MaterialTheme.colorScheme.tertiary,
                    fontSize = 18.sp
                )

            }

            // Edit icon
            Icon(
                modifier = Modifier
                    .weight(weight = 1f, fill = false),
                imageVector = Icons.Outlined.Edit,
                contentDescription = item.title,
                tint = MaterialTheme.colorScheme.primary
            )
        }
    }
}

/**
 * Fills the global editable options list with appropriate options
 *
 * @param displayName name of the user
 * @param showNameDialog the state of the dialog that edits the name
 */
private fun prepareEditableItemsData(displayName: MutableState<String>, showNameDialog: MutableState<Boolean>) {

    val appIcons = Icons.Rounded

    editablesList.add(
        EditableData(
            icon = appIcons.Person,
            title = USER_NAME,
            subTitle = displayName,
            clickAction = {
                showNameDialog.value = true
            }
        )
    )

}

/**
 * Converts a URI into an array of bytes
 * @param context current context
 * @param uri URI to be converted
 */
@SuppressLint("Recycle")
@Throws(IOException::class)
private fun readBytes(context: Context, uri: Uri): ByteArray? =
    context.contentResolver.openInputStream(uri)?.buffered()?.use { it.readBytes() }

/**
 * Represents an editable option
 * @param icon icon that represents the field
 * @param title main title of the field
 * @param subTitle current editable data contained in the field
 * @param clickAction what to do when the field is clicked
 */
data class EditableData(val icon: ImageVector, val title: String, val subTitle: MutableState<String>, val clickAction: () -> Unit)