From e391d16f9477991658f7dbaf26e7ebc5b5f73c84 Mon Sep 17 00:00:00 2001 From: Aaron Labiaga Date: Fri, 22 May 2026 13:02:56 -0600 Subject: [PATCH 1/2] more notification code snippets migration Change-Id: Ib7b51173d55bf49626fe9b19460e4d04d6994470 --- compose/snippets/src/main/AndroidManifest.xml | 1 + .../notifications/NotificationsSnippets.kt | 220 ++++++++++++++++++ .../snippets/notifications/ReplyReceiver.kt | 51 ++++ .../src/main/res/drawable/message.xml | 26 +++ .../snippets/src/main/res/drawable/reply.xml | 27 +++ .../snippets/src/main/res/drawable/snooze.xml | 26 +++ .../snippets/src/main/res/values/strings.xml | 5 + 7 files changed, 356 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt create mode 100644 compose/snippets/src/main/res/drawable/message.xml create mode 100644 compose/snippets/src/main/res/drawable/reply.xml create mode 100644 compose/snippets/src/main/res/drawable/snooze.xml diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index 5c0f57b2e..c036a5c82 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt index c33e13563..db2751de5 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt @@ -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 @@ -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] @@ -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) + } +} +// [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, + // 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" + 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] @@ -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") + } +} \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt new file mode 100644 index 000000000..a24d94d27 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt @@ -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 + // ... + } 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] diff --git a/compose/snippets/src/main/res/drawable/message.xml b/compose/snippets/src/main/res/drawable/message.xml new file mode 100644 index 000000000..f5b9fcda9 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/message.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/reply.xml b/compose/snippets/src/main/res/drawable/reply.xml new file mode 100644 index 000000000..9a60ae408 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/reply.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/snooze.xml b/compose/snippets/src/main/res/drawable/snooze.xml new file mode 100644 index 000000000..f75d5f108 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/snooze.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/compose/snippets/src/main/res/values/strings.xml b/compose/snippets/src/main/res/values/strings.xml index d4b9a63a6..c0a14070e 100644 --- a/compose/snippets/src/main/res/values/strings.xml +++ b/compose/snippets/src/main/res/values/strings.xml @@ -54,5 +54,10 @@ Profile This is just a placeholder. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + channelName + Channel description + Snooze + Reply + Replied From ea13067ae56dd3e54eda7ccedb352b50886ed92c Mon Sep 17 00:00:00 2001 From: Aaron Labiaga Date: Fri, 22 May 2026 14:15:02 -0600 Subject: [PATCH 2/2] fixes from gemini assistant code review Change-Id: Ia2c1c24f4c11b8273210a641485c90fb17985c54 --- .../snippets/notifications/NotificationsSnippets.kt | 9 +++++---- .../compose/snippets/notifications/ReplyReceiver.kt | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt index db2751de5..7f5639d45 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/NotificationsSnippets.kt @@ -50,6 +50,8 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.session.MediaStyleNotificationHelper import com.example.compose.snippets.R +import com.example.compose.snippets.notifications.ReplyReceiver.Companion.KEY_REPLY +import com.example.compose.snippets.notifications.ReplyReceiver.Companion.KEY_TEXT_REPLY import com.example.compose.snippets.touchinput.Button val CHANNEL_ID = "channelId" @@ -109,7 +111,7 @@ fun createNotificationChannel(context: Context) { } // Register the channel with the system. val notificationManager: NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + context.getSystemService(NotificationManager::class.java) as NotificationManager notificationManager.createNotificationChannel(channel) } } @@ -182,7 +184,6 @@ fun addActionButton(context: Context) { // [START android_notification_add_reply] // Key for the string that's delivered in the action's intent. - val KEY_TEXT_REPLY = "key_text_reply" val replyLabel: String = context.resources.getString(R.string.reply_label) val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run { setLabel(replyLabel) @@ -212,7 +213,7 @@ fun addActionButton(context: Context) { // [START android_notification_retrieve_user_input] private fun getMessageText(intent: Intent): CharSequence? { - return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(ReplyReceiver.KEY_TEXT_REPLY) + return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_TEXT_REPLY) } // [START android_notification_retrieve_user_input] @@ -240,7 +241,7 @@ fun handledReply(context: Context) { 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 { + val remoteInput: RemoteInput = RemoteInput.Builder(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. diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt index a24d94d27..f6a32998f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/notifications/ReplyReceiver.kt @@ -21,7 +21,6 @@ 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] @@ -32,13 +31,10 @@ class ReplyReceiver : BroadcastReceiver() { if (imageUri != null) { // Extract the image - try { - val inputStream = context.contentResolver.openInputStream(imageUri) + 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) } } }