Frequently we wish to inform the user of an event, such as receiving an email, or getting a new message in social media. Such a message is known as a notification. In Android, a notification is a message which appears as an icon at the top of the device and also appears in a notification list. The Notification
class represents a notification.
Notification
object, by specifying options for the notification such as the icon and the associated text. The builder is used in a similar way to an AlertDialog.Builder. For example:
val notification = Notification.Builder(this) .setContentTitle("Time update") .setContentText("Time is now ${System.currentTimeMillis()}") .setSmallIcon(R.drawable.caution) .build()This will create a notification with the title "Time update" and full message text showing the current time in milliseconds since Jan 1 1970.
NotificationManager
(a system-wide object) and call its notify()
method, passing in the Notificiation
object:
val nMgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager nMgr.notify(uniqueId, notification) // uniqueId is a unique ID for this notification
In many cases, we will want something to happen when the user clicks on a notification. or example, imagine a mapping app in which you receive a notification when you are nearby a point of interest. You might want a separate activity to launch when the user clicks on the notification, which displays full details of the point of interest (e.g a description, and reviews, for a pub or hotel).
To do this we need to use pending intents. A pending intent is an intent which will occur at some future time (e.g. when the user clicks on the notification) hence the name PendingIntent
.
Here is an example of creating a pending intent. Note we must first create an Intent
, and then wrap it with a PendingIntent
:
// First, create an Intent, to launch the EmailActivity val intent1 = Intent(this, EmailActivity::class.java).apply { putExtra("emailMessageId", 2345) } // Set the flags (options) for the Intent (discussed below) intent1.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // Now create a PendingIntent referencing that intent val launchActivityPendingIntent = PendingIntent.getActivity(this, 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT)You then use the
PendingIntent
in a notification:
val notification = Notification.Builder(this) .setContentTitle("New email message") .setContentText("You have a new email message") .setSmallIcon(R.drawable.message) .setContentIntent(launchActivityPendingIntent) .build()
One common use-case for pending intents is to navigate back to the activity which generated the notification, by clicking on a notification. The main activity of an email app might notify you when an email has been received. However, you might be using another app at the time, with the email app in a stopped state in the background (i.e. between onStop()
and onStart()
). What should happen is that the email app should become visible again when you click on the notification.
This is a little more complex than you might think: you have to account for whether the email activity is already running in the background or not; if it is, it should become visible, but if it is not, it should be launched. The code above will achieve this.
The key things to note here are the flags
of the intent (note: setFlags()
in Java). To repeat this code:
intent1.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOPThese mean the following:
FLAG_ACTIVITY_CLEAR_TOP
- if there are other activities above the activity
generating the notification, clear them from the activity stackFLAG_ACTIVITY_SINGLE_TOP
- if the activity is already the top activity on the stack
(i.e. the one currently showing), do not relaunch it but use the existing
copy of the activityFLAG_ACTIVITY_SINGLE_TOP
, another copy of the activity would be
created and launched when the user presses the notification, so we have two copies
in the stackFLAG_ACTIVITY_SINGLE_TOP
), the activity will not be
relaunched, but an Intent will still be delivered to itonNewIntent()
method which will handle all Intents delivered
to the activity, whether it's relaunched or notaction
and then extracting the extras from itNotifications can have additional actions (which appear as buttons).
You use Notification.Builder
's addAction()
with an Action
object to add these; again providing an icon, text and a PendingIntent. For example, here we create a button "Stop Music Player" which is associated with a PendingIntent
containing a broadcast Intent
to send a broadcast with the action STOP_MUSIC
. (Note how we use PendingIntent.getBroadcast()
rather than PendingIntent.getActivity()
)
val broadcast =Intent().apply { action = "STOP_MUSIC" } val piStopMusic = PendingIntent.getBroadcast(this, 1, intent2, PendingIntent.FLAG_UPDATE_CURRENT) val notification = Notification.Builder(this) .setContentTitle("Song update") .setContentText("Now playing: Oh Well by Fleetwood Mac") .setSmallIcon(R.drawable.stop) .setContentIntent(pendingIntent) .addAction(Notification.Action.Builder(R.drawable.icon2, "Stop Music", piStopMusic).build()) .build()
On Android Oreo (API level 26) and upwards, notifications must be associated with a particular channel. Channels group together related notifications; all notifications on a given channel can be associated with the same sound or light colour (e.g. flashing green for text message, blue for a social media update, and so on. A user can allow or block all notifications on a particular channel for a particular app, by going to the settings for that app, selecting "Notifications", and turning that specific channel on or off.
To use channels:
NotificationChannel
object with a given IDNotification.Builder
specify the channel ID in the Builder constructorThe example was originally based on that provided here, but has been modified. More information on channels can be found on this page, including associating a channel with a notification light colour or vibration, organising channels into groups, opening the user's notification settings and deleting channels.
val channelID = "EMAIL_CHANNEL" // Check that we are running at least Oreo, channels can't be used in older versions if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.O) { val channel = NotificationChannel(channelID, "Email notifications", NotificationManager.IMPORTANCE_DEFAULT) val nMgr = getSystemService(NOTIFICATION_SERVICE) as NotificationManager nMgr.createNotificationChannel(channel) }When you create your notification with your builder, specify the channel ID:
val notification = Notification.Builder(this, channelID).setContentTitle(...).etc...Note that:
channelID
is a unique string identifier for that channel.NotificationChannel
, we supply the channel ID, the visible name of the channel ("Email notifications" here), and the channel importance. The importance controls how prominently the notifications are displayed.val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply{ putExtra(AlarmClock.EXTRA_MESSAGE, "Time to get up for early flight!") putExtra(AlarmClock.EXTRA_HOUR, 4) putExtra(AlarmClock.EXTRA_MINUTES, 30) } if(intent.resolveActivity(packageManager)!=null) { startActivity(intent) } else { Toast.makeText(this, "No activity to handle alarm intent", Toast.LENGTH_LONG).show() }
AlarmClock.ACTION_SET_ALARM
if(intent.resolveActivity(packageManager)!=null) { startActivity(intent) } else { Toast.makeText(this, "No activity to handle alarm intent", Toast.LENGTH_LONG).show() }
Intent.ACTION_DIAL
, this example
will launch the phone dialer with a specific phone number loaded indata
rather than adding extrasval intent = Intent(Intent.ACTION_DIAL).apply { data = Uri.parse("tel:+442382013075") } if(intent.resolveActivity(packageManager)!=null) { startActivity(intent) } else { Toast.makeText(this, "No activity to handle dialer intent", Toast.LENGTH_LONG).show() }
The ACTION_VIEW
implicit intent will open a web browser to view a given URL. Again the URL is set as the intent's data
.
val intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://www.free-map.org.uk") } if(intent.resolveActivity(packageManager)!=null) { startActivity(intent) } else { Toast.makeText(this, "No activity to handle web intent", Toast.LENGTH_LONG).show() }
val viewIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://www.free-map.org.uk") } val chooser = Intent.createChooser(viewIntent, "Pick a web browser") if(viewIntent.resolveActivity(packageManager)!=null) { startActivity(chooser) }
<activity....> <intent-filter> <action android:name="MyAction" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
intent-filter
tag inside the activity
tagintent-filter
tag we specify the action to allow through and
the category of intent (usually you can leave this as DEFAULT as here)getIntent()
getData()
MyAction
setResult()
, and then call finish()
(as you have seen
already with activities launched with regular Intents)BroadcastReceiver
question so that a broadcast is sent back from the service to the activity when a new GPS location is received. Write code in the main activity to display a notification containing the current location whenever the GPS location changes. Ensure you use a notification channel.ACTION_DISPLAY_MAP
and should take latitude, longitude, and map style (regular or OpenTopoMap, see last year) as extras. The activity should read the latitude and longitude from the implicit intent and set the map's location accordingly.
Spinner
(see below). This activity should use an implicit intent to launch your map activity from your original application when a button is pressed.
Spinner
is a dropdown list of items. Here is an example:
<Spinner android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/spAddNote" android:entries="@array/noteTypes" app:layout_constraintTop_toBottomOf="@id/etAddNote" />
android:entries
which is a reference to an array resource, i.e. any XML file with an <array> tag in the values
folder within res
:
<resources> <array name="noteTypes"> <item>Path problem/hazard</item> <item>Path directions</item> <item>Place of interest</item> </array> </resources>
selectedItem
property or its position within the list (starting at 0) with selectedPosition
.