Save data from push notifications to SQLite

Save data from push notifications to SQLite

Β·

9 min read

Introduction

In this tutorial, you'll learn how to create a background service using Kotlin to listen to push notifications even when the app is closed, handle them and save them to SQLite.

Even though the context is a Flutter app, this is applicable to native development as you'll have to write native Android code anyway.

Jump to the final code or keep reading for a detailed walkthrough.

Context

In a recent personal project (Notificador LTV), I had to add push notification capabilities to the Flutter app. I used OneSignal for it and the integration was really easy.

One requirement was to save the received notification in the SQLite database. Implement everything in Flutter was straightforward using sqflite, however another requirement was that this behavior should also happen when the user has closed the app completely. For that, I needed to implement a background service on Android.

OneSignal provides the NotificationExtenderService class that you can extend to add such functionality so you only need to deal with the SQLite code.

Setup

Listen to push notifications

The first step is to create a class that extends NotificationExtenderService. Create CustomNotificationExtender.kt under the folder android/app/src/main/kotlin/your/package/name, alongside with MainActivity.kt.

Add the following code to this file:

package your.package.name

import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult

class CustomNotificationExtender : NotificationExtenderService() {
  protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
    return false
  }
}

This class only listens to the background notifications and allows them to be displayed on the UI by returning false - check OneSignal's documentation about it.

Declare class in manifest

Add the following to your AndroidManifest.xml:

<service
   android:name=".CustomNotificationExtender" // change this if your class has another name
   android:permission="android.permission.BIND_JOB_SERVICE"
   android:exported="false">
   <intent-filter>
      <action android:name="com.onesignal.NotificationExtender" />
   </intent-filter>
</service>

Describe the database

In order to interact with SQLite, you need to describe the database and tables used. Create a helper class for it and for the sake of the example, the table will have only two columns: id and title.

Inside the same file you've created add the following code:

package your.package.name

import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult

object MyData {
  const val TABLE_NAME = "mytable"
  const val COLUMN_NAME_ID = "id"
  const val COLUMN_NAME_TITLE = "title"
}

class MyDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
  private val CREATE_MY_TABLE =
    "CREATE TABLE ${MyData.TABLE_NAME} (" +
      "${MyData.COLUMN_NAME_ID} INTEGER PRIMARY AUTOINCREMENT," +
      "${MyData.COLUMN_NAME_TITLE} TEXT)"

  override fun onCreate(db: SQLiteDatabase) {
    db.execSQL(CREATE_MY_TABLE)
  }

  override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    onCreate(db)
  }

  companion object {
    const val DATABASE_VERSION = 1
    const val DATABASE_NAME = "my_database.db"
  }
}

class CustomNotificationExtender : NotificationExtenderService() {
  protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
    return false
  }
}

Now you've created an object that describes some static values for table and columns name - it will be helpful later on - and an object that extends SQLite's OpenHelper that describes what it should do when connecting to the database - and versions it too.

Persist the notification data

Now you need to access the data received and persist it. Let's break it into a few steps:

  1. Update imports
  2. Access the data received
  3. Check if the data should be persisted
  4. Persist the data
  5. Handle errors

Update imports

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult

Access the data received

The receivedResult variable has the property payload of type OSNotificationPayload which can be used to get the notification message, additional data and more. It also has restoring and isAppInFocus which gives the state of the app.

In this example, we only want the title as the id is autogenerated when a record is inserted:

val title = receivedResult.payload.title

Check if the data should be persisted

Depending on how you're handling notifications on the Flutter code, you may not want to do anything in the native code. If that's the case, you can do something like:

if (receivedResult.isAppInFocus || receivedResult.restoring) {
  Log.i("ExtenderIgnored", "App is in focus or restoring")
  // `false` allows the notification to be displayed
  return false
}

You may want to run a query on SQLite) before deciding if the data should be persisted - prevent two notifications with the same title in this example:

val context = getBaseContext()
val dbHelper = MyDbHelper(context)
val db = dbHelper.getWritableDatabase()
val cursor = db.query(MyData.TABLE_NAME, null, "title = ?", arrayOf(title), null, null, null, null)

// Don't persist data if another record has the same title
if (cursor.count > 0) {
  // `false` allows the notification to be displayed
  return false
}

Persist the data

You need a ContentValues object to persist data:

val values = ContentValues()
values.put(MyData.COLUMN_NAME_TITLE, title)

db.insert(MyData.TABLE_NAME, null, values)

// `false` allows the notification to be displayed
return false

Handle errors

Some of the code above can fail, therefore you should wrap it in a try/catch.

Final code

package your.package.name

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import com.onesignal.NotificationExtenderService
import com.onesignal.OSNotificationReceivedResult

object MyData {
  const val TABLE_NAME = "mytable"
  const val COLUMN_NAME_ID = "id"
  const val COLUMN_NAME_TITLE = "title"
}

class MyDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
  private val CREATE_MY_TABLE =
    "CREATE TABLE ${MyData.TABLE_NAME} (" +
      "${MyData.COLUMN_NAME_ID} INTEGER PRIMARY AUTOINCREMENT," +
      "${MyData.COLUMN_NAME_TITLE} TEXT)"

  override fun onCreate(db: SQLiteDatabase) {
    db.execSQL(CREATE_MY_TABLE)
  }

  override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    onCreate(db)
  }

  companion object {
    const val DATABASE_VERSION = 1
    const val DATABASE_NAME = "my_database.db"
  }
}

class CustomNotificationExtender : NotificationExtenderService() {
  protected override fun onNotificationProcessing(receivedResult: OSNotificationReceivedResult): Boolean {
    try {
      if (receivedResult.isAppInFocus || receivedResult.restoring) {
        Log.i("ExtenderIgnored", "App is in focus or restoring")
        return false
      }

      val title = receivedResult.payload.title

      val context = getBaseContext()
      val dbHelper = MyDbHelper(context)
      val db = dbHelper.getWritableDatabase()
      val cursor = db.query(MyData.TABLE_NAME, null, "title = ?", arrayOf(title), null, null, null, null)

      // Don't persist data if another record has the same title
      if (cursor.count > 0) {
        return false
      }

      val values = ContentValues()
      values.put(MyData.COLUMN_NAME_TITLE, title)

      db.insert(MyData.TABLE_NAME, null, values)

      return false
    } catch (e: Exception) {
      // Report it to an external service if you wish
      Log.e("NotificationExtenderServiceError", e.toString())
      return false
    }
  }
}

Notes

Check OneSignal's documentation for more details.

If you're using a different solution or have any suggestions to improve this example, share them in the comments.

As this was my first time writing native Android code, if I've done something wrong or not recommended, please let me know.

I haven't implemented the same behaviour on iOS so if you have resources to link on this topic, share them in the comments.


I hope you enjoyed this post and follow me on any platform for more.