1 /*
<lambda>null2 * Copyright (C) 2019 The Android Open Source Project
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 package com.example.android.bubbles.data
17
18 import android.app.Notification
19 import android.app.NotificationChannel
20 import android.app.NotificationManager
21 import android.app.PendingIntent
22 import android.app.Person
23 import android.content.Context
24 import android.content.Intent
25 import android.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.graphics.BlendMode
28 import android.graphics.Color
29 import android.graphics.Paint
30 import android.graphics.Rect
31 import android.graphics.drawable.Icon
32 import android.net.Uri
33 import androidx.annotation.DrawableRes
34 import androidx.annotation.WorkerThread
35 import androidx.core.graphics.applyCanvas
36 import com.example.android.bubbles.BubbleActivity
37 import com.example.android.bubbles.MainActivity
38 import com.example.android.bubbles.R
39
40 /**
41 * Handles all operations related to [Notification].
42 */
43 class NotificationHelper(private val context: Context) {
44
45 companion object {
46 /**
47 * The notification channel for messages. This is used for showing Bubbles.
48 */
49 private const val CHANNEL_NEW_MESSAGES = "new_messages"
50
51 private const val REQUEST_CONTENT = 1
52 private const val REQUEST_BUBBLE = 2
53 }
54
55 private val notificationManager = context.getSystemService(NotificationManager::class.java)
56
57 fun setUpNotificationChannels() {
58 if (notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES) == null) {
59 notificationManager.createNotificationChannel(
60 NotificationChannel(
61 CHANNEL_NEW_MESSAGES,
62 context.getString(R.string.channel_new_messages),
63 // The importance must be IMPORTANCE_HIGH to show Bubbles.
64 NotificationManager.IMPORTANCE_HIGH
65 ).apply {
66 description = context.getString(R.string.channel_new_messages_description)
67 }
68 )
69 }
70 }
71
72 @WorkerThread
73 fun showNotification(chat: Chat, fromUser: Boolean) {
74 val icon = Icon.createWithBitmap(roundIcon(context, chat.contact.icon))
75 val person = Person.Builder()
76 .setName(chat.contact.name)
77 .setIcon(icon)
78 .build()
79 val contentUri = Uri.parse("https://android.example.com/chat/${chat.contact.id}")
80 val builder = Notification.Builder(context, CHANNEL_NEW_MESSAGES)
81 // A notification can be shown as a bubble by calling setBubbleMetadata()
82 .setBubbleMetadata(
83 Notification.BubbleMetadata.Builder()
84 // The height of the expanded bubble.
85 .setDesiredHeight(context.resources.getDimensionPixelSize(R.dimen.bubble_height))
86 // The icon of the bubble.
87 // TODO: The icon is not displayed in Android Q Beta 2.
88 .setIcon(icon)
89 .apply {
90 // When the bubble is explicitly opened by the user, we can show the bubble automatically
91 // in the expanded state. This works only when the app is in the foreground.
92 // TODO: This does not yet work in Android Q Beta 2.
93 if (fromUser) {
94 setAutoExpandBubble(true)
95 setSuppressInitialNotification(true)
96 }
97 }
98 // The Intent to be used for the expanded bubble.
99 .setIntent(
100 PendingIntent.getActivity(
101 context,
102 REQUEST_BUBBLE,
103 // Launch BubbleActivity as the expanded bubble.
104 Intent(context, BubbleActivity::class.java)
105 .setAction(Intent.ACTION_VIEW)
106 .setData(Uri.parse("https://android.example.com/chat/${chat.contact.id}")),
107 PendingIntent.FLAG_UPDATE_CURRENT
108 )
109 )
110 .build()
111 )
112 // The user can turn off the bubble in system settings. In that case, this notification is shown as a
113 // normal notification instead of a bubble. Make sure that this notification works as a normal notification
114 // as well.
115 .setContentTitle(chat.contact.name)
116 .setSmallIcon(R.drawable.ic_message)
117 .setCategory(Notification.CATEGORY_MESSAGE)
118 .addPerson(person)
119 .setShowWhen(true)
120 // The content Intent is used when the user clicks on the "Open Content" icon button on the expanded bubble,
121 // as well as when the fall-back notification is clicked.
122 .setContentIntent(
123 PendingIntent.getActivity(
124 context,
125 REQUEST_CONTENT,
126 Intent(context, MainActivity::class.java)
127 .setAction(Intent.ACTION_VIEW)
128 .setData(contentUri),
129 PendingIntent.FLAG_UPDATE_CURRENT
130 )
131 )
132
133 if (fromUser) {
134 // This is a Bubble explicitly opened by the user.
135 builder.setContentText(context.getString(R.string.chat_with_contact, chat.contact.name))
136 } else {
137 // Let's add some more content to the notification in case it falls back to a normal notification.
138 val lastOutgoingId = chat.messages.last { !it.isIncoming }.id
139 val newMessages = chat.messages.filter { message ->
140 message.id > lastOutgoingId
141 }
142 val lastMessage = newMessages.last()
143 builder
144 .setStyle(
145 if (lastMessage.photo != null) {
146 Notification.BigPictureStyle()
147 .bigPicture(BitmapFactory.decodeResource(context.resources, lastMessage.photo))
148 .bigLargeIcon(icon)
149 .setSummaryText(lastMessage.text)
150 } else {
151 Notification.MessagingStyle(person)
152 .apply {
153 for (message in newMessages) {
154 addMessage(message.text, message.timestamp, person)
155 }
156 }
157 .setGroupConversation(false)
158 }
159 )
160 .setContentText(newMessages.joinToString("\n") { it.text })
161 .setWhen(newMessages.last().timestamp)
162 }
163
164 notificationManager.notify(chat.contact.id.toInt(), builder.build())
165 }
166
167 fun dismissNotification(id: Long) {
168 notificationManager.cancel(id.toInt())
169 }
170
171 fun canBubble(): Boolean {
172 val channel = notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES)
173 return notificationManager.areBubblesAllowed() && channel.canBubble()
174 }
175 }
176
177 @WorkerThread
roundIconnull178 private fun roundIcon(context: Context, @DrawableRes id: Int): Bitmap {
179 val original = BitmapFactory.decodeResource(context.resources, id)
180 val width = original.width
181 val height = original.height
182 val rect = Rect(0, 0, width, height)
183 val icon = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
184 val paint = Paint().apply {
185 isAntiAlias = true
186 color = Color.BLACK
187 }
188 icon.applyCanvas {
189 drawARGB(0, 0, 0, 0)
190 drawOval(0f, 0f, width.toFloat(), height.toFloat(), paint)
191 paint.blendMode = BlendMode.SRC_IN
192 drawBitmap(original, rect, rect, paint)
193 }
194 return icon
195 }
196