NotificationUtils.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Andy Scherzinger
  6. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  7. * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. package com.nextcloud.talk.utils
  23. import android.annotation.TargetApi
  24. import android.app.Notification
  25. import android.app.NotificationChannel
  26. import android.app.NotificationManager
  27. import android.content.Context
  28. import android.graphics.drawable.BitmapDrawable
  29. import android.media.AudioAttributes
  30. import android.net.Uri
  31. import android.os.Build
  32. import android.service.notification.StatusBarNotification
  33. import android.text.TextUtils
  34. import android.util.Log
  35. import androidx.core.app.NotificationManagerCompat
  36. import androidx.core.graphics.drawable.IconCompat
  37. import coil.executeBlocking
  38. import coil.imageLoader
  39. import coil.request.ImageRequest
  40. import coil.transform.CircleCropTransformation
  41. import com.bluelinelabs.logansquare.LoganSquare
  42. import com.nextcloud.talk.BuildConfig
  43. import com.nextcloud.talk.R
  44. import com.nextcloud.talk.data.user.model.User
  45. import com.nextcloud.talk.models.RingtoneSettings
  46. import com.nextcloud.talk.utils.bundle.BundleKeys
  47. import com.nextcloud.talk.utils.preferences.AppPreferences
  48. import java.io.IOException
  49. @Suppress("TooManyFunctions")
  50. object NotificationUtils {
  51. const val TAG = "NotificationUtils"
  52. enum class NotificationChannels {
  53. NOTIFICATION_CHANNEL_MESSAGES_V4,
  54. NOTIFICATION_CHANNEL_CALLS_V4,
  55. NOTIFICATION_CHANNEL_UPLOADS
  56. }
  57. const val DEFAULT_CALL_RINGTONE_URI =
  58. "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_call"
  59. const val DEFAULT_MESSAGE_RINGTONE_URI =
  60. "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_message"
  61. // RemoteInput key - used for replies sent directly from notification
  62. const val KEY_DIRECT_REPLY = "key_direct_reply"
  63. // notification group keys
  64. const val KEY_UPLOAD_GROUP = "com.nextcloud.talk.utils.KEY_UPLOAD_GROUP"
  65. const val GROUP_SUMMARY_NOTIFICATION_ID = -1
  66. @TargetApi(Build.VERSION_CODES.O)
  67. private fun createNotificationChannel(
  68. context: Context,
  69. notificationChannel: Channel,
  70. sound: Uri?,
  71. audioAttributes: AudioAttributes?
  72. ) {
  73. val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  74. if (
  75. Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
  76. notificationManager.getNotificationChannel(notificationChannel.id) == null
  77. ) {
  78. val importance = if (notificationChannel.isImportant) {
  79. NotificationManager.IMPORTANCE_HIGH
  80. } else {
  81. NotificationManager.IMPORTANCE_LOW
  82. }
  83. val channel = NotificationChannel(
  84. notificationChannel.id,
  85. notificationChannel.name,
  86. importance
  87. )
  88. channel.description = notificationChannel.description
  89. channel.enableLights(true)
  90. channel.lightColor = R.color.colorPrimary
  91. channel.setSound(sound, audioAttributes)
  92. channel.setBypassDnd(false)
  93. notificationManager.createNotificationChannel(channel)
  94. }
  95. }
  96. private fun createCallsNotificationChannel(context: Context, appPreferences: AppPreferences) {
  97. val audioAttributes =
  98. AudioAttributes.Builder()
  99. .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
  100. .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
  101. .build()
  102. val soundUri = getCallRingtoneUri(context, appPreferences)
  103. createNotificationChannel(
  104. context,
  105. Channel(
  106. NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name,
  107. context.resources.getString(R.string.nc_notification_channel_calls),
  108. context.resources.getString(R.string.nc_notification_channel_calls_description),
  109. true
  110. ),
  111. soundUri,
  112. audioAttributes
  113. )
  114. }
  115. private fun createMessagesNotificationChannel(context: Context, appPreferences: AppPreferences) {
  116. val audioAttributes =
  117. AudioAttributes.Builder()
  118. .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
  119. .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
  120. .build()
  121. val soundUri = getMessageRingtoneUri(context, appPreferences)
  122. createNotificationChannel(
  123. context,
  124. Channel(
  125. NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name,
  126. context.resources.getString(R.string.nc_notification_channel_messages),
  127. context.resources.getString(R.string.nc_notification_channel_messages_description),
  128. true
  129. ),
  130. soundUri,
  131. audioAttributes
  132. )
  133. }
  134. private fun createUploadsNotificationChannel(context: Context) {
  135. createNotificationChannel(
  136. context,
  137. Channel(
  138. NotificationChannels.NOTIFICATION_CHANNEL_UPLOADS.name,
  139. context.resources.getString(R.string.nc_notification_channel_uploads),
  140. context.resources.getString(R.string.nc_notification_channel_uploads_description),
  141. false
  142. ),
  143. null,
  144. null
  145. )
  146. }
  147. fun registerNotificationChannels(context: Context, appPreferences: AppPreferences) {
  148. createCallsNotificationChannel(context, appPreferences)
  149. createMessagesNotificationChannel(context, appPreferences)
  150. createUploadsNotificationChannel(context)
  151. }
  152. @TargetApi(Build.VERSION_CODES.O)
  153. fun removeOldNotificationChannels(context: Context) {
  154. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  155. val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  156. // Current version does not use notification channel groups - delete all groups
  157. for (channelGroup in notificationManager.notificationChannelGroups) {
  158. notificationManager.deleteNotificationChannelGroup(channelGroup.id)
  159. }
  160. val channelsToKeep = NotificationChannels.values().map { it.name }
  161. // Delete all notification channels created by previous versions
  162. for (channel in notificationManager.notificationChannels) {
  163. if (!channelsToKeep.contains(channel.id)) {
  164. notificationManager.deleteNotificationChannel(channel.id)
  165. }
  166. }
  167. }
  168. }
  169. @TargetApi(Build.VERSION_CODES.O)
  170. private fun getNotificationChannel(context: Context, channelId: String): NotificationChannel? {
  171. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  172. val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  173. return notificationManager.getNotificationChannel(channelId)
  174. }
  175. return null
  176. }
  177. private inline fun scanNotifications(
  178. context: Context?,
  179. conversationUser: User,
  180. callback: (
  181. notificationManager: NotificationManager,
  182. statusBarNotification: StatusBarNotification,
  183. notification: Notification
  184. ) -> Unit
  185. ) {
  186. if (conversationUser.id == -1L || context == null) {
  187. return
  188. }
  189. val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  190. val statusBarNotifications = notificationManager.activeNotifications
  191. var notification: Notification?
  192. for (statusBarNotification in statusBarNotifications) {
  193. notification = statusBarNotification.notification
  194. if (
  195. notification != null &&
  196. !notification.extras.isEmpty &&
  197. conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
  198. ) {
  199. callback(notificationManager, statusBarNotification, notification)
  200. }
  201. }
  202. }
  203. fun cancelAllNotificationsForAccount(context: Context?, conversationUser: User) {
  204. scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, _ ->
  205. notificationManager.cancel(statusBarNotification.id)
  206. }
  207. }
  208. fun cancelNotification(context: Context?, conversationUser: User, notificationId: Long?) {
  209. scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
  210. if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
  211. notificationManager.cancel(statusBarNotification.id)
  212. }
  213. }
  214. }
  215. fun findNotificationForRoom(
  216. context: Context?,
  217. conversationUser: User,
  218. roomTokenOrId: String
  219. ): StatusBarNotification? {
  220. scanNotifications(context, conversationUser) { _, statusBarNotification, notification ->
  221. if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) {
  222. return statusBarNotification
  223. }
  224. }
  225. return null
  226. }
  227. fun cancelExistingNotificationsForRoom(context: Context?, conversationUser: User, roomTokenOrId: String) {
  228. scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
  229. if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN) &&
  230. !notification.extras.getBoolean(BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION)
  231. ) {
  232. notificationManager.cancel(statusBarNotification.id)
  233. }
  234. }
  235. }
  236. fun isNotificationVisible(context: Context?, notificationId: Int): Boolean {
  237. var isVisible = false
  238. val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  239. val notifications = notificationManager.activeNotifications
  240. for (notification in notifications) {
  241. if (notification.id == notificationId) {
  242. isVisible = true
  243. break
  244. }
  245. }
  246. return isVisible
  247. }
  248. fun isCallsNotificationChannelEnabled(context: Context): Boolean {
  249. val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name)
  250. if (channel != null) {
  251. return isNotificationChannelEnabled(context, channel)
  252. }
  253. return false
  254. }
  255. fun isMessagesNotificationChannelEnabled(context: Context): Boolean {
  256. val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name)
  257. if (channel != null) {
  258. return isNotificationChannelEnabled(context, channel)
  259. }
  260. return false
  261. }
  262. private fun isNotificationChannelEnabled(context: Context, channel: NotificationChannel): Boolean {
  263. return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  264. channel.importance != NotificationManager.IMPORTANCE_NONE
  265. } else {
  266. NotificationManagerCompat.from(context).areNotificationsEnabled()
  267. }
  268. }
  269. private fun getRingtoneUri(
  270. context: Context,
  271. ringtonePreferencesString: String?,
  272. defaultRingtoneUri: String,
  273. channelId: String
  274. ): Uri? {
  275. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  276. val channel = getNotificationChannel(context, channelId)
  277. if (channel != null) {
  278. return channel.sound
  279. }
  280. // Notification channel will not be available when starting the application for the first time.
  281. // Ringtone uris are required to register the notification channels -> get uri from preferences.
  282. }
  283. return if (TextUtils.isEmpty(ringtonePreferencesString)) {
  284. Uri.parse(defaultRingtoneUri)
  285. } else {
  286. try {
  287. val ringtoneSettings =
  288. LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
  289. ringtoneSettings.ringtoneUri
  290. } catch (exception: IOException) {
  291. Uri.parse(defaultRingtoneUri)
  292. }
  293. }
  294. }
  295. fun getCallRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? {
  296. return getRingtoneUri(
  297. context,
  298. appPreferences.callRingtoneUri,
  299. DEFAULT_CALL_RINGTONE_URI,
  300. NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
  301. )
  302. }
  303. fun getMessageRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? {
  304. return getRingtoneUri(
  305. context,
  306. appPreferences.messageRingtoneUri,
  307. DEFAULT_MESSAGE_RINGTONE_URI,
  308. NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
  309. )
  310. }
  311. fun loadAvatarSync(avatarUrl: String, context: Context): IconCompat? {
  312. var avatarIcon: IconCompat? = null
  313. val request = ImageRequest.Builder(context)
  314. .data(avatarUrl)
  315. .transformations(CircleCropTransformation())
  316. .placeholder(R.drawable.account_circle_96dp)
  317. .target(
  318. onSuccess = { result ->
  319. val bitmap = (result as BitmapDrawable).bitmap
  320. avatarIcon = IconCompat.createWithBitmap(bitmap)
  321. },
  322. onError = { error ->
  323. error?.let {
  324. val bitmap = (error as BitmapDrawable).bitmap
  325. avatarIcon = IconCompat.createWithBitmap(bitmap)
  326. }
  327. Log.w(TAG, "Can't load avatar for URL: $avatarUrl")
  328. }
  329. )
  330. .build()
  331. context.imageLoader.executeBlocking(request)
  332. return avatarIcon
  333. }
  334. private data class Channel(
  335. val id: String,
  336. val name: String,
  337. val description: String,
  338. val isImportant: Boolean
  339. )
  340. }