ContactAvatarFetcher.kt 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2022-2025 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.glide
  22. import android.content.Context
  23. import android.graphics.Bitmap
  24. import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
  25. import ch.threema.app.R
  26. import ch.threema.app.preference.service.PreferenceService
  27. import ch.threema.app.services.AvatarCacheServiceImpl
  28. import ch.threema.app.services.ContactService
  29. import ch.threema.app.services.UserService
  30. import ch.threema.app.utils.AndroidContactUtil
  31. import ch.threema.app.utils.AvatarConverterUtil
  32. import ch.threema.app.utils.ColorUtil
  33. import ch.threema.app.utils.ContactUtil
  34. import ch.threema.data.models.ContactModel
  35. import ch.threema.data.repositories.ContactModelRepository
  36. import com.bumptech.glide.Priority
  37. import com.bumptech.glide.load.data.DataFetcher
  38. /**
  39. * This class is used to get the avatars from the database or create the default avatars. The results of the loaded bitmaps will be cached by glide (if possible).
  40. * While the name suggests that it can only deal with contacts, it can actually also deal with the user's own profile picture.
  41. * TODO(ANDR-4021): Consider properly generalizing or splitting this class, such that it does no need to
  42. * rely on "fake" contact models for the user itself
  43. */
  44. class ContactAvatarFetcher(
  45. context: Context,
  46. private val userService: UserService?,
  47. private val contactService: ContactService?,
  48. private val contactModelRepository: ContactModelRepository?,
  49. private val contactAvatarConfig: AvatarCacheServiceImpl.ContactAvatarConfig,
  50. private val preferenceService: PreferenceService?,
  51. ) : AvatarFetcher(context) {
  52. private val contactDefaultAvatar: VectorDrawableCompat? by lazy {
  53. VectorDrawableCompat.create(
  54. context.resources,
  55. R.drawable.ic_contact,
  56. null,
  57. )
  58. }
  59. private val contactBusinessAvatar: VectorDrawableCompat? by lazy {
  60. VectorDrawableCompat.create(
  61. context.resources,
  62. R.drawable.ic_business,
  63. null,
  64. )
  65. }
  66. override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
  67. val highRes = contactAvatarConfig.options.highRes
  68. // Show profile picture from contact (if set)
  69. val profilePicReceive: Boolean
  70. // Show default avatar
  71. val defaultAvatar: Boolean
  72. // Return default avatar if no other avatar is found
  73. val returnDefaultIfNone: Boolean
  74. when (contactAvatarConfig.options.defaultAvatarPolicy) {
  75. AvatarOptions.DefaultAvatarPolicy.DEFAULT_FALLBACK -> {
  76. profilePicReceive = preferenceService?.profilePicReceive == true
  77. defaultAvatar = false
  78. returnDefaultIfNone = true
  79. }
  80. AvatarOptions.DefaultAvatarPolicy.CUSTOM_AVATAR -> {
  81. profilePicReceive = true
  82. defaultAvatar = false
  83. returnDefaultIfNone = false
  84. }
  85. AvatarOptions.DefaultAvatarPolicy.DEFAULT_AVATAR -> {
  86. profilePicReceive = false
  87. defaultAvatar = true
  88. returnDefaultIfNone = true
  89. }
  90. }
  91. val backgroundColor = getBackgroundColor(contactAvatarConfig.options)
  92. // TODO(ANDR-4021): It should be possible to load the user's own profile picture without having to extract the identity from a "fake" contact
  93. val identity = contactAvatarConfig.model?.identity
  94. val avatar = if (identity != null && userService?.isMe(identity) == true) {
  95. getUserDefinedProfilePicture(identity, highRes)
  96. ?: if (returnDefaultIfNone) {
  97. buildDefaultAvatar(contactModel = null, highRes, backgroundColor)
  98. } else {
  99. null
  100. }
  101. } else {
  102. val contactModel = identity?.let { contactModelRepository?.getByIdentity(identity) }
  103. if (defaultAvatar) {
  104. buildDefaultAvatar(contactModel, highRes, backgroundColor)
  105. } else {
  106. loadContactAvatar(
  107. contactModel,
  108. highRes,
  109. profilePicReceive,
  110. returnDefaultIfNone,
  111. backgroundColor,
  112. )
  113. }
  114. }
  115. callback.onDataReady(avatar)
  116. }
  117. private fun loadContactAvatar(
  118. contactModel: ContactModel?,
  119. highRes: Boolean,
  120. profilePicReceive: Boolean,
  121. returnDefaultIfNone: Boolean,
  122. backgroundColor: Int,
  123. ): Bitmap? {
  124. if (contactModel == null) {
  125. return buildDefaultAvatar(null, highRes, backgroundColor)
  126. }
  127. // Try the contact defined profile picture
  128. if (profilePicReceive) {
  129. getContactDefinedProfilePicture(contactModel, highRes)?.let {
  130. return it
  131. }
  132. }
  133. // Try the user defined profile picture
  134. getUserDefinedProfilePicture(contactModel.identity, highRes)?.let {
  135. return it
  136. }
  137. // Try the android defined profile picture
  138. getAndroidDefinedProfilePicture(contactModel, highRes)?.let {
  139. return it
  140. }
  141. return if (returnDefaultIfNone) {
  142. buildDefaultAvatar(contactModel, highRes, backgroundColor)
  143. } else {
  144. null
  145. }
  146. }
  147. private fun getContactDefinedProfilePicture(
  148. contactModel: ContactModel,
  149. highRes: Boolean,
  150. ): Bitmap? {
  151. try {
  152. val result = fileService?.getContactDefinedProfilePicture(contactModel.identity)
  153. if (result != null && !highRes) {
  154. return AvatarConverterUtil.convert(this.context.resources, result)
  155. }
  156. return result
  157. } catch (e: Exception) {
  158. return null
  159. }
  160. }
  161. private fun getUserDefinedProfilePicture(
  162. identity: String,
  163. highRes: Boolean,
  164. ): Bitmap? {
  165. return try {
  166. var result = fileService?.getUserDefinedProfilePicture(identity)
  167. if (result != null && !highRes) {
  168. result = AvatarConverterUtil.convert(this.context.resources, result)
  169. }
  170. result
  171. } catch (e: Exception) {
  172. null
  173. }
  174. }
  175. private fun getAndroidDefinedProfilePicture(
  176. contactModel: ContactModel,
  177. highRes: Boolean,
  178. ): Bitmap? {
  179. if (ContactUtil.isGatewayContact(contactModel.identity) || AndroidContactUtil.getInstance()
  180. .getAndroidContactUri(contactModel) == null
  181. ) {
  182. return null
  183. }
  184. // regular contacts
  185. return try {
  186. var result = fileService?.getAndroidDefinedProfilePicture(contactModel)
  187. if (result != null && !highRes) {
  188. result = AvatarConverterUtil.convert(this.context.resources, result)
  189. }
  190. result
  191. } catch (e: Exception) {
  192. null
  193. }
  194. }
  195. private fun buildDefaultAvatar(
  196. contactModel: ContactModel?,
  197. highRes: Boolean,
  198. backgroundColor: Int,
  199. ): Bitmap {
  200. val color = contactService?.getAvatarColor(contactModel) ?: ColorUtil.getInstance().getCurrentThemeGray(context)
  201. val drawable =
  202. if (contactModel != null && ContactUtil.isGatewayContact(contactModel.identity)) {
  203. contactBusinessAvatar
  204. } else {
  205. contactDefaultAvatar
  206. }
  207. return if (highRes) {
  208. buildDefaultAvatarHighRes(drawable, color, backgroundColor)
  209. } else {
  210. buildDefaultAvatarLowRes(drawable, color)
  211. }
  212. }
  213. }