Save data from push notifications to SQLite
Tags: tutorial, android, sqlite, kotlin
Published: 2020-09-01 14:20:29

---
title: Save data from push notifications to SQLite
published: true
description: A Kotlin tutorial using OneSignal
tags: tutorial, android, sqlite, kotlin
cover_image: https://res.cloudinary.com/arthurdenner/image/upload/v1598137748/posts/background_service_android.png
---

# 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](#finalcode) or keep reading for a detailed walkthrough.

# Context

In a recent personal project ([Notificador LTV](https://play.google.com/store/apps/details?id=com.notificador.legendastv)), I had to add push notification capabilities to the Flutter app. I used [OneSignal](https://onesignal.com/) for it and the integration was really easy.

One requirement was to save the received notification in the [SQLite](https://www.tutorialspoint.com/android/android_sqlite_database.htm) database. Implement everything in Flutter was straightforward using [`sqflite`](https://pub.dev/packages/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](https://developer.android.com/guide/components/services) on Android.

OneSignal provides the [`NotificationExtenderService`](https://documentation.onesignal.com/v7.0/docs/service-extensions#android-notification-service-extension) 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:

```kotlin
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](https://documentation.onesignal.com/v7.0/docs/service-extensions#android-notification-service-extension) about it.

## Declare class in manifest

Add the following to your `AndroidManifest.xml`:

```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:

```kotlin
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](https://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper) 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
1. Access the data received
1. Check if the data should be persisted
1. Persist the data
1. Handle errors

### Update imports

```kotlin
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`](https://documentation.onesignal.com/v7.0/docs/40-api-android-native#osnotificationpayload-class) 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:

```kotlin
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:

```kotlin
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](https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase#query(java.lang.String,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String)) before deciding if the data should be persisted - prevent two notifications with the same title in this example:

```kotlin
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`](https://developer.android.com/reference/android/content/ContentValues) object to persist data:

```kotlin
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 <a name="finalcode"></a>

```kotlin
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](https://documentation.onesignal.com/v7.0/docs/service-extensions) 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](https://linktr.ee/arthurdenner) on any platform for more.