This tutorial will get you familiar with the fundamentals of setting up push notifications in your Android project using Firebase.
Firebase serves as a module between your server and the devices that will be receiving the push notifications that you create. Your server informs Firebase that a notification has to be sent. Then Firebase does the work behind the scenes to get the notification published.
In order to establish connection with Firebase, you need to create a project for your own app in the Firebase console
. You must set up your project in such a way that every time a user installs it, their device is registered in Firebase with a unique token. Although this may seem complex, the setup is actually simple.
Head to the Firebase console, log in and create a new Android project. You will be asked to enter your package name. You can find your package name at the top of pretty much every Java file in your project. You can also find it in your AndroidManifest.xml
file. (You can also add a SHA1 key for extra security measures, but that will not be required for this tutorial.)
Next, you will get a google-services.json
file, which contains information about your project signature. In case you missed the instructions while creating your app, you need to add this file to your app directory, which you can find when you open your file structure to Project.
After that you need to add the gms
dependency in your project-level build.gradle
file:
1buildscript {
2dependencies {
3// Add this line
4 classpath 'com.google.gms:google-services:3.0.0'
5 }
6}
Then, add the corresponding plugin in your app-level build.gradle
. You will also need to add a Firebase messaging dependency in this file.
1dependencies {
2compile 'com.google.firebase:firebase-messaging:10.2.1'
3}
4// Bottom of your file
5apply plugin: 'com.google.gms.google-services'
Note that the firebase-messaging
dependency must be the same version as other gms libraries. Otherwise there is a strong chance that your app will fail to register the device and obtain a Firebase token. There is an even greater chance that the application won't compile at all because it fails to find the Firebase classes that you're about to implement.
The next step is to create two services. One will handle the device registration process, and the other will handle receiving the actual notifications.
Go to your AndroidManifest.xml
file and add these service declarations under the application
tag:
1<service
2 android:name=".notifications.MyFirebaseMessagingService"
3 android:permission="com.google.android.c2dm.permission.SEND">
4 <intent-filter>
5 <action android:name="com.google.firebase.MESSAGING_EVENT" />
6 <action android:name="com.google.android.c2dm.intent.RECEIVE" />
7 </intent-filter>
8 </service>
9 <service android:name=".notifications.MyFirebaseInstanceIDService">
10 <intent-filter>
11 <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
12 </intent-filter>
13 </service>
Under the same tag you can also add metadata for default notification values, but it is not mandatory:
1<meta-data
2 android:name="com.google.firebase.messaging.default_notification_icon"
3 android:resource="@mipmap/ic_launcher" />
4 <meta-data
5 android:name="com.google.firebase.messaging.default_notification_color"
6 android:resource="@color/colorTransparent" />
7 <meta-data android:name="com.google.android.gms.version"
8 android:value="@integer/google_play_services_version" />
The last thing that you need to add to your manifest file is a RECEIVE permission:
1<uses-permission android:name="android.permission.INTERNET" />
2<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
Next, go ahead and create those two Java class services that you declared in the manifest in a new package called notifications
.
This is the implementation of the MyFirebaseInstanceIDService
class:
1import android.content.SharedPreferences;
2import android.preference.PreferenceManager;
3import android.util.Log;
4import com.google.firebase.iid.FirebaseInstanceId;
5import com.google.firebase.iid.FirebaseInstanceIdService;
6import co.centroida.notifications.Constants;
7public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
8private static final String TAG = "MyFirebaseIIDService";
9
10@Override
11public void onTokenRefresh() {
12 // Get updated InstanceID token.
13 String refreshedToken = FirebaseInstanceId.getInstance().getToken();
14 Log.d(TAG, "Refreshed token: " + refreshedToken);
15 // If you want to send messages to this application instance or
16 // manage this apps subscriptions on the server side, send the
17 // Instance ID token to your app server.
18 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
19 preferences.edit().putString(Constants.FIREBASE_TOKEN, refreshedToken).apply();
20 }
21}
The purpose of this service is very simple:
It obtains a Firebase Token
, thereby forging the connection between the device and Firebase. Through this token, you can send notifications to this specific device. When obtained, this token is saved in a shared preference for future use. Naturally, you would like to send it to your server at some point, say user registration, or even right away, so that the server can send this device notifications through Firebase.
Moving on to the more interesting class, namely MyFirebaseMessagingService
.
1public class MyFirebaseMessagingService extends FirebaseMessagingService {
2@Override
3public void onMessageReceived(RemoteMessage remoteMessage) {
4
5 }
6}
This service needs to extend FirebaseMessagingService. When the target device receives a notification, onMessageReceived
is called. In your hands, you already have the remoteMessage
object, which contains all the information that you received about the notification. Now let's create an actual notification with that information.
1@Override public void onMessageReceived(RemoteMessage remoteMessage) {
2
3 notificationManager =
4 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
5
6 //Setting up Notification channels for android O and above
7 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
8 setupChannels();
9 }
10 int notificationId = new Random().nextInt(60000);
11
12 Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
13 NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, ADMIN_CHANNEL_ID)
14 .setSmallIcon(R.drawable.ic_notification_small) //a resource for your custom small icon
15 .setContentTitle(remoteMessage.getData().get("title")) //the "title" value you sent in your notification
16 .setContentText(remoteMessage.getData().get("message")) //ditto
17 .setAutoCancel(true) //dismisses the notification on click
18 .setSound(defaultSoundUri);
19
20 NotificationManager notificationManager =
21 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
22
23 notificationManager.notify(notificationId /* ID of notification */, notificationBuilder.build());
24
25}
Here, to get unique notifications each time you receive a new message, for the sake of this example, we generate a random number and use it as notification ID. With this ID, you can do several things to your notifications. As such, you should probably group them if they are of the same kind, or update them. If you want to see each notification individually from the others, their IDs need to be different.
Every app that targets SDK 26 or above (Android O) must implement notification channels and add its notifications to at least one of them. I won't go into the specifics of how those work, as it is out of the scope of this article. Simply put, you separate your notifications into channels based on their function and importance level. Having more channels gives the users more control over what notifications they receive. You can read more about channels here. If you want the newest phones to receive any of your notifications, paste this method in your service.
1 @RequiresApi(api = Build.VERSION_CODES.O)
2 private void setupChannels(){
3 CharSequence adminChannelName = getString(R.string.notifications_admin_channel_name);
4 String adminChannelDescription = getString(R.string.notifications_admin_channel_description);
5
6 NotificationChannel adminChannel;
7 adminChannel = new NotificationChannel(ADMIN_CHANNEL_ID, adminChannelName, NotificationManager.IMPORTANCE_LOW);
8 adminChannel.setDescription(adminChannelDescription);
9 adminChannel.enableLights(true);
10 adminChannel.setLightColor(Color.RED);
11 adminChannel.enableVibration(true);
12 if (notificationManager != null) {
13 notificationManager.createNotificationChannel(adminChannel);
14 }
15 }
I've initialized a constant ADMIN_CHANNEL_ID
which is of type String
. I use that id variable to refer to my newly created channel. So every time I use NotificationCompat.Builder
to create a new notification, I initialize the builder object and pass in the id in the constructor, like so:
1NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, ADMIN_CHANNEL_ID) {...}
Also another thing that should be noted is that we use remoteMessage.getData()
to access the values of the received notification. Now, there are two types of Firebase notifications - data messages and notification messages.
Data messages are handled here in onMessageReceived
whether the app is in the foreground or background. However, notification messages are received only when the app is in foreground, making them kind of useless or at least rather boring, don't you think?
For an unified notifications system, we use messages that have a data-only payload.
Messages containing both notification and data payloads are treated as notification messages, so they won't be handled by MyFirebaseMessagingService
when the app is in the background!
If you haven't set up your server yet, you can still test your push notifications with a POST
http request directly to Firebase. You can use any app you find fit for that, we use the Google Postman plugin. You can download it from this link.
The endpoint you can use from firebase is this one: https://fcm.googleapis.com/fcm/send
This request requires an Authorization header, the value of which is the Server key of your application in Firebase, preceded by a key=
. You can find it in your Project settings in the Firebase console under the tab Cloud messaging.
This is what your POST
request should look like:
And this should be the body of your request:
1{
2 "to":
3 "d4_9RguWZNQ:APA91bH677zDilszjdf30-i12-0W-02314-0@0-123495-0-02-Something-rXEtW_wZNXa6K_-V96rEHPEysXSIfL",
4 "data": {
5 "title": "I'd tell you a chemistry joke",
6 "message": "but I know I wouldn't get a reaction",
7 "image-url":
8 "https://docs.centroida.co/wp-content/uploads/2017/05/notification.png"
9 }
10}
In the body of your request you need to specify the "to" field to send a notification to the targeted device. It's value is the aforementioned Firebase token that your device obtains in MyFirebaseInstanceIDService
. You can retrieve it from the log message or directly from the shared preferences. In the data payload, you can specify all kinds of key-value pairs that would suit your application needs.
Now that we can send and test notifications, we can make them fancier. First let's add a click functionality for the notification:
1 Intent notificationIntent;
2 if(StartActivity.isAppRunning){
3 notificationIntent = new Intent(this, ChildActivity.class);
4 }else{
5 notificationIntent = new Intent(this, StartActivity.class);
6 }
7 notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
8
9 final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
10 PendingIntent.FLAG_ONE_SHOT);
11
12 ...
13
14 Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
15 NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
16 .setSmallIcon(R.drawable.ic_notification_small)
17 .setContentTitle(remoteMessage.getData().get("title"))
18 .setContentText(remoteMessage.getData().get("message"))
19 .setAutoCancel(true)
20 .setSound(defaultSoundUri)
21 .setContentIntent(pendingIntent);
It is recommended that you handle users clicking the notification while the app is still running. In this case we use a static boolean isAppRunning
to determine whether the root activity (StartActivity
) is running.
1public class StartActivity extends AppCompatActivity {
2public static boolean isAppRunning;
3
4 @Override
5 protected void onCreate(Bundle savedInstanceState) {
6 super.onCreate(savedInstanceState);
7 setContentView(R.layout.activity_start);
8 }
9 @Override
10 protected void onDestroy() {
11 super.onDestroy();
12 isAppRunning = false;
13 }
14}
You should consider what you want your notification intent to do so that it handles both cases without breaking your app's navigation structure.
Pictures in notifications can be a great attention-grabber. This is how we can add an image to our push notification:
1Bitmap bitmap = getBitmapfromUrl(remoteMessage.getData().get("image-url")); //obtain the image
2Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
3 NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
4 .setLargeIcon(bitmap) //set it in the notification
5 .setSmallIcon(R.mipmap.ic_launcher)
6 .setContentTitle(remoteMessage.getData().get("title"))
7 .setContentText(remoteMessage.getData().get("message"))
8 .setAutoCancel(true)
9 .setSound(defaultSoundUri)
10 .setContentIntent(pendingIntent);
11....
12}
13//Simple method for image downloading
14public Bitmap getBitmapfromUrl(String imageUrl) {
15try {
16 URL url = new URL(imageUrl);
17 HttpURLConnection connection = (HttpURLConnection) url.openConnection();
18 connection.setDoInput(true);
19 connection.connect();
20 InputStream input = connection.getInputStream();
21 return BitmapFactory.decodeStream(input);
22
23 } catch (Exception e) {
24 e.printStackTrace();
25 return null;
26 }
27}
In this case we send an image URL in the notification payload for the app to download. Usually, such processes get executed on a separate thread. However, in this case, this class is a service, so once the code in onMessageReceived
executes, the service, which is a thread different from the main thread, gets destroyed and with it goes every thread that was created by the service. Hence, we can afford to make the image download synchronously. This shouldn't pose a threat to performance, as the service thread is not the main thread.
The NotificationCompat.Builder
supports a few different types of styles for notifications, including a player and ones with custom layouts:
1Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
2NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
3 .setLargeIcon(bitmap)
4 .setSmallIcon(R.drawable.ic_notification_small)
5 .setContentTitle(remoteMessage.getData().get("title"))
6 .setStyle(new NotificationCompat.BigPictureStyle()
7 .setSummaryText(remoteMessage.getData().get("message"))
8 .bigPicture(bitmap))
9 .setContentText(remoteMessage.getData().get("message"))
10 .setAutoCancel(true)
11 .setSound(defaultSoundUri)
12 .setContentIntent(pendingIntent);
That's all you need to get started with push notifications in Android!
Here is the link to the github repo of the working example project from this tutorial: https://github.com/DimitarStoyanoff/Notifications
Now you can have some fun exploring and styling your notifications. I hope this tutorial helped you out and you found it enjoyable. Until next time!