ImageViewExtensions.kt 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
  5. * SPDX-FileCopyrightText: 2022 Tim Krüger <t@timkrueger.me>
  6. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
  7. * SPDX-License-Identifier: GPL-3.0-or-later
  8. */
  9. @file:Suppress("TooManyFunctions")
  10. package com.nextcloud.talk.extensions
  11. import android.content.Context
  12. import android.graphics.Bitmap
  13. import android.graphics.Canvas
  14. import android.graphics.Color
  15. import android.graphics.Paint
  16. import android.graphics.drawable.BitmapDrawable
  17. import android.graphics.drawable.ColorDrawable
  18. import android.graphics.drawable.Drawable
  19. import android.graphics.drawable.LayerDrawable
  20. import android.util.Log
  21. import android.widget.ImageView
  22. import androidx.core.content.ContextCompat
  23. import androidx.core.content.res.ResourcesCompat
  24. import coil.annotation.ExperimentalCoilApi
  25. import coil.imageLoader
  26. import coil.load
  27. import coil.request.CachePolicy
  28. import coil.request.ImageRequest
  29. import coil.request.SuccessResult
  30. import coil.result
  31. import coil.transform.CircleCropTransformation
  32. import coil.transform.RoundedCornersTransformation
  33. import com.nextcloud.talk.R
  34. import com.nextcloud.talk.chat.data.model.ChatMessage
  35. import com.nextcloud.talk.data.user.model.User
  36. import com.nextcloud.talk.models.domain.ConversationModel
  37. import com.nextcloud.talk.models.json.conversations.Conversation
  38. import com.nextcloud.talk.models.json.conversations.ConversationEnums
  39. import com.nextcloud.talk.ui.theme.ViewThemeUtils
  40. import com.nextcloud.talk.utils.ApiUtils
  41. import com.nextcloud.talk.utils.DisplayUtils
  42. import com.nextcloud.talk.utils.TextDrawable
  43. import java.util.Locale
  44. private const val ROUNDING_PIXEL = 16f
  45. private const val TAG = "ImageViewExtensions"
  46. @Deprecated("use other constructor that expects com.nextcloud.talk.models.domain.ConversationModel")
  47. fun ImageView.loadConversationAvatar(
  48. user: User,
  49. conversation: Conversation,
  50. ignoreCache: Boolean,
  51. viewThemeUtils: ViewThemeUtils?
  52. ): io.reactivex.disposables.Disposable {
  53. return loadConversationAvatar(
  54. user,
  55. ConversationModel.mapToConversationModel(conversation, user),
  56. ignoreCache,
  57. viewThemeUtils
  58. )
  59. }
  60. @Suppress("ReturnCount")
  61. fun ImageView.loadConversationAvatar(
  62. user: User,
  63. conversation: ConversationModel,
  64. ignoreCache: Boolean,
  65. viewThemeUtils: ViewThemeUtils?
  66. ): io.reactivex.disposables.Disposable {
  67. val imageRequestUri = ApiUtils.getUrlForConversationAvatarWithVersion(
  68. 1,
  69. user.baseUrl,
  70. conversation.token,
  71. DisplayUtils.isDarkModeOn(this.context),
  72. conversation.avatarVersion
  73. )
  74. if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) {
  75. when (conversation.type) {
  76. ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
  77. return loadDefaultGroupCallAvatar(viewThemeUtils)
  78. ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
  79. return loadDefaultPublicCallAvatar(viewThemeUtils)
  80. else -> {}
  81. }
  82. }
  83. // these placeholders are only used when the request fails completely. The server also return default avatars
  84. // when no own images are set. (although these default avatars can not be themed for the android app..)
  85. val errorPlaceholder =
  86. when (conversation.type) {
  87. ConversationEnums.ConversationType.ROOM_GROUP_CALL ->
  88. ContextCompat.getDrawable(context, R.drawable.ic_circular_group)
  89. ConversationEnums.ConversationType.ROOM_PUBLIC_CALL ->
  90. ContextCompat.getDrawable(context, R.drawable.ic_circular_link)
  91. else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp)
  92. }
  93. return loadAvatarInternal(user, imageRequestUri, ignoreCache, errorPlaceholder)
  94. }
  95. fun ImageView.loadUserAvatar(
  96. user: User,
  97. avatarId: String,
  98. requestBigSize: Boolean = true,
  99. ignoreCache: Boolean
  100. ): io.reactivex.disposables.Disposable {
  101. val imageRequestUri = ApiUtils.getUrlForAvatar(
  102. user.baseUrl!!,
  103. avatarId,
  104. requestBigSize
  105. )
  106. return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
  107. }
  108. fun ImageView.loadFederatedUserAvatar(message: ChatMessage): io.reactivex.disposables.Disposable {
  109. val cloudId = message.actorId!!
  110. val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0
  111. val ignoreCache = false
  112. val requestBigSize = true
  113. return loadFederatedUserAvatar(
  114. message.activeUser!!,
  115. message.activeUser!!.baseUrl!!,
  116. message.token!!,
  117. cloudId,
  118. darkTheme,
  119. requestBigSize,
  120. ignoreCache
  121. )
  122. }
  123. @Suppress("LongParameterList")
  124. fun ImageView.loadFederatedUserAvatar(
  125. user: User,
  126. baseUrl: String,
  127. token: String,
  128. cloudId: String,
  129. darkTheme: Int,
  130. requestBigSize: Boolean = true,
  131. ignoreCache: Boolean
  132. ): io.reactivex.disposables.Disposable {
  133. val imageRequestUri = ApiUtils.getUrlForFederatedAvatar(
  134. baseUrl,
  135. token,
  136. cloudId,
  137. darkTheme,
  138. requestBigSize
  139. )
  140. Log.d(TAG, "federated avatar URL: $imageRequestUri")
  141. return loadAvatarInternal(user, imageRequestUri, ignoreCache, null)
  142. }
  143. @OptIn(ExperimentalCoilApi::class)
  144. private fun ImageView.loadAvatarInternal(
  145. user: User?,
  146. url: String,
  147. ignoreCache: Boolean,
  148. errorPlaceholder: Drawable?
  149. ): io.reactivex.disposables.Disposable {
  150. val cachePolicy = if (ignoreCache) {
  151. CachePolicy.WRITE_ONLY
  152. } else {
  153. CachePolicy.ENABLED
  154. }
  155. if (ignoreCache && this.result is SuccessResult) {
  156. val result = this.result as SuccessResult
  157. val memoryCacheKey = result.memoryCacheKey
  158. val memoryCache = context.imageLoader.memoryCache
  159. memoryCacheKey?.let { memoryCache?.remove(it) }
  160. val diskCacheKey = result.diskCacheKey
  161. val diskCache = context.imageLoader.diskCache
  162. diskCacheKey?.let { diskCache?.remove(it) }
  163. }
  164. return DisposableWrapper(
  165. load(url) {
  166. user?.let {
  167. addHeader(
  168. "Authorization",
  169. ApiUtils.getCredentials(user.username, user.token)!!
  170. )
  171. }
  172. transformations(CircleCropTransformation())
  173. error(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp))
  174. fallback(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp))
  175. listener(onError = { _, result ->
  176. Log.w(TAG, "Can't load avatar with URL: $url", result.throwable)
  177. })
  178. memoryCachePolicy(cachePolicy)
  179. diskCachePolicy(cachePolicy)
  180. }
  181. )
  182. }
  183. @Deprecated("Use function loadAvatar", level = DeprecationLevel.WARNING)
  184. fun ImageView.loadAvatarWithUrl(user: User? = null, url: String): io.reactivex.disposables.Disposable {
  185. return loadAvatarInternal(user, url, false, null)
  186. }
  187. fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.Disposable {
  188. val requestBuilder = ImageRequest.Builder(context)
  189. .data(url)
  190. .crossfade(true)
  191. .target(this)
  192. .transformations(CircleCropTransformation())
  193. val layers = arrayOfNulls<Drawable>(2)
  194. layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
  195. layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
  196. requestBuilder.placeholder(LayerDrawable(layers))
  197. if (url.startsWith(user.baseUrl!!) &&
  198. (url.contains("index.php/core/preview") || url.contains("/avatar/"))
  199. ) {
  200. requestBuilder.addHeader(
  201. "Authorization",
  202. ApiUtils.getCredentials(user.username, user.token)!!
  203. )
  204. }
  205. return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
  206. }
  207. fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null): io.reactivex.disposables.Disposable {
  208. var finalPlaceholder = placeholder
  209. if (finalPlaceholder == null) {
  210. finalPlaceholder = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_file)
  211. }
  212. val requestBuilder = ImageRequest.Builder(context)
  213. .data(url)
  214. .crossfade(true)
  215. .target(this)
  216. .placeholder(finalPlaceholder)
  217. .error(finalPlaceholder)
  218. .transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL))
  219. if (url.startsWith(user.baseUrl!!) &&
  220. (url.contains("index.php/core/preview") || url.contains("/avatar/"))
  221. ) {
  222. requestBuilder.addHeader(
  223. "Authorization",
  224. ApiUtils.getCredentials(user.username, user.token)!!
  225. )
  226. }
  227. return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
  228. }
  229. fun ImageView.loadAvatarOrImagePreview(
  230. url: String,
  231. user: User,
  232. placeholder: Drawable? = null
  233. ): io.reactivex.disposables.Disposable {
  234. return if (url.contains("/avatar/")) {
  235. loadAvatarInternal(user, url, false, null)
  236. } else {
  237. loadImage(url, user, placeholder)
  238. }
  239. }
  240. fun ImageView.loadUserAvatar(any: Any?): io.reactivex.disposables.Disposable {
  241. return DisposableWrapper(
  242. load(any) {
  243. transformations(CircleCropTransformation())
  244. }
  245. )
  246. }
  247. fun ImageView.loadSystemAvatar(): io.reactivex.disposables.Disposable {
  248. val layers = arrayOfNulls<Drawable>(2)
  249. layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
  250. layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
  251. val layerDrawable = LayerDrawable(layers)
  252. val data: Any = layerDrawable
  253. return DisposableWrapper(
  254. load(data) {
  255. transformations(CircleCropTransformation())
  256. }
  257. )
  258. }
  259. fun ImageView.loadNoteToSelfAvatar(): io.reactivex.disposables.Disposable {
  260. val layers = arrayOfNulls<Drawable>(2)
  261. layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
  262. layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_note_to_self)
  263. val layerDrawable = LayerDrawable(layers)
  264. val data: Any = layerDrawable
  265. return DisposableWrapper(
  266. load(data) {
  267. transformations(CircleCropTransformation())
  268. }
  269. )
  270. }
  271. fun ImageView.loadFirstLetterAvatar(letter: String): io.reactivex.disposables.Disposable {
  272. val layers = arrayOfNulls<Drawable>(2)
  273. layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
  274. layers[1] = createTextDrawable(context, letter.uppercase(Locale.ROOT))
  275. val layerDrawable = LayerDrawable(layers)
  276. val data: Any = layerDrawable
  277. return DisposableWrapper(
  278. load(data) {
  279. transformations(CircleCropTransformation())
  280. }
  281. )
  282. }
  283. fun ImageView.loadChangelogBotAvatar(): io.reactivex.disposables.Disposable {
  284. return loadSystemAvatar()
  285. }
  286. fun ImageView.loadBotsAvatar(): io.reactivex.disposables.Disposable {
  287. val layers = arrayOfNulls<Drawable>(2)
  288. layers[0] = ColorDrawable(context.getColor(R.color.black))
  289. layers[1] = TextDrawable(context, ">")
  290. val layerDrawable = LayerDrawable(layers)
  291. val data: Any = layerDrawable
  292. return DisposableWrapper(
  293. load(data) {
  294. transformations(CircleCropTransformation())
  295. }
  296. )
  297. }
  298. fun ImageView.loadDefaultGroupCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
  299. val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_group) as Any
  300. return loadUserAvatar(data)
  301. }
  302. fun ImageView.loadDefaultAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
  303. val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.account_circle_96dp) as Any
  304. return loadUserAvatar(data)
  305. }
  306. fun ImageView.loadDefaultPublicCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
  307. val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_link) as Any
  308. return loadUserAvatar(data)
  309. }
  310. fun ImageView.loadMailAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
  311. val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_mail) as Any
  312. return loadUserAvatar(data)
  313. }
  314. fun ImageView.loadGuestAvatar(user: User, name: String, big: Boolean): io.reactivex.disposables.Disposable {
  315. return loadGuestAvatar(user.baseUrl!!, name, big)
  316. }
  317. fun ImageView.loadGuestAvatar(baseUrl: String, name: String, big: Boolean): io.reactivex.disposables.Disposable {
  318. val imageRequestUri = ApiUtils.getUrlForGuestAvatar(
  319. baseUrl,
  320. name,
  321. big
  322. )
  323. return DisposableWrapper(
  324. load(imageRequestUri) {
  325. transformations(CircleCropTransformation())
  326. listener(onError = { _, result ->
  327. Log.w(TAG, "Can't load guest avatar with URL: $imageRequestUri", result.throwable)
  328. })
  329. }
  330. )
  331. }
  332. @Suppress("MagicNumber")
  333. private fun createTextDrawable(context: Context, letter: String): Drawable {
  334. val size = 100
  335. val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
  336. val canvas = Canvas(bitmap)
  337. val paint = Paint().apply {
  338. color = ResourcesCompat.getColor(context.resources, R.color.grey_600, null)
  339. style = Paint.Style.FILL
  340. }
  341. canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint)
  342. val textPaint = Paint().apply {
  343. color = Color.WHITE
  344. textSize = size / 2f
  345. isAntiAlias = true
  346. textAlign = Paint.Align.CENTER
  347. }
  348. val xPos = size / 2f
  349. val yPos = (canvas.height / 2 - (textPaint.descent() + textPaint.ascent()) / 2)
  350. canvas.drawText(letter.take(1), xPos, yPos, textPaint)
  351. return BitmapDrawable(context.resources, bitmap)
  352. }
  353. private class DisposableWrapper(private val disposable: coil.request.Disposable) : io.reactivex.disposables
  354. .Disposable {
  355. override fun dispose() {
  356. disposable.dispose()
  357. }
  358. override fun isDisposed(): Boolean {
  359. return disposable.isDisposed
  360. }
  361. }