EditSendContactViewModel.kt 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package ch.threema.app.mediaattacher
  2. import android.content.Context
  3. import android.net.Uri
  4. import androidx.lifecycle.LiveData
  5. import androidx.lifecycle.MutableLiveData
  6. import androidx.lifecycle.ViewModel
  7. import androidx.lifecycle.viewModelScope
  8. import ch.threema.app.files.AppDirectoryProvider
  9. import ch.threema.app.utils.FileUtil
  10. import ch.threema.app.utils.VCardExtractor
  11. import ch.threema.base.utils.getThreemaLogger
  12. import ch.threema.common.DispatcherProvider
  13. import ch.threema.common.takeUnlessEmpty
  14. import ezvcard.Ezvcard
  15. import ezvcard.VCard
  16. import ezvcard.property.FormattedName
  17. import ezvcard.property.StructuredName
  18. import ezvcard.property.VCardProperty
  19. import java.io.BufferedReader
  20. import java.io.File
  21. import java.io.InputStreamReader
  22. import kotlinx.coroutines.launch
  23. import kotlinx.coroutines.withContext
  24. private val logger = getThreemaLogger("EditSendContactViewModel")
  25. /**
  26. * Contains the data needed in the EditSendContactActivity.
  27. */
  28. class EditSendContactViewModel(
  29. private val appContext: Context,
  30. private val vCardExtractor: VCardExtractor,
  31. private val appDirectoryProvider: AppDirectoryProvider,
  32. private val dispatcherProvider: DispatcherProvider,
  33. ) : ViewModel() {
  34. /* The currently shown formatted name in the edit texts */
  35. private val formattedName: MutableLiveData<FormattedName> = MutableLiveData()
  36. /* The currently shown structured name in the edit texts */
  37. private val structuredName: MutableLiveData<StructuredName> = MutableLiveData()
  38. /* The properties (except the name properties) */
  39. private val properties: MutableLiveData<MutableMap<VCardProperty, Boolean>> = MutableLiveData()
  40. /* The modified contact that should be sent */
  41. private val modifiedContact: MutableLiveData<Pair<String, File>> = MutableLiveData()
  42. /* The state of the bottom sheet */
  43. var bottomSheetExpanded: Boolean = false
  44. /**
  45. * Get formatted name live data
  46. */
  47. fun getFormattedName(): LiveData<FormattedName> = formattedName
  48. /**
  49. * Get structured name live data
  50. */
  51. fun getStructuredName(): LiveData<StructuredName> = structuredName
  52. /**
  53. * Get property live data
  54. */
  55. fun getProperties(): LiveData<MutableMap<VCardProperty, Boolean>> = properties
  56. /**
  57. * Get the modified contact (ready to be sent)
  58. * @return a pair with the name of the contact and a vCard file
  59. */
  60. fun getModifiedContact(): LiveData<Pair<String, File>> = modifiedContact
  61. /**
  62. * Initializes the view model based on the given contact uri.
  63. */
  64. fun initializeContact(contactUri: Uri) {
  65. if (properties.value != null) {
  66. return
  67. }
  68. viewModelScope.launch {
  69. val vCard = readVCard(contactUri)
  70. if (vCard == null) {
  71. logger.warn("vCard was null")
  72. structuredName.postValue(StructuredName())
  73. } else if (createFormattedName(vCard) == null && !vCard.formattedName?.value.isNullOrEmpty()) {
  74. formattedName.postValue(vCard.formattedName)
  75. } else {
  76. structuredName.postValue(vCard.structuredName ?: StructuredName())
  77. }
  78. properties.postValue(
  79. vCard?.properties?.associateWith { true }?.toMutableMap() ?: mutableMapOf(),
  80. )
  81. }
  82. }
  83. private suspend fun readVCard(contactUri: Uri): VCard? = withContext(dispatcherProvider.io) {
  84. appContext.contentResolver.openInputStream(contactUri)
  85. .use { inputStream ->
  86. BufferedReader(InputStreamReader(inputStream)).useLines { l -> l.joinToString("\n") }
  87. }
  88. .let { vCardString ->
  89. Ezvcard.parse(vCardString).first()
  90. }
  91. }
  92. /**
  93. * Get the formatted name and the vCard as file containing the selected properties.
  94. */
  95. fun prepareFinalVCard(contactUri: Uri) {
  96. viewModelScope.launch(dispatcherProvider.io) {
  97. val vCard = VCard()
  98. if (structuredName.value != null) {
  99. vCard.setProperty(structuredName.value)
  100. vCard.setProperty(
  101. FormattedName(
  102. createFormattedName(vCard) ?: "",
  103. ),
  104. )
  105. } else if (formattedName.value != null) {
  106. vCard.setProperty(formattedName.value)
  107. }
  108. // Add selected properties to the vcard
  109. properties.value?.filter { it.value }?.map { it.key }?.forEach {
  110. vCard.addProperty(it)
  111. }
  112. val mimeType = FileUtil.getMimeTypeFromUri(appContext, contactUri)
  113. val modifiedContactFile = File(appDirectoryProvider.cacheDirectory, FileUtil.getDefaultFilename(mimeType))
  114. val writer = Ezvcard.write(vCard).prodId(false)
  115. writer.go(modifiedContactFile)
  116. modifiedContact.postValue((vCard.formattedName?.value ?: "") to modifiedContactFile)
  117. }
  118. }
  119. /**
  120. * Create the formatted name (FN) based on the structured name (N).
  121. */
  122. private fun createFormattedName(vcard: VCard): String? =
  123. vcard.structuredName
  124. ?.let { structuredName ->
  125. try {
  126. vCardExtractor.getText(structuredName, false)
  127. .trim()
  128. .takeUnlessEmpty()
  129. } catch (e: Exception) {
  130. if (e !is VCardExtractor.VCardExtractionException) {
  131. logger.error("Could not extract name of contact", e)
  132. }
  133. null
  134. }
  135. }
  136. }