Android

Scheduled Notifications


Reading Time: 5 minutes

Have you ever wondered how apps like Tinder, Facebook, Reddit and many more send annoying recurrent notifications to remind you haven’t read your messages or to let you know there are new people near you? Well, in this post I am going to show you my approach to creating your own annoying notifications. This is not so hard to do, however, there are multiple steps involved, so let me break it up for you:

  1. Create a simple database that will keep the user information.
  2. Make sure that our app is capable of receiving the push notification and store information into the database.
  3. Create a cloud function that checks the database and sends the notification.
  4. Create a cron job that calls our cloud function.

Create a simple database

Firestore

Let’s first create our little database. For this example, I am going to use Firestore, a NoSQL database you can create for free just by creating a project in Firebase. The structure is going to be pretty simple. For this project, I am not going to worry about the security so I will just go ahead and start the database in test mode. If you are going to create this in a production environment, be sure to add security. 

So the database will look like this:

users/device_id : {<data>}

 

We want to represent users by their device_id and keep all the information that way. The device_id will be provided by Firebase when we get the information into our device, for this, we just start the users collection, we don’t need to do anything else.

 

Push notifications and Firebase connection capabilities

So now we need an app capable of writing into Firebase and of receiving push notifications. For this we create a simple app on React Native. We are going to use the React Native Firebase and Moment libraries. React Native Firebase will help us get the information into Firestore and will also help us to receive the push notifications. This blogpost is not going to get into details about how to set up push notifications. For that you can review the official docs:

Initial setup: https://invertase.io/oss/react-native-firebase/quick-start/new-project

Firestore: https://invertase.io/oss/react-native-firebase/v6/firestore/quick-start

Cloud Messaging (Push Notifications): https://invertase.io/oss/react-native-firebase/v6/messaging/quick-start

Now as for the code, here is a snippet of what I do, the complete project is on GitHub if you want to check it out:

import AsyncStorage from '@react-native-community/async-storage';

import messaging from '@react-native-firebase/messaging';

import firestore from '@react-native-firebase/firestore';

import moment from 'moment';

const getToken = async () => {

   letfcmToken=awaitAsyncStorage.getItem('firebaseToken');

   console.log('IS TOKEN STORED: ', fcmToken)

   if (!fcmToken) {

      fcmToken=awaitmessaging().getToken();

      console.log('NEW TOKEN: ', fcmToken)

      if (fcmToken) {

         awaitAsyncStorage.setItem('firebaseToken', fcmToken);

      }

   }

};

const requestPermission = async () => messaging()

   .requestPermission()

   .then(() => {

   getToken();

})

.catch((error) => {

   console.warn(`${error} permission rejected`);

});

export const checkPermission = async () => {

   constenabled=awaitmessaging().hasPermission();

   if (enabled) {

      getToken();

   } else {

      requestPermission();

   }

};

export const firestoreNotificationData = async (userId) => {

   // Firestore Logic

   constenabled=awaitmessaging().hasPermission();

   if (enabled) {

      constfcmToken=awaitmessaging().getToken();

      lettoken=awaitAsyncStorage.getItem('firebaseToken');

      if (fcmToken!==token) {

         awaitAsyncStorage.setItem('firebaseToken', fcmToken);

         token=fcmToken;

      }

      if (token) {

         constref=awaitfirestore().collection('users').doc(token);

         firestore()

         .runTransaction(async (transaction) => {

            constdoc=awaittransaction.get(ref);

            // if it does not exist set the population to one

            if (!doc.exists) {

               transaction.set(ref, {

                  uuid:userId,

                  lastUpdate:moment().unix()

               });

               return userId;

            }

            transaction.update(ref, {

               uuid:userId,

               lastUpdate:moment().unix()

            });

            return userId;

         })

         .catch((error) => {

            console.log('Transaction failed: ', error);

         });

         return 'ok';

      }

   }

   return 'ok';

};



Google Cloud function

Now that we have our simple app finished which can receive notifications and write the device_id and the last time you logged into the database, we need to create a function that will handle the search into the database, then we should compare it to the last time we updated it with a day or any time you want the notification to be sent, and if the conditions are correct, then send the push notification.

For this, we’re going to keep on using Google, and we choose Google Cloud Functions as the one to handle our WSS. First, you will need to download the Firebase functions SDK toolkit into your machine.

Follow this guide up to step 3: https://firebase.google.com/docs/functions/get-started

Finally, we can create our function, So here’s what I have:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const moment = require('moment');
admin.initializeApp();
const TWO_MINUTES = 120;
exports.sendNotification = functions.https.onCall(async (input, context) => {
  try {
    var db = admin.firestore();
    const snapshot = await db.collection("users").get();
    const currentUnixTime = moment().unix();
    console.log("currentUnixTime", currentUnixTime);
    const payload = {
      notification: {
          title: 'Annoying Notification',
          body: 'You just send this automatically good for you!'
      }
    };
    snapshot.forEach(doc => {
      if(doc.data().uuid && ( doc.data().lastUpdate + TWO_MINUTES ) <= currentUnixTime) {
        console.log(`Sending to: ${doc.data().uuid}`, doc.id);
        admin.messaging().sendToDevice(doc.id, payload).then((response) => {
          // Response is a message ID string.
            console.log(`Successfully sent message to id ${doc.id}:`, response );
            console.log(`Error: ${doc.id}:`, response.results[0] );
            db.collection("users").doc(doc.id).update({
              lastUpdate: currentUnixTime
            });
            return response;
          }).catch((error) => {
            console.log('Error sending message:', error);
            return error;
          });
        }
    });
    return 'Success!';
  } catch (error) {
    console.log(error);
    return error;
  }
});

Cron job

Up to this point, we can test the complete app functionality by calling the function, after that, you should receive the notification into your app. Technically speaking, you can just press a button every day at noon or whenever you feel like it; nevertheless, we are not robots and there are tools that we can use for automation. I am going to use a Pub/Sub from Google Scheduler, which simply means an automatic task. At the moment of this writing, you can do 93 calls a month for free. However, if you were to want to test this part, you would need to add a card to your Google account. These 93 calls a month simply mean that you can call your function 3 times a day. Be careful not to exceed it, or else you will be billed by Google.

So first things first, we need to activate billing in our projects. You can do so by pressing here and following the steps on the Google Cloud console.

After this, you will need to go to https://console.cloud.google.com/apis/library/cloudscheduler.googleapis.com and just press the ‘enable’ button.

Lastly, be sure to enable the pub sub button in here, https://console.cloud.google.com/apis/library/pubsub.googleapis.com

 

Now we have our account ready to receive a Pub/Sub. Instead of our regular call: functions.https.onCall, we change it for this: functions.pubsub.schedule(‘0 10,14,20 * * *’).timeZone(‘America/New_York’).onRun

And what does this mean? (you might ask yourself). The first part should be pretty obvious, we are calling the pubsub function from functions instead of a https.onCall. It gets more interesting in the next part. As you can see, I am calling schedule with some arbitrary numbers and asterisks:

The first number is the exact minute you want the function to be executed, the second one is for the hour, next is the day of the month, after that is the month, and finally the day of the week. You can do any combination needed in the most accurate way for you. Here’s a cool page that can help you with this: https://crontab.guru. What I mentioned before means that every time the minute is zero, the hours are 10am, 2pm or 8pm, it does not matter the day, the month, or the day of the week, we should execute this function. Now, the complete version of it looks like this: (I just change it to one day in order to show you can add any time you need)

const functions = require('firebase-functions');
const admin = require('firebase-admin');

const moment = require('moment');

admin.initializeApp();

const ONE_DAY = 86400;

exports.scheduleNotification = functions.pubsub.schedule('0 17 * * *')

.timeZone('America/New_York')

.onRun(async context => {

  try {

    var db = admin.firestore();

    const snapshot = await db.collection("users").get();

    const currentUnixTime = moment().unix();

    console.log("currentUnixTime", currentUnixTime);

    const payload = {

      notification: {

          title: 'Annoying Notification',

          body: 'You just send this automatically good for you!'

      }

    };

    snapshot.forEach(doc => {

      if(doc.data().uuid && ( doc.data().lastUpdate + ONE_DAY ) <= currentUnixTime) {

        console.log(`Sending to: ${doc.data().uuid}`, doc.id);

        admin.messaging().sendToDevice(doc.id, payload).then((response) => {

          // Response is a message ID string.

            console.log(`Successfully sent message to id ${doc.id}:`, response );

            console.log(`Error: ${doc.id}:`, response.results[0] );

            db.collection("users").doc(doc.id).update({

              lastUpdate: currentUnixTime

            });

            return response;

          }).catch((error) => {

            console.log('Error sending message:', error);

            return error;

          });

        }

    });

    return 'Success!';

  } catch (error) {

    console.log(error);

    return error;

  }

});

And finally, you can set the timezone you want, for example, if you need a CT hour, you can change it to “America/Mexico_City”. Here’s a list of timezones that should work: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

As you can see, it is not as hard as it seems. If you want to check the complete code, you can check it here. As for the next steps, be sure to add some security into the firestore database, if you want that some users receive notifications, whereas some others don’t, be sure to check how to subscribe to a database, meaning you will be creating different tables for specific users. 

Well as for today, that would be it, be sure to ask anything in the comments part. I hope you find this useful, and thanks for reading.

Development
Triggerino And Arduino Based: Introducing
Best Practices
Hacking With Google
Development
Unicorn vs Benchmarking Thin