07 - Android Notifications
Notifications
Notifications
- Status bar
- Drawer
- Lock screen
- Heads-up (5.0+)
- App icon badge
- Wear OS
Design
Material Design guide
Notifications design guide
Android Wear! maybe its coming back?
- Oppo, Xiaomi Mi
Standard notification
- Small icon - required
- App name – by system
- Time stamp – by system – or setWhen() or setShowWhen(false)
- Large icon – optional, only for contact photos
- Title – optional, setContentTitle()
- Text – optional, setContentText()
- Actions – add buttons, open app when notification is tapped
Changes over versions
-
API 21 (5.0) – Do not disturb mode
-
API 24 (7.0) – Notification grouping, Direct text input from notification
-
API 26 (8.0) - Notification channels are mandatory
- Better control by user (alarms, visibility, etc.)
- Max 1 notification sound per 1 second per 1 app
-
Notification is required for foreground service – long lived service in background and noticeable to user – ala media player or gps app.
Creating a Notification
-
Create the channel and set the importance – if on 8.0+
-
Specify the UI information and actions for a notification in a NotificationCompat.Builder object
-
Build the notification object
-
Pass the Notification object to the system by calling NotificationManager.notify()
-
Create channel, call it OnCreate
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"channel",
"Default channel",
NotificationManager.IMPORTANCE_DEFAULT
);
channel.description = "Default channel for Channel Demo"
var notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager;
notificationManager.createNotificationChannel(channel)
}
}
-
Required notification contents
- Small icon – setSmallIcon()
-
Optional contents
- Title – setContentTitle()
- Detail text – setContentText()
- Actions, priority, sound, led flashing, time, vibrate, ticker, …
- http://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html
fun button0OnClick(view: View) {
var builder = NotificationCompat.Builder(this, "channel")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setContentTitle("Title is here")
.setContentText("Content text is this!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
NotificationManagerCompat.from(this).notify(0, builder.build())
}
Notification action
- You should add at least one action – to navigate into your app/activity
- You can also add Buttons for additional actions
- Action is defined by PendingIntent, containing an Intent
- Navigation stack - what should happen in your activity, when back is pressed (should it navigate to the home screen or to some other activity)
fun button1OnClick(view: View) {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent
.getActivity(this,0, intent, PendingIntent.FLAG_IMMUTABLE)
var builder = NotificationCompat.Builder(this, "channel")
.setSmallIcon(android.R.drawable.sym_contact_card)
.setContentTitle("Title is here")
.setContentText("Content text is this!")
//.setContentInfo("info is here")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
NotificationManagerCompat.from(this).notify(0, builder.build())
}
Expanded layout
Expanded layout – action buttons depend on it (android 4.1+)
Update and remove
Update notification
- Issue notification with a notification ID by calling
NotificationManager.notify()
- Update or create a NotificationCompat.Builder object
- build a Notification object from it, and issue the Notification with the same ID.
- If the previous notification is still visible, the system updates it.
- If the previous notification has been dismissed, a new notification is created.
Remove notification
notificatoinManager.cancel(id)
notificatoinManager.cancelAll()
- Or use
setTimeoutAfter()
for timed removal
Navigation
Regular activity
Start an activity, that is part of normal workflow of your app
- ActivityA is your main activity
- ActivityB is started from notification
- Backstack: Home <- ActivityA <- ActivityB
Special activity
Start an activity, that is an extension of your notification
- No backstack needed, touching back takes you directly to Home screen
Regular notification
Define hierarchy in manifest
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ResultActivity"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
- Create Intent
- Create stack builder – TaskStackBuilder.Create()
- Get and use pendingIntent
// Create an Intent for the activity you want to start
val resultIntent = Intent(this, ResultActivity::class.java)
// Create the TaskStackBuilder
val resultPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run {
// Add the intent, which inflates the back stack
addNextIntentWithParentStack(resultIntent)
// Get the PendingIntent containing the entire back stack
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE)
}
Special notification
Manifest
<activity
android:name=".ResultActivity"
android:launchMode="singleTask"
android:taskAffinity=""
android:excludeFromRecents="true">
</activity>
Build and issue the notification
- Create Intent for Activity
- setFlags() with the flags FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TASK
- Create a PendingIntent from the Intent by calling getActivity()
fun onClickButtonNotify(view: View) {
// create intent
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
// construct and show notification
var builder = NotificationCompat.Builder(this, "channel")
.setSmallIcon(R.drawable.baseline_android_24)
.setContentTitle("Notification title")
.setContentText("Notification body text")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
// should notification be removed, when user clicks on it
.setAutoCancel(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
NotificationManagerCompat.from(this).notify(0, builder.build())
}
Communication from notification
Action, without starting whole new Activity
- Use broadcasts, ofcourse!
= PendingIntent.getBroadcast()
- And regular (not local) BroadcastReceiver in your app/activity
var broadcastReceiverIntentFilter = IntentFilter()
private val broadcastReceiver = CustomBrodcastReceiver()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
createNotificationChannel()
broadcastReceiverIntentFilter.addAction("ee.itcollege.akaver.cp")
}
fun button3OnClick(view: View) {
val notifyView = RemoteViews(packageName, R.layout.notification)
val intentLaunch = Intent(this, MainActivity::class.java)
val pendingIntentLaunch = PendingIntent.getActivity(this,0, intentLaunch,0)
val intent = Intent("ee.itcollege.akaver.cp")
val pendingIntent = PendingIntent.getBroadcast(this, 0, intent,0)
notifyView.setOnClickPendingIntent(R.id.imageButtonCheckpoint, pendingIntent);
notifyView.setTextViewText(R.id.textViewCounter, "some text")
var builder = NotificationCompat.Builder(this, "channel")
.setSmallIcon(android.R.drawable.sym_contact_card)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntentLaunch)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notifyview)
.setContent(notifyview)
NotificationManagerCompat.from(this).notify(0, builder.build())
}
private inner class CustomBrodcastReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("receiver", "onReceive")
}
}
override fun onResume() {
super.onResume()
this.registerReceiver(broadcastReceiver,broadcastReceiverIntentFilter )
}
override fun onPause() {
super.onPause()
this.unregisterReceiver(broadcastReceiver)
}
Custom layout
Use RemoteViews
Define layout in usual way (xml), max height 64dp (expanded 256dp)
val intentCp = Intent("ee.itcollege.akaver.cp")
val pendingIntentCp = PendingIntent.getBroadcast(this, 0, intentCp, 0)
val notifyview = RemoteViews(packageName, R.layout.notification)
notifyview.setOnClickPendingIntent(R.id.imageButtonCheckpoint, pendingIntentCp)
// construct and show notification
var builder = NotificationCompat.Builder(this, "channel")
.setSmallIcon(R.drawable.baseline_my_location_black_24)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notifyview)
.setContent(notifyview)
NotificationManagerCompat.from(this).notify(0, builder.build())
RemoteViews
Supports only these layouts:
- AdapterViewFlipper
- FrameLayout
- GridLayout
- GridView
- LinearLayout
- ListView
- RelativeLayout
- StackView
- ViewFlipper
And only these widgets:
- AnalogClock
- Button
- Chronometer
- ImageButton
- ImageView
- ProgressBar
- TextClock
- TextView
Descendants of these classes are also not supported.
NB!
Don’t set onClick on shared views (views used both in regular layouts and notifications)
RemoteViews - update content
- Update texts
remoteView.setTextViewText(R.id.text, "text")
- Set intents to button clicks
remoteView.setOnClickPendingIntent(R.id.button, pendingIntent)
- Since this is pending intent, you need to use proper broadcastReceiver to receive it
registerReceiver(mBroadcastReceiver, intentFilter)
unregisterReceiver(mBroadcastReceiver)
Custom Layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="64dp"
internal:layout_maxHeight="64dp"
internal:layout_minHeight="64dp" >
<ImageView
android:id="@+id/notification_icon_iv"
android:layout_width="64dp"
android:layout_height="64dp"
android:padding="10dp"
android:layout_alignParentLeft="true"
android:scaleType="center"
android:src="@drawable/notification_icon"
android:background="#3333B5E5" />
<ImageButton
android:id="@+id/notification_closebtn_ib"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:scaleType="centerInside"
android:src="@drawable/notification_exitbtn"
android:background="@drawable/notification_imagebtn_bg"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical"
android:gravity="top"
android:minHeight="64dp"
android:layout_toRightOf="@id/notification_icon_iv"
android:layout_toLeftOf="@id/notification_closebtn_ib"
android:orientation="vertical"
android:paddingBottom="2dp"
android:paddingEnd="8dp"
android:paddingTop="2dp" >
<TextView
android:id="@+id/notification_title_tv"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:ellipsize="marquee"
android:paddingTop="6dp"
android:singleLine="true"
android:text="JMols Service" />
<TextView
android:id="@+id/notification_contenttext_tv"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="Service running in the background" />
</LinearLayout>
</RelativeLayout>
Notifcation Background
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_pressed="true" android:drawable="@drawable/notification_bg_normal_pressed" />
<item android:state_pressed="false" android:drawable="@drawable/notification_bg_normal" />
</selector>
Imagebutton background
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_pressed="true" android:drawable="@drawable/notification_imagebtn_bg_normal_pressed" />
<item android:state_pressed="false" android:drawable="@drawable/notification_imagebtn_bg_normal" />
</selector>
Foreground service and notification
// construct and show notification
var builder = NotificationCompat.Builder(applicationContext, C.NOTIFICATION_CHANNEL)
.setSmallIcon(R.drawable.baseline_gps_fixed_24)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
builder.setContent(notifyview)
// Super important, start as foreground service - ie android considers this as an active app. Need visual reminder - notification.
// must be called within 5 secs after service starts.
startForeground(C.NOTIFICATION_ID, builder.build())