Skip to content

07 - Android Notifications

Notifications

Notifications

  • Status bar

Notifications

  • Drawer

Notifications

  • Lock screen

Notifications

  • Heads-up (5.0+)

Notifications

  • App icon badge
  • Wear OS

Design

Material Design guide

  • http://developer.android.com/training/material/index.html

Notifications design guide

  • http://developer.android.com/design/patterns/notifications.html

Android Wear! maybe its coming back? - Oppo, Xiaomi Mi

Notifications

Notifications

Notifications

Notifications

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

Notifications

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
1
2
3
4
5
6
7
8
9
    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)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    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+)

Notifications

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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

1
2
3
4
5
6
<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()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
 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
    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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

 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
<?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

1
2
3
4
5
6
<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

1
2
3
4
5
6
<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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
         // 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())