Android Notification Direct Reply action lets us reply to the notification message, it’s very popular with chat notifications such as Whatsapp and Facebook messenger notification messages.
Android Nougat has introduced several new features. It offers some awesome features such as Inline Reply Actions and Bundled Notifications. In this tutorial, we’ll be implementing Inline Replies in our application
Android Notification Direct Reply
Inline Reply Actions (also known as Direct Replies) allows us to reply to messages from the notifications itself. It makes life easier by removing the need to open applications for providing input. Such features are commonly seen in messaging applications. Direct Replies uses a combination of Notification Actions and Remote Input. Remote Input API provides a mechanism to access the entered text from the notification in our application.
RemoteInput requires the following Strings as input.
- Unique Key: This is used to correctly identify the text entered from the inline notification later on.
- Label: This is displayed to the user as a hint text.
Let’s implement a basic application that triggers a Notification with Inline Reply set as the Action.
Android Notification Direct Reply Project Structure
Android Notification Direct Reply Code
The code for the activity_main.xml
layout is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.journaldev.directreplynotification.MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BASIC INLINE REPLY NOTIFICATION" android:id="@+id/btn_basic_inline_reply" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/txt_inline_reply" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Replied text will be displayed here" android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/btn_basic_inline_reply" android:layout_marginLeft="8dp" app:layout_constraintLeft_toLeftOf="parent" android:layout_marginRight="8dp" app:layout_constraintRight_toRightOf="parent" /> <Button android:id="@+id/btn_inline_replies_with_history" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="INLINE REPLIES WITH HISTORY" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/txt_inline_reply" /> </android.support.constraint.ConstraintLayout> |
Note: We’ll deal with the second Button later in the tutorial.
The code for the MainActivity.java
class is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
package com.journaldev.directreplynotification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity implements View.OnClickListener { String KEY_REPLY = "key_reply"; public static final int NOTIFICATION_ID = 1; Button btnBasicInlineReply; TextView txtReplied; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); clearExistingNotifications(); btnBasicInlineReply = (Button) findViewById(R.id.btn_basic_inline_reply); txtReplied = (TextView) findViewById(R.id.txt_inline_reply); btnBasicInlineReply.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_basic_inline_reply: //Create notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle("Inline Reply Notification"); String replyLabel = "Enter your reply here"; //Initialise RemoteInput RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) .setLabel(replyLabel) .build(); //PendingIntent that restarts the current activity instance. Intent resultIntent = new Intent(this, MainActivity.class); resultIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent resultPendingIntent = PendingIntent.getActivity(this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); //Notification Action with RemoteInput instance added. NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( android.R.drawable.sym_action_chat, "REPLY", resultPendingIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .build(); //Notification.Action instance added to Notification Builder. builder.addAction(replyAction); Intent intent = new Intent(this, MainActivity.class); intent.putExtra("notificationId", NOTIFICATION_ID); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent dismissIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "DISMISS", dismissIntent); //Create Notification. NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, builder.build()); break; } } private void clearExistingNotifications() { int notificationId = getIntent().getIntExtra("notificationId", 0); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(notificationId); } } |
Two Notification Actions namely “REPLY” and “DISMISS” are set on the notification. Clicking REPLY would trigger the Direct Reply feature. Running the above code on the emulator would give the following output.
In the above gif, clicking the send icon from the notification shows an indicator. This implies that the notification is waiting for an acknowledgment from the activity. We haven’t processed the replied text in our activity yet. The inline replied text is retrieved using the key set in the RemoteInput instance as shown in the updated code for MainActivity.java
below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
package com.journaldev.directreplynotification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.util.Random; public class MainActivity extends AppCompatActivity implements View.OnClickListener { String KEY_REPLY = "key_reply"; public static final int NOTIFICATION_ID = 1; Button btnBasicInlineReply; TextView txtReplied; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnBasicInlineReply = (Button) findViewById(R.id.btn_basic_inline_reply); txtReplied = (TextView) findViewById(R.id.txt_inline_reply); btnBasicInlineReply.setOnClickListener(this); clearExistingNotifications() } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); processInlineReply(intent); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_basic_inline_reply: //Create notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle("Inline Reply Notification"); String replyLabel = "Enter your reply here"; //Initialise RemoteInput RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) .setLabel(replyLabel) .build(); int randomRequestCode = new Random().nextInt(54325); //PendingIntent that restarts the current activity instance. Intent resultIntent = new Intent(this, MainActivity.class); //Set a unique request code for this pending intent PendingIntent resultPendingIntent = PendingIntent.getActivity(this, randomRequestCode, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); //Notification Action with RemoteInput instance added. NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( android.R.drawable.sym_action_chat, "REPLY", resultPendingIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .build(); //Notification.Action instance added to Notification Builder. builder.addAction(replyAction); Intent intent = new Intent(this, MainActivity.class); intent.putExtra("notificationId", NOTIFICATION_ID); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent dismissIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "DISMISS", dismissIntent); //Create Notification. NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, builder.build()); break; } } private void clearExistingNotifications() { int notificationId = getIntent().getIntExtra("notificationId", 0); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(notificationId); } private void processInlineReply(Intent intent) { Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); if (remoteInput != null) { String reply = remoteInput.getCharSequence( KEY_REPLY).toString(); //Set the inline reply text in the TextView txtReplied.setText("Reply is "+reply); //Update the notification to show that the reply was received. NotificationCompat.Builder repliedNotification = new NotificationCompat.Builder(this) .setSmallIcon( android.R.drawable.stat_notify_chat) .setContentText("Inline Reply received"); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, repliedNotification.build()); } } } |
In the above code, onNewIntent is invoked when the reply key is pressed(Thanks to the Intent flags SINGLE_TOP and CLEAR_TOP).
The method processInlineReply()
is where we fetch the text entered in the notification and set it in the TextView. The notification is then updated (with the same NOTIFICATION_ID as when created) to get rid of the progress indicator and display that the reply was received.
In another world, instead of updating the notification we could have cancelled the notification by using the below snippet.
1 2 3 4 5 6 7 8 9 10 11 |
//Cancel notification NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(NOTIFICATION_ID); //Remove this from the MainActivity.java NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, repliedNotification.build()); |
The updated code above would give the following output in the emulator.
Note: Multiple Inline Reply Actions can be added in a notification by adding multiple addAction(remoteInput)
instances on the builder instance. Though it’s recommended to have just one inline reply action button per notification.
Notifications With Reply History
We can display previous inline responses in the Notification too with the help of the method setRemoteInputHistory
.
In this application we’ll be storing the Charsequences returned in inline replies in the form of a LinkedList as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
package com.journaldev.directreplynotification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.util.LinkedList; import java.util.List; import java.util.Random; public class MainActivity extends AppCompatActivity implements View.OnClickListener { String KEY_REPLY = "key_reply"; String KEY_REPLY_HISTORY = "key_reply_history"; public static final int NOTIFICATION_ID = 1; Button btnBasicInlineReply, btnInlineReplyHistory; TextView txtReplied; private static List<CharSequence> responseHistory = new LinkedList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnBasicInlineReply = (Button) findViewById(R.id.btn_basic_inline_reply); btnInlineReplyHistory = (Button) findViewById(R.id.btn_inline_replies_with_history); txtReplied = (TextView) findViewById(R.id.txt_inline_reply); btnBasicInlineReply.setOnClickListener(this); btnInlineReplyHistory.setOnClickListener(this); clearExistingNotifications(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); processInlineReply(intent); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_basic_inline_reply: createInlineNotification(); break; case R.id.btn_inline_replies_with_history: if (!responseHistory.isEmpty()) { CharSequence[] history = new CharSequence[responseHistory.size()]; createInlineNotificationWithHistory(responseHistory.toArray(history)); } else { createInlineNotificationWithHistory(null); } break; } } private void clearExistingNotifications() { int notificationId = getIntent().getIntExtra("notificationId", 0); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(notificationId); } private void processInlineReply(Intent intent) { Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); if (remoteInput != null) { CharSequence charSequence = remoteInput.getCharSequence( KEY_REPLY); if (charSequence != null) { //Set the inline reply text in the TextView String reply = charSequence.toString(); txtReplied.setText("Reply is " + reply); //Update the notification to show that the reply was received. NotificationCompat.Builder repliedNotification = new NotificationCompat.Builder(this) .setSmallIcon( android.R.drawable.stat_notify_chat) .setContentText("Inline Reply received"); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, repliedNotification.build()); /**Uncomment the below code to cancel the notification. * Comment the above code too. * **/ /*NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(NOTIFICATION_ID);*/ } else { String reply = remoteInput.getCharSequence(KEY_REPLY_HISTORY).toString(); responseHistory.add(0, reply); if (!responseHistory.isEmpty()) { CharSequence[] history = new CharSequence[responseHistory.size()]; createInlineNotificationWithHistory(responseHistory.toArray(history)); } else { createInlineNotificationWithHistory(null); } } } } private void createInlineNotification() { //Create notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle("Inline Reply Notification"); String replyLabel = "Enter your reply here"; //Initialise RemoteInput RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) .setLabel(replyLabel) .build(); int randomRequestCode = new Random().nextInt(54325); //PendingIntent that restarts the current activity instance. Intent resultIntent = new Intent(this, MainActivity.class); resultIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); //Set a unique request code for this pending intent PendingIntent resultPendingIntent = PendingIntent.getActivity(this, randomRequestCode, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); //Notification Action with RemoteInput instance added. NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( android.R.drawable.sym_action_chat, "REPLY", resultPendingIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .build(); //Notification.Action instance added to Notification Builder. builder.addAction(replyAction); Intent intent = new Intent(this, MainActivity.class); intent.putExtra("notificationId", NOTIFICATION_ID); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent dismissIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "DISMISS", dismissIntent); //Create Notification. NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, builder.build()); } private void createInlineNotificationWithHistory(CharSequence[] history) { //Create notification builder NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle("Inline Reply Notification With History"); String replyLabel = "Enter your reply here"; //Initialise RemoteInput RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY_HISTORY) .setLabel(replyLabel) .build(); int randomRequestCode = new Random().nextInt(54325); //PendingIntent that restarts the current activity instance. Intent resultIntent = new Intent(this, MainActivity.class); resultIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); //Set a unique request code for this pending intent PendingIntent resultPendingIntent = PendingIntent.getActivity(this, randomRequestCode, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); //Notification Action with RemoteInput instance added. NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( android.R.drawable.sym_action_chat, "REPLY", resultPendingIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .build(); //Notification.Action instance added to Notification Builder. builder.addAction(replyAction); Intent intent = new Intent(this, MainActivity.class); intent.putExtra("notificationId", NOTIFICATION_ID); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent dismissIntent = PendingIntent.getActivity(getBaseContext(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "DISMISS", dismissIntent); if (history != null) { builder.setRemoteInputHistory(history); } //Create Notification. NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, builder.build()); } } |
We’ve added a new Button to handle Inline Replies With History.
In the above code, responseHistory
holds the history of replies.
The following snippet is used to set the previous inline replies in the notification.
1 2 3 4 5 |
if (history != null) { builder.setRemoteInputHistory(history); } |
The else
part of method processInlineReply() updates the current notification with the new history of inline replies as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 |
else { String reply = remoteInput.getCharSequence(KEY_REPLY_HISTORY).toString(); responseHistory.add(0, reply); if (!responseHistory.isEmpty()) { CharSequence[] history = new CharSequence[responseHistory.size()]; createInlineNotificationWithHistory(responseHistory.toArray(history)); } else { createInlineNotificationWithHistory(null); } } |
responseHistory.add(0, reply);
adds the latest inline reply in the history of replies.
The output that the above code would give is:
Note: The above concept is for demonstration purposes only. You can tweak it according to your requirements.
Inline Replies do not work on versions prior to Android N. So to make the application functional on them, you can add a Basic EditText in the activity.
This brings an end to this tutorial. You can download the final Android DirectReplyNotifications Project from the link below.