Using Firebase Cloud Messaging with Android

Firebase Cloud Messaging (FCM) is a free service offered by Google to send and receive cross platform messages. One of the primary example of FCM is Android application exchanging messages or receiving push notifications from a remote server. The notifications can be used to notify the Android Application about a new update or a new feed is available for download. The purpose can be extended to any developer need where the client application is required to be notified of any new event.

Building blocks of FCM

  • Remote Server Application – A remote trusted environment like Firebase GUI or Firebase Cloud Functions or any other server application.
  • FCM Cloud Messaging server – This is Google’s own infrastructure which handles and manages all messaging and exchange of messages between the Client apps and Server application.
  • Client Application – This is the client application like Android, iOS, WEB which receives messages.

FCM Configuration

Android Project Setup

Before we enable Firebase Cloud Messaging in Android project we need to create and link Firebase project. If you are new to Firebase then check out this post on how to connect Firebase project with Android application. Add Firebase Cloud Messaging from the tools menu and add FCM dependencies to your project app’s build.gradle file.

    implementation 'com.google.firebase:firebase-core:16.0.7'
    implementation 'com.google.firebase:firebase-messaging:17.4.0'

After the above step the Android device is ready to receive push notification.

Android Message targeting

If the goal is to target specific user devices or if the messages needs to be handled by the app then we can extend FirebaseMessagingService service. FirebaseMessagingService provides a function onNewToken which is required to push notification from the remote server application. onNewToken generates registration FCM token that can be sent to your server for targeting user device.

Update build.gradle file to add Retrofit2 and Firebase Auth libraries.

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.codeexa.com.fcmexample"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.google.firebase:firebase-core:16.0.7'
    implementation 'com.google.firebase:firebase-messaging:17.4.0'

    implementation 'com.google.firebase:firebase-auth:16.1.0'

    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
}

If the purpose is to send messages to this application instance or manage this apps subscriptions on the server side, send FCM Instance ID token to your app server.

In this example I have designed a basic email & password form backed by Firebase Auth. Replace activity layout file with the following xml code

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <android.support.design.widget.AppBarLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" >


        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" >

        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>
    <android.support.constraint.ConstraintLayout
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/registerButton"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="32dp"
            android:layout_marginRight="16dp"
            android:text="Login"
            android:textAppearance="@style/TextAppearance.AppCompat.Button"
            android:textColor="@android:color/white"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textInputLayout2" />

        <TextView
            android:id="@+id/fcmToken"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="8dp"
            android:text="Token: "
            android:textAppearance="@style/TextAppearance.AppCompat.Body1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/registerText" />

        <Button
            android:id="@+id/pushButton"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginRight="16dp"
            android:text="Push Message"
            android:visibility="gone"
            android:textAppearance="@style/TextAppearance.AppCompat.Button"
            android:textColor="@android:color/white"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/fcmToken" />

        <android.support.design.widget.TextInputLayout
            android:id="@+id/textInputLayout"
            android:layout_width="395dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/logo">

            <android.support.design.widget.TextInputEditText
                android:id="@+id/evEmail"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Email"
                android:inputType="textEmailAddress" />
        </android.support.design.widget.TextInputLayout>

        <ImageView
            android:id="@+id/logo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:adjustViewBounds="false"
            android:contentDescription="splash logo"
            android:text="Register"
            android:textAppearance="@style/TextAppearance.AppCompat.Headline"
            app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/progressBar"
            app:srcCompat="@mipmap/ic_launcher_round" />

        <android.support.design.widget.TextInputLayout
            android:id="@+id/textInputLayout2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textInputLayout">

            <android.support.design.widget.TextInputEditText
                android:id="@+id/evPassword"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Password"
                android:inputType="textPassword" />
        </android.support.design.widget.TextInputLayout>

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:indeterminate="true"
            android:max="100"
            android:visibility="invisible"
            android:backgroundTint="@android:color/white"
            android:indeterminateTint="@color/colorAccent"
            android:layout_marginTop="4dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/registerText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="8dp"
            android:text="New to App? Register Now!"
            android:textAppearance="@style/TextAppearance.AppCompat.Button"
            android:textColor="@color/design_default_color_primary"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/registerButton" />

    </android.support.constraint.ConstraintLayout>
</android.support.design.widget.CoordinatorLayout>

This layout will show a login form and on successful login attempt the app is designed to generate FCM token for the device.

Firebase AUTH for Android

 

Update AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.codeexa.com.fcmexample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">
        <activity android:name=".MainActivity"
            android:theme="@style/AppTheme.NoActionBar">
        <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Update styles.xml file to support Toolbar.

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.NoActionBar" parent="AppTheme">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
</resources>

Let’s see now how to create generate notification token. Call FirebaseInstanceId instance to fetch token.

    private void fetchInstanceRegistrationToken() {
        FirebaseInstanceId.getInstance().getInstanceId()
                .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
                    @Override
                    public void onComplete(@NonNull Task<InstanceIdResult> task) {
                        if(task.isSuccessful()){

                            // send token to app server
                            String token = task.getResult().getToken();

                        }else{
                            // token  failed
                        }
                    }
                });
    }

Activity Class

package com.codeexa.com.fcmexample;

import android.support.annotation.NonNull;
import android.support.design.widget.TextInputEditText;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;

import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    //("https://<region>-<project-id>.cloudfunctions.net")
    private static final String FIREBASE_CLOUD_FUNCTIONS_URL = "https://us-central1-<PROJECT-ID>.cloudfunctions.net/";

    private TextView mTextView;
    private TextInputEditText mEmailView, mPasswordView;
    private ProgressBar mProgressBar;
    private Button mSubmitBtn, mPushButton;
    private String instanceToken;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar= findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mSubmitBtn = findViewById(R.id.registerButton);
        mPushButton = findViewById(R.id.pushButton);
        TextView mRegisterText = findViewById(R.id.registerText);
        mSubmitBtn.setOnClickListener(this);
        mPushButton.setOnClickListener(this);
        mRegisterText.setOnClickListener(this);

        mEmailView = findViewById(R.id.evEmail);
        mPasswordView = findViewById(R.id.evPassword);
        mProgressBar = findViewById(R.id.progressBar);

        mTextView = findViewById(R.id.fcmToken);
    }

    @Override
    public void onClick(View v) {
        if(v.getId() == R.id.registerButton) {
            mSubmitBtn.setEnabled(false);
            mProgressBar.setVisibility(View.VISIBLE);
            loginUser();
        }else if(v.getId() == R.id.registerText){
            mSubmitBtn.setEnabled(false);
            mProgressBar.setVisibility(View.VISIBLE);
            registerUser();
        }else if(v.getId() == R.id.pushButton){
            mSubmitBtn.setEnabled(false);
            mProgressBar.setVisibility(View.VISIBLE);
            sendPushMessage();
        }
    }

    private void sendPushMessage() {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .build();
        Retrofit.Builder builder =
                new Retrofit.Builder()
                        .client(okHttpClient)
                        .baseUrl(FIREBASE_CLOUD_FUNCTIONS_URL)
                        .addConverterFactory(GsonConverterFactory.create());
        Retrofit retrofit = builder.build();
        RetrofitService service= retrofit.create(RetrofitService.class);
        service.sendPushNotification(instanceToken).enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {
                mProgressBar.setVisibility(View.INVISIBLE);

            }

            @Override
            public void onFailure(Call<String> call, Throwable t) {
                mProgressBar.setVisibility(View.INVISIBLE);

            }
        });
    }

    private void loginUser() {
        final FirebaseAuth mAuth = FirebaseAuth.getInstance();
        mAuth.signInWithEmailAndPassword(mEmailView.getText().toString(), mPasswordView.getText().toString())
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        mProgressBar.setVisibility(View.INVISIBLE);
                        if (task.isSuccessful()) {
                            // Sign in success, update UI with the signed-in user's information
                            fetchInstanceRegistrationToken();

                            //startMainActivity();
                        } else {
                            // If sign in fails, display a message to the user.
                            mProgressBar.setVisibility(View.INVISIBLE);
                            mTextView.setText(task.getException().getMessage());
                        }
                        // ...
                    }
                });
    }

    private void registerUser() {
        final FirebaseAuth mAuth = FirebaseAuth.getInstance();
        mAuth.createUserWithEmailAndPassword(mEmailView.getText().toString(), mPasswordView.getText().toString())
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()) {
                            // Sign in success, update UI with the signed-in user's information

                            fetchInstanceRegistrationToken();

                        } else {
                            // If sign in fails, display a message to the user.
                            mProgressBar.setVisibility(View.INVISIBLE);
                            mTextView.setText(task.getException().getMessage());

                        }
                        // ...
                    }
                });
    }

    private void fetchInstanceRegistrationToken() {
        FirebaseInstanceId.getInstance().getInstanceId()
                .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
                    @Override
                    public void onComplete(@NonNull Task<InstanceIdResult> task) {
                        if(task.isSuccessful()){

                            // send token to app server
                            instanceToken = task.getResult().getToken();
                            mTextView.setText(instanceToken);
                            if(!TextUtils.isEmpty(instanceToken)){
                                final FirebaseAuth mAuth = FirebaseAuth.getInstance();
                                FirebaseUser user = mAuth.getCurrentUser();
                                sendTokenToAppServer(user.getUid(), instanceToken);
                            }
                            mPushButton.setVisibility(View.VISIBLE);
                        }else{
                            mProgressBar.setVisibility(View.INVISIBLE);
                            // token  failed
                            mTextView.setText(task.getException().getMessage());
                        }
                    }
                });
    }

    private void sendTokenToAppServer(String uid, String token) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .build();
        Retrofit.Builder builder =
                new Retrofit.Builder()
                        .client(okHttpClient)
                        .baseUrl(FIREBASE_CLOUD_FUNCTIONS_URL)
                        .addConverterFactory(GsonConverterFactory.create());
        Retrofit retrofit = builder.build();
        RetrofitService service= retrofit.create(RetrofitService.class);
        service.sendTokenToServer(uid, token).enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {
                mProgressBar.setVisibility(View.INVISIBLE);
            }

            @Override
            public void onFailure(Call<String> call, Throwable t) {
                mProgressBar.setVisibility(View.INVISIBLE);
            }
        });
    }

    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {

    }
}

Custom Message Handling

If you want to handle the notification in the app then you can extend the FirebaseMessagingService class.

package com.codeexa.com.fcmexample;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.View;

import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onNewToken(String token) {
        //Get updated InstanceID token.
        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // Instance ID token to your app server.
        final FirebaseAuth mAuth = FirebaseAuth.getInstance();
        FirebaseUser user = mAuth.getCurrentUser();
        if(user!=null)
            updateTokenToServer(user.getUid(), token);
    }

    private void updateTokenToServer(String uid, String token) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .build();
        Retrofit.Builder builder =
                new Retrofit.Builder()
                        .client(okHttpClient)
                        .baseUrl("_YOUR_CLOUD_URL_")
                        .addConverterFactory(GsonConverterFactory.create());
        Retrofit retrofit = builder.build();
        RetrofitService service= retrofit.create(RetrofitService.class);
        service.sendTokenToServer(uid, token).enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {

            }
            @Override
            public void onFailure(Call<String> call, Throwable t) {

            }
        });
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        String title = remoteMessage.getNotification().getTitle();
        String body = remoteMessage.getNotification().getBody();
        sendNotification(this, title, body);
    }

    public static void sendNotification(Context mcontext,String messageTitle, String messageBody) {
        Intent intent = new Intent(mcontext, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(mcontext, 0 /* Request code */, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        NotificationManager notificationManager = (NotificationManager) mcontext.getSystemService(Context.NOTIFICATION_SERVICE);

        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel notificationChannel = new NotificationChannel(mcontext.getString(R.string.default_notification_channel_id),
                    messageTitle , NotificationManager.IMPORTANCE_DEFAULT);

            // Configure the notification channel.
            notificationChannel.setDescription("Channel description");
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.GREEN);
            notificationChannel.setVibrationPattern(new long[]{0, 500, 200, 500});
            notificationChannel.enableVibration(true);
            notificationManager.createNotificationChannel(notificationChannel);
        }

        android.support.v4.app.NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mcontext, mcontext.getString(R.string.default_notification_channel_id))
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(mcontext.getString(R.string.app_name))
                .setContentTitle(messageTitle)
                .setContentText(messageBody)
                .setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);


        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }
}

Add service class in AndroidManifest.xml to handle new token and messages received.

      <service android:name=".MyFirebaseMessagingService" >
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

Firebase Message Sender

We have created our Android application to receive FCM push notifications. Let’s check now how we can use Firebase Cloud Functions to send FCM message. Firebase Admin SDK provides messaging API to send message to group of tokens.

I have explained in my earlier post how to create Firebase Cloud Functions. Follow my guide to create your instance of Firebase function and deploy.

I will show you a basic example of how to use Firebase Cloud functions to send messages to a particular device.

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


admin.initializeApp();

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.registerToken = functions.https.onRequest((request, response) => {
    var qParams = request.body;
    console.log(qParams);
    response.send("Hello from Firebase!");
});

exports.sendPushNotification = functions.https.onRequest((req, res)=>{
    var messaging = admin.messaging();
    var qParams = req.query;
    var token = qParams.token;
    console.error("request processing:" + token);
    var payload = {
        notification: {
            title: 'New Message',
            body: "A new version of the app is available. Download now!"
        }
    };
    var options = {
        priority: "high",
        timeToLive: 60 * 60 * 24
    };

    return messaging.sendToDevice(token, payload, options)
        .then(()=>{
            console.error("Message sent");
            res.send("Notification sent");
        }).catch(error => {
            console.error("Error in new offer request processing:" + JSON.stringify(error));
            res.send("Error in sending push");
        });
});

In the above code sendPushNotificationfunction receives FCM token from the Android client and is used to create the message.

 

Leave a Reply