Android Notifications

Most notification features need at least API 26 (Android 8) to work.

Implementation

We have to first create a NotificationChannel. Then we define a NotificationManager instance.

A NotificationChannel has an ID, a name and a description. It also has an importance level. This defines how to interrupt the user for any notification belonging to this channel.

We need to create the NotificationChannel before posting any notification.

class MainActivity: AppCompatActivity() {
	private val channelID = "es.codes.mario.template.channel1"
	private var notificationManager: NotificationManager? = null
	
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)
		
		notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
		createNotificationChannel(channelID, "TemplateChannel", "oh no! this is the channel's description")
		
		button.setOnClickListener {
			displayNotification()
		}
	}
	
	private fun displayNotification() {
		val notificationId = 45
		val notification = NotificationCompat.Builder(this@MainActivity.channelID)
			.setContentTitle("Template title")
			.setContentText("This is a test notification")
			.setSmallIcon(android.R.drawable.ic_dialog_info)
			.setAutoCancel(true)
			.setPriority(NotificationCompat.PRIORITY_HIGH)
			.build()
			
		notificationManager?.notify(notificationId, notification)
	}
	
	private fun createNotificationChannel(id: String, name: String, description: String) {
		// has to be higher than Oreo
		if(Build.VERSION.SDK_INT >= Build.VERSION_CODES_O) {
			val importance = NotificationManager.IMPORTANCE_HIGH
			val channel = NotificationChannel(id, name, importance).apply {
				description = channelDescription		
			}
		}
		
		notificationManager?.createNotificationChannel(channel)
		}
	}
}

Tap Actions

A user responds to a tap by opening an Activity or Fragment that corresponds to that notification. For this, we need to specify an Intent. We use PendingIntent when we want to use an Intent at some point in the future.

Expanding on the previous example.

private fun displayNotification() {
	val notificationId = 45

	// new changes
	val tapResultIntent = Intent(this, SecondActivity::class.java)
	val pendingIntent = PendingIntent.getActivity(this, 0, tapResultIntent, PendingIntent.FLAG_UPDATE_CURRENT)

	val notification = NotificationCompat.Builder(this@MainActivity.channelID)
		.setContentTitle("Template title")
		.setContentText("This is a test notification")
		.setSmallIcon(android.R.drawable.ic_dialog_info)
		.setAutoCancel(true)
		.setPriority(NotificationCompat.PRIORITY_HIGH)
		// set it here
		.setContentIntent(pendingIntent)
		.build()

	notificationManager?.notify(notificationId, notification)
}

Android lets use customize our Intent by adding flags.

val tapResultIntent = Intent(this, SecondActivity::class.java).apply {
	flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
}

Action Buttons

These buttons will appear beneath the notification message and directly let us open a new Activity.

private fun displayNotification() {
	val notificationId = 45

	val tapResultIntent = Intent(this, SecondActivity::class.java)
	val pendingIntent = PendingIntent.getActivity(this, 0, tapResultIntent, PendingIntent.FLAG_UPDATE_CURRENT)

	// new changes for action button
	val intentActivity = Intent(this, AnotherActivity::class.java)
	val pendingIntentActivity = PendingIntent.getActivity(this, 0, intentActivity, PendingIntent.FLAG_UPDATE_CURRENT)
	val actionActivity = NotificationCompat.Action.Builder(0, "Details", pendingIntentActivity)
		.build()

	val notification = NotificationCompat.Builder(this@MainActivity.channelID)
		.setContentTitle("Template title")
		.setContentText("This is a test notification")
		.setSmallIcon(android.R.drawable.ic_dialog_info)
		.setAutoCancel(true)
		.setPriority(NotificationCompat.PRIORITY_HIGH)
		.setContentIntent(pendingIntent)
		// add it here
		.addAction(actionActivity)
		.build()

	notificationManager?.notify(notificationId, notification)
}

Direct Reply

This allows us to facilitate the user to reply to a message without opening an activity or a fragment.

We need to define a key for the reply. This will be used to receive the user’s input.

class MainActivity: AppCompatActivity() {
	private val channelID = "es.codes.mario.template.channel1"
	private var notificationManager: NotificationManager? = null
	
	// we add this key
	private val KEY_REPLY = "key_reply"
	
	// no changes
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)
		
		notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
		createNotificationChannel(channelID, "TemplateChannel", "oh no! this is the channel's description")
		
		button.setOnClickListener {
			displayNotification()
		}
	}
	
	private fun displayNotification() {
		val notificationId = 45
		val tapResultIntent = Intent(this, SecondActivity::class.java)
		val pendingIntent = PendingIntent.getActivity(this, 0, tapResultIntent, PendingIntent.FLAG_UPDATE_CURRENT)

		// reply action
		val remoteInput = RemoteInput.Builder(KEY_REPLY).run {
			setLabel("Insert your name here")
			build()
		}
		
		val replyAction = NotificationCompat.Action.Builder(0, "REPLY", pendingIntent)
			.addRemoteInput(remoteInput)
			.build()

		// action button
		val intentActivity = Intent(this, AnotherActivity::class.java)
		val pendingIntentActivity = PendingIntent.getActivity(this, 0, intentActivity, PendingIntent.FLAG_UPDATE_CURRENT)
		val actionActivity = NotificationCompat.Action.Builder(0, "Details", pendingIntentActivity)
			.build()

		val notification = NotificationCompat.Builder(this@MainActivity.channelID)
			.setContentTitle("Template title")
			.setContentText("This is a test notification")
			.setSmallIcon(android.R.drawable.ic_dialog_info)
			.setAutoCancel(true)
			.setPriority(NotificationCompat.PRIORITY_HIGH)
			// remove this
			//.setContentIntent(pendingIntent)
			// add this instead
			.addAction(replyAction)
			.addAction(actionActivity)
			.build()

		notificationManager?.notify(notificationId, notification)
	}	
	
	// no changes
	private fun createNotificationChannel(id: String, name: String, description: String) {
		// has to be higher than Oreo
		if(Build.VERSION.SDK_INT >= Build.VERSION_CODES_O) {
			val importance = NotificationManager.IMPORTANCE_HIGH
			val channel = NotificationChannel(id, name, importance).apply {
				description = channelDescription		
			}
		}
		
		notificationManager?.createNotificationChannel(channel)
		}
	}
}

Then we add the code inside this new Activity to process the notification’s input and reply. To update to this notification we have to use same channelID and notificationId we used before or it would create a new notification instead of a reply.

class SecondActivity: AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_second)
		receiveInput()
	}
	
	private fun receiveInput() {
		// this has to be same value as origin
		val KEY_REPLY = "key_reply"
		val intent = this.intent
		val remoteInput = RemoteInput.getResultsFromIntent(intent)
		if(remoteInput != null) {
			val inputString = remoteInput.getCharSequence(KEY_REPLY).toString()
			// this is just a normal text view in the xml layout for output purposes
			result_text_view.text = inputString
			
			// this need to be the same as the ones we used to create the notification
			val channelID = "es.codes.mario.template.channel1"
			val notificationId = 45
			
			val repliedNotification = NotificationCompat.Builder(this, channelID)
				.setSmallIcon(android.R.drawable.ic_dialog_info)
				.setContentText("reply received")
				.build()
				
			val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
			notificationManager.notify(notificationId.repliedNotification)
		}
	}
}