Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compose/snippets/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,33 @@
package com.example.compose.snippets.notifications

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.Icon
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.annotation.RequiresPermission
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.EXTRA_NOTIFICATION_ID
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
Expand All @@ -43,6 +52,8 @@ import androidx.media3.session.MediaStyleNotificationHelper
import com.example.compose.snippets.R
import com.example.compose.snippets.touchinput.Button

val CHANNEL_ID = "channelId"

@Composable
fun NotificationSnippets(context: Context) {
// [START android_notification_authenticated_action]
Expand All @@ -61,6 +72,200 @@ fun NotificationSnippets(context: Context) {
// [END android_notification_authenticated_action]
}

fun createNotification(context: Context) {
// [START android_notification_create]
val textTitle = "Title"
val textContent = "Content"
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle(textTitle)
.setContentText(textContent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// [END android_notification_create]
}

fun createNotificationWithStyle(context: Context) {
// [START android_notification_set_style]
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle("My notification")
.setContentText("Much longer text that cannot fit one line...")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Much longer text that cannot fit one line..."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// [END android_notification_set_style]
}

// [START android_notification_create_channel]
fun createNotificationChannel(context: Context) {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is not in the Support Library.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = context.getString(R.string.channel_name)
val descriptionText = context.getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system.
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
Comment on lines +113 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and consistency with modern Android practices, use context.getSystemService(NotificationManager::class.java). This avoids manual casting and is safer when the SDK version is 23 or higher.

Suggested change
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager?.createNotificationChannel(channel)

}
}
// [END android_notification_create_channel]

fun createNotificationTapAction(context: Context) {
// [START android_notification_tap_action]
// Create an explicit intent for an Activity in your app.
val intent = Intent(context, AlertDetails::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Set the intent that fires when the user taps the notification.
.setContentIntent(pendingIntent)
.setAutoCancel(true)
// [END android_notification_tap_action]
}

fun showNotification(context: Context) {
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
val notificationId = 1 // This is demonstrative and should be unique
// [START android_notification_show_notification]
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
// TODO: Consider calling ActivityCompat#requestPermissions here
// to request the missing permissions, and then overriding
// public fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
// grantResults: IntArray)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.

return@with
}
// notificationId is a unique int for each notification that you must define.
notify(notificationId, builder.build())
// [END android_notification_show_notification]
}
}

fun addActionButton(context: Context) {
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
// [START android_notification_add_action]
val ACTION_SNOOZE = "snooze"
val snoozeIntent = Intent(context, MyBroadcastReceiver::class.java).apply {
action = ACTION_SNOOZE
putExtra(EXTRA_NOTIFICATION_ID, 0)
}
val snoozePendingIntent: PendingIntent =
PendingIntent.getBroadcast(context, 0, snoozeIntent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.addAction(R.drawable.snooze, context.getString(R.string.snooze),
snoozePendingIntent)
// [END android_notification_add_action]

// [START android_notification_add_reply]
// Key for the string that's delivered in the action's intent.
val KEY_TEXT_REPLY = "key_text_reply"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is recommended to use the constant ReplyReceiver.KEY_TEXT_REPLY instead of a hardcoded string. This ensures that the key used to send the reply matches the key used to retrieve it in the receiver.

Suggested change
val KEY_TEXT_REPLY = "key_text_reply"
val KEY_TEXT_REPLY = ReplyReceiver.KEY_TEXT_REPLY

val replyLabel: String = context.resources.getString(R.string.reply_label)
val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
setLabel(replyLabel)
build()
}
// [END android_notification_add_reply]

val conversationId = 1 // This is demonstrative and should be unique.
// [START android_notification_add_reply_pending_intent]
// Build a PendingIntent for the reply action to trigger.
val replyPendingIntent: PendingIntent =
PendingIntent.getBroadcast(context,
conversationId,
getMessageReplyIntent(conversationId),
PendingIntent.FLAG_MUTABLE)
// [END android_notification_add_reply_pending_intent]

// [START android_notification_add_reply_action]
// Create the reply action and add the remote input.
val action: NotificationCompat.Action =
NotificationCompat.Action.Builder(R.drawable.reply,
context.getString(R.string.reply_label), replyPendingIntent)
.addRemoteInput(remoteInput)
.build()
// [END android_notification_add_reply_action]
}

// [START android_notification_retrieve_user_input]
private fun getMessageText(intent: Intent): CharSequence? {
return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(ReplyReceiver.KEY_TEXT_REPLY)
}
// [START android_notification_retrieve_user_input]

fun getMessageReplyIntent(conversationId: Any): Intent {
// This is for demonstrative purposes.
TODO("Not yet implemented")
}

@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
fun handledReply(context: Context) {
val notificationId = 1 // This is demonstrative and should be unique.
// [START android_notification_handled_reply]
// Build a new notification, which informs the user that the system
// handled their interaction with the previous notification.
val repliedNotification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.message)
.setContentText(context.getString(R.string.replied))
.build()

// Issue the new notification.
NotificationManagerCompat.from(context).notify(notificationId, repliedNotification)
// [END android_notification_handled_reply]
}

fun retrieveOtherData(context: Context) {
// [START android_notification_remote_input_retrieve_data]
val replyLabel: String = context.resources.getString(R.string.reply_label)
val remoteInput: RemoteInput = RemoteInput.Builder(ReplyReceiver.KEY_REPLY).run {
setLabel(replyLabel)
// Allow for image data types in the input.
// This method can be used again to allow for other data types.
setAllowDataType("image/*", true)
build()
}
// [END android_notification_remote_input_retrieve_data]
}

fun fullScreenIntent(context: Context) {
// [START android_notification_full_screen_intent]
val fullScreenIntent = Intent(context, ImportantActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0,
fullScreenIntent, PendingIntent.FLAG_IMMUTABLE)

val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setFullScreenIntent(fullScreenPendingIntent, true)
// [END android_notification_full_screen_intent]
}


@Composable
fun NotificationSnippetRequestPostPermission() {
// [START android_notification_request_post_permission]
Expand Down Expand Up @@ -228,3 +433,18 @@ val messages = listOf(
Person.Builder().setName("Frank").build()
)
)

// For demonstrative purposes only when used for a full screen intent.
class ImportantActivity : ComponentActivity() {
}

// For demonstrative purposes only for launching from a tap action.
class AlertDetails : ComponentActivity() {
}

// For demonstrative purposes only for handling a snooze action.
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2026 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
*
* https://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.compose.snippets.notifications

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import androidx.core.app.RemoteInput

// [START android_notification_reply_receiver]
class ReplyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val dataResults = RemoteInput.getDataResultsFromIntent(intent, KEY_REPLY)
val imageUri: Uri? = dataResults?.get("image/*") as? Uri

if (imageUri != null) {
// Extract the image
try {
val inputStream = context.contentResolver.openInputStream(imageUri)
val bitmap = BitmapFactory.decodeStream(inputStream)
// Display the image
// ...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The InputStream obtained from openInputStream is not closed, which can lead to resource leaks. Use the .use extension function to ensure the stream is closed automatically even if an exception occurs during processing.

                context.contentResolver.openInputStream(imageUri)?.use { inputStream ->
                    val bitmap = BitmapFactory.decodeStream(inputStream)
                    // Display the image
                    // ...
                }

} catch (e: Exception) {
Log.e("ReplyReceiver", "Failed to process image URI", e)
}
}
}

companion object {
const val KEY_REPLY = "key_reply"
const val KEY_TEXT_REPLY = "key_text_reply"
}
}
// [END android_notification_reply_receiver]
26 changes: 26 additions & 0 deletions compose/snippets/src/main/res/drawable/message.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 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

https://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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M184,415.93Q171,402.86 171,383.93Q171,365 184.07,352Q197.14,339 216.07,339Q235,339 248,352.07Q261,365.14 261,384.07Q261,403 247.93,416Q234.86,429 215.93,429Q197,429 184,415.93ZM712,415.93Q699,402.86 699,383.93Q699,365 712.07,352Q725.14,339 744.07,339Q763,339 776,352.07Q789,365.14 789,384.07Q789,403 775.93,416Q762.86,429 743.93,429Q725,429 712,415.93ZM78,516L78,516L78,516L78,516L78,516L78,516L78,516Q78,516 78,516Q78,516 78,516ZM912,516L912,516L912,516L912,516Q912,516 912,516Q912,516 912,516L912,516L912,516L912,516L912,516ZM168,864L96,864L96,696L264,696Q204,696 162.5,654Q121,612 121,552L193,552Q193,581.7 213.5,602.85Q234,624 263.96,624L263.96,504L362,504L330,369Q309,281 239,224.5Q169,168 78,168L48,168L48,96L78,96Q192,96 281.5,165Q371,234 398,345L444,530Q449,547.48 438.22,561.74Q427.44,576 410,576L336,576L336,696Q336,725.7 314.85,746.85Q293.7,768 264,768L168,768L168,864ZM864,864L792,864L792,768L696,768Q666.3,768 645.15,746.85Q624,725.7 624,696L624,576L552,576Q534,576 523,561Q512,546 517,529L564,335Q592,232 678,164Q764,96 876,96L912,96L912,168L876,168Q791,168 722,221Q653,274 630,369L598,504L696.04,504L696.04,624Q726,624 747.09,602.85Q768.19,581.7 768.19,552L840,552Q840,612 798,654Q756,696 696,696L864,696L864,864ZM336,696L336,696L336,624L336,624L336,696ZM624,696L624,624L624,624L624,696Z"/>
</vector>
27 changes: 27 additions & 0 deletions compose/snippets/src/main/res/drawable/reply.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 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

https://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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M744,750L744,606Q744,556 709,521Q674,486 624,486L282,486L405,609L354,660L144,450L354,240L405,291L282,414L624,414Q704,414 760,470Q816,526 816,606L816,750L744,750Z"/>
</vector>
Loading
Loading