proton-mail-android/app/src/main/java/ch/protonmail/android/contacts/details/presentation/ContactDetailsActivity.kt

433 lines
16 KiB
Kotlin

/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.contacts.details.presentation
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import ch.protonmail.android.R
import ch.protonmail.android.activities.StartCompose
import ch.protonmail.android.contacts.details.edit.EditContactDetailsActivity
import ch.protonmail.android.contacts.details.presentation.model.ContactDetailsUiItem
import ch.protonmail.android.contacts.details.presentation.model.ContactDetailsViewState
import ch.protonmail.android.databinding.ActivityContactDetailsBinding
import ch.protonmail.android.usecase.create.VCARD_TEMP_FILE_NAME
import ch.protonmail.android.utils.FileHelper
import ch.protonmail.android.utils.extensions.showToast
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showTwoButtonInfoDialog
import ch.protonmail.android.views.ListItemThumbnail
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.util.kotlin.EMPTY_STRING
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@AndroidEntryPoint
class ContactDetailsActivity : AppCompatActivity() {
private lateinit var thumbnailImage: ImageView
private lateinit var detailsAdapter: ContactDetailsAdapter
private lateinit var thumbnail: ListItemThumbnail
private lateinit var contactNameView: TextView
private lateinit var detailsContainer: NestedScrollView
private lateinit var progressBar: ProgressBar
private var contactId: String = EMPTY_STRING
private var vCardToShare: String = EMPTY_STRING
private var contactEmail: String = EMPTY_STRING
private var contactPhone: String = EMPTY_STRING
private var decryptedCardType0: String? = null
private var decryptedCardType1: String? = null
private var decryptedCardType2: String? = null
private var decryptedCardType3: String? = null
private val viewModel: ContactDetailsViewModel by viewModels()
private val startComposeLauncher = registerForActivityResult(StartCompose()) { messageId ->
messageId?.let {
val snack = Snackbar.make(
findViewById(R.id.contact_details_layout),
R.string.snackbar_message_draft_saved,
Snackbar.LENGTH_LONG
)
snack.setAction(R.string.move_to_trash) {
viewModel.moveDraftToTrash(messageId)
Snackbar.make(
findViewById(R.id.contact_details_layout),
R.string.snackbar_message_draft_moved_to_trash,
Snackbar.LENGTH_LONG
).show()
}
snack.show()
}
}
@Inject
lateinit var fileHelper: FileHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityContactDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar as Toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.contact_details)
}
progressBar = binding.progressBarContactDetails
detailsContainer = binding.scrollViewContactDetails
contactNameView = binding.textViewContactDetailsContactName
thumbnail = binding.thumbnailContactDetails
thumbnailImage = binding.imageViewContactDetailsThumbnail
val itemDecoration = DividerItemDecoration(
this,
LinearLayout.VERTICAL
).apply {
getDrawable(R.drawable.list_divider)?.let {
setDrawable(it)
}
}
detailsAdapter = ContactDetailsAdapter(
::onWriteToContact,
::onCallContact,
::onAddressClicked,
::onUrlClicked
)
binding.recyclerViewContactsDetails.apply {
layoutManager = LinearLayoutManager(context)
adapter = detailsAdapter
addItemDecoration(itemDecoration)
}
binding.includeContactDetailsButtons.textViewContactDetailsCompose.setOnClickListener {
onWriteToContact(contactEmail)
}
binding.includeContactDetailsButtons.textViewContactDetailsCall.setOnClickListener {
onCallContact(contactPhone)
}
binding.includeContactDetailsButtons.textViewContactDetailsShare.setOnClickListener {
onShare(contactNameView.text.toString(), vCardToShare, this)
}
viewModel.contactsViewState
.onEach { renderState(it) }
.launchIn(lifecycleScope)
viewModel.vCardSharedFlow
.onEach { shareVcard(it, contactNameView.text.toString()) }
.launchIn(lifecycleScope)
val contactId = requireNotNull(intent.extras?.getString(EXTRA_ARG_CONTACT_ID))
viewModel.getContactDetails(contactId)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_contact_details, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_contact_details_edit -> onEditContacts()
R.id.action_contact_details_delete ->
onDeleteContact(
requireNotNull(intent.extras?.getString(EXTRA_ARG_CONTACT_ID)),
contactNameView.text.toString()
)
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
super.onBackPressed()
finish()
}
private fun onEditContacts() {
val vCardFilePath1 = if (!decryptedCardType1.isNullOrEmpty()) {
val filePath = "$cacheDir${File.separator}$VCARD_TEMP_FILE_NAME"
fileHelper.saveStringToFile(filePath, checkNotNull(decryptedCardType1))
filePath
} else {
EMPTY_STRING
}
val vCardFilePath3 = if (!decryptedCardType3.isNullOrEmpty()) {
val filePath = "$cacheDir${File.separator}$VCARD_TEMP_FILE_NAME"
fileHelper.saveStringToFile(filePath, checkNotNull(decryptedCardType3))
filePath
} else {
EMPTY_STRING
}
EditContactDetailsActivity.startEditContactActivity(
this, contactId,
EditContactDetailsActivity.REQUEST_CODE_EDIT_CONTACT,
decryptedCardType0,
vCardFilePath1,
decryptedCardType2,
vCardFilePath3
)
}
private fun onDeleteContact(contactId: String, contactName: String) {
val clickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
dialog.dismiss()
if (which == DialogInterface.BUTTON_POSITIVE) {
viewModel.deleteContact(contactId)
finish()
}
}
if (!isFinishing) {
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.confirm)
.setMessage(String.format(getString(R.string.delete_contact), contactName))
.setNegativeButton(R.string.no, clickListener)
.setPositiveButton(R.string.yes, clickListener)
.create()
.show()
}
}
private fun renderState(state: ContactDetailsViewState) {
Timber.v("State $state received")
when (state) {
is ContactDetailsViewState.Loading -> showLoading()
is ContactDetailsViewState.Error -> showError(state.exception)
is ContactDetailsViewState.Data -> showData(state)
}
}
private fun showError(exception: Throwable) {
progressBar.isVisible = false
detailsContainer.isVisible = false
Timber.i(exception, "Fetching contacts data has failed")
}
private fun showLoading() {
progressBar.isVisible = true
detailsContainer.isVisible = false
}
private fun showData(state: ContactDetailsViewState.Data) {
progressBar.isVisible = false
detailsContainer.isVisible = true
vCardToShare = state.vCardToShare
contactId = state.contactId
decryptedCardType0 = state.vDecryptedCardType0
decryptedCardType1 = state.vDecryptedCardType1
decryptedCardType2 = state.vDecryptedCardType2
decryptedCardType3 = state.vDecryptedCardType3
setHeaderData(
state.title,
state.initials,
state.photoUrl,
state.photoBytes
)
detailsAdapter.submitList(state.contactDetailsItems)
state.contactDetailsItems.onEach { item ->
setContactDataForActionButtons(item)
}
}
private fun setHeaderData(
title: String,
initials: String,
photoUrl: String?,
photoBytes: List<Byte>?
) {
contactNameView.text = title
thumbnail.bind(isSelectedActive = false, isMultiselectActive = false, initials = initials)
if (!photoBytes.isNullOrEmpty()) {
setThumbnailImage(photoBytes)
} else if (photoUrl != null) {
showTwoButtonInfoDialog(
titleStringId = R.string.contact_details_remote_content_dialog_title,
messageStringId = R.string.contact_details_remote_content_dialog_message,
positiveStringId = R.string.contact_details_remote_content_dialog_positive_button,
cancelable = false,
cancelOnTouchOutside = false,
onNegativeButtonClicked = { setPlaceholderThumbnailImage() }
) { loadThumbnailImage(photoUrl) }
}
}
private fun setPlaceholderThumbnailImage() {
thumbnailImage.apply {
setImageResource(R.drawable.ic_file_image)
background = ContextCompat.getDrawable(
this@ContactDetailsActivity,
R.drawable.circle_background_interaction_weak
)
setPadding(resources.getDimensionPixelSize(R.dimen.padding_4xl))
isVisible = true
}
thumbnail.isVisible = false
}
private fun setThumbnailImage(photoBytes: List<Byte>) {
val byteArray = photoBytes.toByteArray()
val imageBitmap: Bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val roundedImage = RoundedBitmapDrawableFactory.create(resources, imageBitmap).apply {
setAntiAlias(true)
cornerRadius = resources.getDimensionPixelSize(R.dimen.avatar_size) / 2f
}
thumbnailImage.apply {
setImageDrawable(roundedImage)
isVisible = true
}
thumbnail.isVisible = false
}
private fun loadThumbnailImage(photoUrl: String?) {
Timber.v("Loading photo url: $photoUrl")
val targetSize = resources.getDimensionPixelSize(R.dimen.avatar_size)
val imageRequest = ImageRequest.Builder(this)
.data(photoUrl)
.size(targetSize, targetSize)
.transformations(CircleCropTransformation())
.target(
onSuccess = { drawable ->
Timber.d("Thumbnail loading finished")
thumbnailImage.apply {
setImageDrawable(drawable)
isVisible = true
}
thumbnail.isVisible = false
},
onError = {
Timber.i("Thumbnail loading error")
thumbnailImage.isVisible = false
thumbnail.isVisible = true
}
)
.build()
ImageLoader(this).enqueue(imageRequest)
}
private fun setContactDataForActionButtons(item: ContactDetailsUiItem) {
if (item is ContactDetailsUiItem.Email) {
contactEmail = item.value
}
if (item is ContactDetailsUiItem.TelephoneNumber) {
contactPhone = item.value
}
}
private fun onWriteToContact(emailAddress: String) {
if (emailAddress.isNotEmpty()) {
startComposeLauncher.launch(StartCompose.Input(toRecipients = listOf(emailAddress)))
} else {
showToast(R.string.email_empty, Toast.LENGTH_SHORT)
}
}
private fun onCallContact(phoneNumber: String) {
Timber.v("On Call $phoneNumber")
if (phoneNumber.isNotEmpty()) {
val callIntent = Intent(Intent.ACTION_DIAL)
callIntent.data = Uri.parse("tel:$phoneNumber")
startActivity(callIntent)
} else {
showToast(R.string.contact_vcard_new_row_phone, Toast.LENGTH_SHORT)
}
}
private fun onShare(contactName: String, vCardToShare: String, context: Context) {
if (vCardToShare.isNotEmpty()) {
viewModel.saveVcard(vCardToShare, contactName, context)
} else {
showToast(R.string.default_error_message, Toast.LENGTH_SHORT)
}
}
private fun shareVcard(vcfFileUri: Uri, contactName: String) {
if (vcfFileUri != Uri.EMPTY) {
Timber.v("Share contact uri: $vcfFileUri")
val intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, vcfFileUri)
type = ContactsContract.Contacts.CONTENT_VCARD_TYPE
}
startActivity(Intent.createChooser(intent, contactName))
}
}
private fun onAddressClicked(address: String) {
onUrlClicked("geo:0,0?q=$address")
}
private fun onUrlClicked(url: String) {
Timber.v("On Url clicked $url")
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(url)
)
startActivity(intent)
}
companion object {
const val EXTRA_ARG_CONTACT_ID = "extra_arg_contact_id"
}
}