Blog Infos
Author
Published
Topics
Published

 

 

Intro

In Chapter 2 we provided a robust mechanism for queueing notifications. The system is able to persist notifications and fetch it with a given interval. This chapter is focused on showing notifications to a user.

Appearance component in high-level design

Notification types

There are two types of notifications in the app. We named it Fullscreen and System. All notifications are sharing some configuration like:

Other properties that should be configured independently are declared in NotificationData class.

System

It is a regular notification in the Android system. We only use Notification.Builder() API for setting it up and showing.

Fullscreen

Notification based on new activity. It has a fully custom UI, fully custom gesture detection for swipe and click, fully custom animation for hiding and so on. Generally it’s used when notifications demands the user’s immediate attention.

Prior to API29 we could just send an Intent for opening a particular activity. But API29 brought new restrictions. So after API29 we must use setFullscreenIntent() of Notification.Builder(). Behaviour is differs but I’m considering result as a satisfying.

Implementation
System

Declare the simple interface that will convert somehow NotificationData to android.app.Notification :

// :notifications
interface SystemNotificationBuilder {

 fun buildSystemNotification(data: NotificationData): Notification
}

I intentionally choose an API with returning type and move call NotificationManager.notify() outside of this abstraction. In future, it’ll be beneficial for Fullscreen notifications implementation.

After API26 we have to create NotificationChannel before showing notifications. So we need two(at least for now) implementations of the builder — one for API26 and one for older Android versions.

Let’s start with the second one:

// :notifications
class VintageSystemNotificationBuilder(
 private val context: Context
) : SystemNotificationBuilder {

 override fun buildSystemNotification(data: NotificationData): Notification {
   val remoteViews = setupRemoteViews(data) // <1>

   return NotificationCompat.Builder(context, data.channel.toString())
     .setSmallIcon(R.drawable.ic_notification)
     .setAutoCancel(true)
     .setContentText(data.description)
     .setContentTitle(data.title)
     .setPriority(PRIORITY_MAX)
     .setCategory(CATEGORY_ALARM)
     .setDefaults(Notification.DEFAULT_SOUND or Notification.DEFAULT_VIBRATE)
     .setCustomContentView(remoteViews)
     .build()
 }
}

VintageSystemNotificationBuilder is a core of all notifications in the framework. It keeps the most common configuration for notifications and sharing it across all use cases. Also here (1) is settled up custom view for notification (remembering from Chapter 1 — we should have gradient background there).

// :notifications
@RequiresApi(Build.VERSION_CODES.O)
class Api26SystemNotificationBuilder(
 private val context: Context,
 private val delegate: SystemNotificationBuilder, // <1>
 private val notificationManager: NotificationManager
) : SystemNotificationBuilder {

 override fun buildSystemNotification(data: NotificationData): Notification {
   val channel = createChannel(data.channel) 
   notificationManager.createNotificationChannel(channel)

   return delegate.buildSystemNotification(data) // <2>
 }

 private fun createChannel(channel: NotificationData.Channel): NotificationChannel =
   NotificationChannel(
     channel.toString(),
     context.getString(channel.displayNameRes),
     channel.importance
   )
}

This implementation is targeting to API26 and creating NotificationChannel first and then delegating notification creation to VintageSystemNotificationBuilder

  1. as a VintageSystemNotificationBuilder
  2. Delegating the creation

Now it is ready to be used. We should inject correct implementation to NotificationPoller . I’m using a factory for it:

// :notifications
class SystemNotificationBuilderFactory @Inject constructor(
 private val context: Context,
 private val notificationManager: NotificationManager
) {

 fun create(): SystemNotificationBuilder {
   val vintageSystemNotificationManager = VintageSystemNotificationBuilder(context)

   return if (Build.VERSION.SDK_INT >= 26) Api26SystemNotificationBuilder(
     context,
     vintageSystemNotificationManager,
     notificationManager
   )
   else vintageSystemNotificationManager
 }
}

In DI module (Dagger2 in snippet) you should provide SystemNotificationBuilder using this factory:

// :notifications
@Provides
@Singleton
fun systemNotificationBuilder(factory: SystemNotificationBuilderFactory): SystemNotificationBuilder =
 factory.create()
Fullscreen
// :notifications
interface FullscreenNotificationManager {

 fun showFullscreenNotification(data: NotificationData)
}

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

It’s named Manager and the API is different from SystemNotificationBuilder — it doesn’t return android.Notification because it’s showing should happen inside.

Let’s start with a vintage:

// :notifications
class VintageFullscreenNotificationManager(
 private val context: Context
) : FullscreenNotificationManager {

 override fun showFullscreenNotification(data: NotificationData) {
   val intent = context
     .fullscreenIntent(data) // <1>
     .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } // <2>

   context.startActivity(intent)
 }
}
  1. Simple extension on Context for creating explicit intent to open FullscreenNotificationActivity (will described further)
  2. Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag
// :notifications
@RequiresApi(Build.VERSION_CODES.Q)
class Api29FullscreenNotificationManager(
 private val systemNotificationBuilder: SystemNotificationBuilder, // <1>
 private val notificationManager: NotificationManager,
 private val context: Context
) : FullscreenNotificationManager {

 override fun showFullscreenNotification(data: NotificationData) {
   val intent = context.fullscreenPendingIntent(data) {
     flags = Intent.FLAG_ACTIVITY_NEW_TASK
   } // <2>
  
   val systemNotification = systemNotificationBuilder.buildSystemNotification(data) // <3>

   val notification = NotificationCompat.Builder(context, systemNotification)
     .setFullScreenIntent(intent, true)

   notificationManager.notify(data.id, notification)
 }
}
  1. as a API26SystemNotificationBuilder
  2. Simple extension on Context for creating PendingIntent to open FullscreenNotificationActivity (will be described further)
  3. Delegating the creation of system notifications for getting commonly configured notification

Using the same factory approach:

// :notifications
class FullscreenNotificationManagerFactory @Inject constructor(
 private val context: Context,
 private val notificationManager: NotificationManager,
 private val systemNotificationBuilder: SystemNotificationBuilder,
) {

 fun create(): FullscreenNotificationManager =
   if (Build.VERSION.SDK_INT >= 29) Api29FullscreenNotificationManager(
     systemNotificationBuilder,
     notificationManager,
     context
   )
   else VintageFullscreenNotificationManager(context)
}
Fullscreen Activity

Your project will have its own activity implementation, purpose of this snippet is help with basic scenarios you’ll need to handle.

// :notifications
class FullscreenNotificationActivity : Activity() {

  private lateinit var notificationData: NotificationData
  private lateinit var binding: ActivityNotificationFullscreenBinding

  @Inject
  lateinit var notificationManager : NotificationManager

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityNotificationFullscreenBinding.inflate(layoutInflater)
    setContentView(binding.root)

    notificationData = intent.getSerializableExtra(EXTRA_NOTIFICATION_DATA) as NotificationData // <1>

    setupWindow()
    applyData(notificationData)

    binding.notification.setOnClickListener {
        swipeNotification(-screenWidth) {
          performNotificationAction()
        }
      }
    }
  }

  @SuppressLint("ClickableViewAccessibility")
  private fun setupWindow() { // <2>
    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
    window.statusBarColor = Color.TRANSPARENT

    val swipeThreshold = 500

    var swipeStartX = 0f
    var swipeDistance = 0f
    var viewStartX = 0f

    window.decorView.rootView.setOnTouchListener { _, motionEvent ->
      val notificationView = binding.notification.root
      val idleX = notificationView.marginStart

      return@setOnTouchListener when (motionEvent.action) {
        MotionEvent.ACTION_DOWN -> {
          swipeStartX = motionEvent.rawX
          viewStartX = notificationView.x
          true
        }
        MotionEvent.ACTION_MOVE -> {
          swipeDistance = motionEvent.rawX - swipeStartX
          notificationView.x = viewStartX + swipeDistance
          true
        }
        MotionEvent.ACTION_UP -> {
          val stickX =
            if (swipeDistance.absoluteValue < swipeThreshold) idleX
            else if (swipeDistance > 0) screenWidth
            else -screenWidth

          swipeNotification(stickX) {
            if (stickX != idleX) {
              finishAndCancelNotification()
            }
          }

          true
        }
        else -> super.onTouchEvent(motionEvent)
      }
    }
  }

  private fun swipeNotification(stickX: Int, onEndAction: () -> Unit) { // <3>
    binding.notification.root.animate()
      .x(stickX.toFloat())
      .setInterpolator(AccelerateDecelerateInterpolator())
      .withEndAction(onEndAction)
      .start()
  }

  private fun applyData(data: NotificationData) { // <4>
    val gradient = GradientDrawable(
      GradientDrawable.Orientation.LEFT_RIGHT,
      intArrayOf(data.backgroundGradientStart, data.backgroundGradientEnd)
    )

    binding.notification.apply {
      title.text = data.title
      title.setTextColor(data.textColor)

      description.text = data.description
      description.setTextColor(data.textColor)

      root.background = gradient

      when (val iconData = data.icon) {
        is NotificationData.Icon.Res -> {
          icon.setImageResource(iconData.resId)
          if (iconData.tintRes != 0) {
            icon.imageTintList = ColorStateList.valueOf(getColor(iconData.tintRes))
          }
        }
        is NotificationData.Icon.Url -> {
          icon.load(iconData.url)
        }
      }
    }
  }

  private fun performNotificationAction() { // <5>
    val contentIntent = contentIntent(notificationData)
    startActivity(contentIntent)
    finishAndCancelNotification()
  }

  private fun finishAndCancelNotification() { // <6>
    notificationManager.cancel(notificationData.id)
    finish()
  }
}
  1. Extracting NotificationData from intent
  2. Setting up Window. We need a fully transparent window, so it is important to make the status bar transparent. Also here is settled up gestures handles
  3. Swipe notification body out of screen with end action.
  4. Applying NotificationData to the UI
  5. Perform notification action on click on notification body. It is simple starting the main (and single) activity with forwarding NotificationData as an serializable extra
  6. As Fullscreen notification is fully custom handled at API < 26 we shouldn’t forget to let NotificationManager know that notification is canceled

Another important part is configuring FullscreenNotificationActivity in AndroidManifest.xml

// :notifications
<activity
    android:name=".fullscreen.FullscreenNotificationActivity"
    android:excludeFromRecents="true"
    android:exported="false"
    android:launchMode="singleTask"
    android:theme="@android:style/Theme.Translucent.NoTitleBar" />
Usage

Now when implementation is done, we are ready to use it in NotificationPoller.showNotifications() method:

// NotificationPoller
private fun showNotification(data: NotificationData) {
 when (data.type) {
   NotificationData.Type.FULLSCREEN ->
     fullscreenNotificationManager.showFullscreenNotification(data)

   NotificationData.Type.SYSTEM -> {
     val notification = systemNotificationBuilder.buildSystemNotification(data)
     notificationManager.notify(data.id, notification)
   }
 }
}
Conclusion

This chapter covers implementations for showing notifications. Framework having a core of notification appearance declared in a single place — VintageSystemNotificationBuilder and using this consistently across all the implementations for System and Fullscreen notifications. FullscreenNotificationActivity can be configured as you need, but the core use cases like gesture detection for swipes, canceling notification in NotificationManager, performing notification action, and manifest configuration can be used as is.

The first 3 chapters are covering the infrastructure for the notifications framework: polling, persistence, and appearance. The final chapter will cover notification sources as well as the core of client-based notifications — scheduling.

This article was originally published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In Chapter 1 we have chosen a Looper and Queue as a high-level design…
READ MORE
blog
Showing notifications is a great mechanism for engaging users at mobile platforms. Smart management…
READ MORE
blog
At this point we have an infrastructure to robustly show notifications. Now we are…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu