• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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