blob: 825489c3a19952b55e82a0e67bf44ccf7a0e5a9f [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bubbles.data
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Person
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BlendMode
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.WorkerThread
import androidx.core.graphics.applyCanvas
import com.example.android.bubbles.BubbleActivity
import com.example.android.bubbles.MainActivity
import com.example.android.bubbles.R
/**
* Handles all operations related to [Notification].
*/
class NotificationHelper(private val context: Context) {
companion object {
/**
* The notification channel for messages. This is used for showing Bubbles.
*/
private const val CHANNEL_NEW_MESSAGES = "new_messages"
private const val REQUEST_CONTENT = 1
private const val REQUEST_BUBBLE = 2
}
private val notificationManager = context.getSystemService(NotificationManager::class.java)
fun setUpNotificationChannels() {
if (notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_NEW_MESSAGES,
context.getString(R.string.channel_new_messages),
// The importance must be IMPORTANCE_HIGH to show Bubbles.
NotificationManager.IMPORTANCE_HIGH
).apply {
description = context.getString(R.string.channel_new_messages_description)
}
)
}
}
@WorkerThread
fun showNotification(chat: Chat, fromUser: Boolean) {
val icon = Icon.createWithBitmap(roundIcon(context, chat.contact.icon))
val person = Person.Builder()
.setName(chat.contact.name)
.setIcon(icon)
.build()
val contentUri = Uri.parse("https://android.example.com/chat/${chat.contact.id}")
val builder = Notification.Builder(context, CHANNEL_NEW_MESSAGES)
// A notification can be shown as a bubble by calling setBubbleMetadata()
.setBubbleMetadata(
Notification.BubbleMetadata.Builder()
// The height of the expanded bubble.
.setDesiredHeight(context.resources.getDimensionPixelSize(R.dimen.bubble_height))
// The icon of the bubble.
// TODO: The icon is not displayed in Android Q Beta 2.
.setIcon(icon)
.apply {
// When the bubble is explicitly opened by the user, we can show the bubble automatically
// in the expanded state. This works only when the app is in the foreground.
// TODO: This does not yet work in Android Q Beta 2.
if (fromUser) {
setAutoExpandBubble(true)
setSuppressInitialNotification(true)
}
}
// The Intent to be used for the expanded bubble.
.setIntent(
PendingIntent.getActivity(
context,
REQUEST_BUBBLE,
// Launch BubbleActivity as the expanded bubble.
Intent(context, BubbleActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setData(Uri.parse("https://android.example.com/chat/${chat.contact.id}")),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
)
// The user can turn off the bubble in system settings. In that case, this notification is shown as a
// normal notification instead of a bubble. Make sure that this notification works as a normal notification
// as well.
.setContentTitle(chat.contact.name)
.setSmallIcon(R.drawable.ic_message)
.setCategory(Notification.CATEGORY_MESSAGE)
.addPerson(person)
.setShowWhen(true)
// The content Intent is used when the user clicks on the "Open Content" icon button on the expanded bubble,
// as well as when the fall-back notification is clicked.
.setContentIntent(
PendingIntent.getActivity(
context,
REQUEST_CONTENT,
Intent(context, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setData(contentUri),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
if (fromUser) {
// This is a Bubble explicitly opened by the user.
builder.setContentText(context.getString(R.string.chat_with_contact, chat.contact.name))
} else {
// Let's add some more content to the notification in case it falls back to a normal notification.
val lastOutgoingId = chat.messages.last { !it.isIncoming }.id
val newMessages = chat.messages.filter { message ->
message.id > lastOutgoingId
}
val lastMessage = newMessages.last()
builder
.setStyle(
if (lastMessage.photo != null) {
Notification.BigPictureStyle()
.bigPicture(BitmapFactory.decodeResource(context.resources, lastMessage.photo))
.bigLargeIcon(icon)
.setSummaryText(lastMessage.text)
} else {
Notification.MessagingStyle(person)
.apply {
for (message in newMessages) {
addMessage(message.text, message.timestamp, person)
}
}
.setGroupConversation(false)
}
)
.setContentText(newMessages.joinToString("\n") { it.text })
.setWhen(newMessages.last().timestamp)
}
notificationManager.notify(chat.contact.id.toInt(), builder.build())
}
fun dismissNotification(id: Long) {
notificationManager.cancel(id.toInt())
}
fun canBubble(): Boolean {
val channel = notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES)
return notificationManager.areBubblesAllowed() && channel.canBubble()
}
}
@WorkerThread
private fun roundIcon(context: Context, @DrawableRes id: Int): Bitmap {
val original = BitmapFactory.decodeResource(context.resources, id)
val width = original.width
val height = original.height
val rect = Rect(0, 0, width, height)
val icon = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val paint = Paint().apply {
isAntiAlias = true
color = Color.BLACK
}
icon.applyCanvas {
drawARGB(0, 0, 0, 0)
drawOval(0f, 0f, width.toFloat(), height.toFloat(), paint)
paint.blendMode = BlendMode.SRC_IN
drawBitmap(original, rect, rect, paint)
}
return icon
}