In the previous article, we created an android project, connected it to firebase, enabled different navigation paths depending on login status and sent a notification from within the app. If you haven’t, please go through it here. In this article, we are going to purely work with notifications sent through the backend to firebase to our application. This is known as downstream messaging. We already set all the groundwork ready, and here we will be working only on the android app.

From where we left off if you click create user you should get a user object sent back from the backend (clear the db so that we start on a clean slate) and the user + token should be saved in shared preferences. When you click the snackbar, you should be directed to the Main Activity which at the moment does not do much, let us change that.

Messaging

Main activity (only for navigation)

The main activity acts as a router for the last two activities in this project - Messaging and Weather. Create these two activities based on the empty activity template and move back to the main activity for edits.

Here are the contents for the MainActivity java file:

public class MainActivity extends BaseActivity implements View.OnClickListener {

    Button navigateToMessaging;
    Button navigateToWeather;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        navigateToMessaging = findViewById(R.id.btnMessaging);
        navigateToWeather = findViewById(R.id.btnWeather);

        navigateToMessaging.setOnClickListener(this);
        navigateToWeather.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnMessaging:
                navigate("messaging");
                break;
            case R.id.btnWeather:
                navigate("weather");
                break;
        }
    }

    private void navigate(String screen) {
        Intent intent = null;
        if (screen.equalsIgnoreCase("messaging")) {
            intent = new Intent(MainActivity.this, MessagingActivity.class);
        } else if (screen.equalsIgnoreCase("weather")) {
            intent = new Intent(MainActivity.this, WeatherActivity.class);
        }

        startActivity(intent);
    }
    }

This activity is self explanatory with the button clicks navigating the user to the respective screens. However, there are changes in the base activity. Here are the additions:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.settings:
                Intent intent = new Intent(BaseActivity.this, SettingsActivity.class);
                startActivity(intent);
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

Create a settings activity based on the empty template for this piece of code to work. Also, add menu.xml file under the res > menu folder with the following:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/settings"
        android:title="@string/settings" />
</menu>

We will be handling the logic for the settings later. With this, we can move to the main_activity.xml file:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include layout="@layout/toolbar"/>

    <Button
        android:id="@+id/btnMessaging"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginLeft="32dp"
        android:layout_marginEnd="32dp"
        android:layout_marginRight="32dp"
        android:layout_marginBottom="120dp"
        android:text="@string/messaging"
        app:layout_constraintBottom_toTopOf="@+id/btnWeather"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btnWeather"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginLeft="32dp"
        android:layout_marginEnd="32dp"
        android:layout_marginRight="32dp"
        android:text="@string/weather"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.50" />

</androidx.constraintlayout.widget.ConstraintLayout>


This is a constraint layout with 2 buttons for both actions. We include a layout called toolbar (toolbar.xml), create it with the following content:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>


</LinearLayout>

This is just an app bar layout with a custom toolbar that we use for displaying the settings menu item later.

With this we should have the main activity navigating to both the messaging and weather activities successfully. Run the code to make sure everything works well.

A good practice would be to navigate with a navigation drawer, but that is not in the scope of this tutorial.

Messaging

Modify the messaging activity to have the following content:

public class MessagingActivity extends BaseActivity {

    TextView txtMessageSender;
    TextView txtMessageBody;
    EditText txtRecipientMessage;
    EditText txtRecipientName;
    Button btnSendMessage;
    RetrofitClient client;
    CoordinatorLayout coordinatorLayout;
    private boolean loadData = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messaging);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            String loadDataValue = bundle.getString("load_data");
            if (!TextUtils.isEmpty(loadDataValue)) {
                loadData = Boolean.parseBoolean(loadDataValue);
            }
        }

        if (loadData) {
            Toast.makeText(this, "Load data about the message from the backend here", Toast.LENGTH_LONG).show();
        }

        txtMessageSender = findViewById(R.id.txtMessageSender);
        txtMessageBody = findViewById(R.id.txtMessageBody);
        txtRecipientName = findViewById(R.id.txtRecipientName);
        txtRecipientMessage = findViewById(R.id.txtRecipientMessage);
        btnSendMessage = findViewById(R.id.btnSendMessage);
        coordinatorLayout = findViewById(R.id.coordinator);
        client = getClient();

        txtRecipientName.requestFocus();

        btnSendMessage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String userName = getSharedPrefs().getString(Constants.USER_NAME, null);
                if (userName == null) {
                    Toast.makeText(MessagingActivity.this, "The token is blank", Toast.LENGTH_LONG).show();
                    return;
                }

                String recipientName = txtRecipientName.getText().toString().trim();
                String recipientMessage = txtRecipientMessage.getText().toString().trim();

                if (TextUtils.isEmpty(recipientName) || TextUtils.isEmpty(recipientMessage)) {
                    Toast.makeText(MessagingActivity.this, "Please fill in both fields", Toast.LENGTH_SHORT).show();
                    return;
                }

                Notification notification = new Notification();
                notification.setSenderUserName(userName);
                notification.setRecipientUserName(recipientName);
                notification.setMessage(recipientMessage);

                client.getApi().sendMessage(notification).enqueue(new Callback<Notification>() {
                    @Override
                    public void onResponse(Call<Notification> call, Response<Notification> response) {
                        if (response.isSuccessful()) {
                            Notification notification1 = response.body();
                            if (notification1.isSuccess()) {
                                Snackbar snackbar = Snackbar.make(coordinatorLayout, notification1.getMessage(), BaseTransientBottomBar.LENGTH_LONG);
                                snackbar.show();
                            } else {
                                Toast.makeText(MessagingActivity.this, notification1.getMessage(), Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            try {
                                Toast.makeText(MessagingActivity.this, response.errorBody().string(), Toast.LENGTH_SHORT).show();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    @Override
                    public void onFailure(Call<Notification> call, Throwable t) {
                        Toast.makeText(MessagingActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });

    }
}

Let us go through this code.

  • We first check if the actionbar is null, if not we pass true to setDisplayHomeAsUpEnabled() this enables back navigation to the main activity
  • We then check on a value called load_data from the bundle. If you followed along from part one (the spring project), then you have this object passed into the data map in the backend. We will write code to get it to this activity shortly
  • We show a toast if the value is true from the bundle. More explanation on this below
  • We then set the ‘hooks’ for all elements on screen
  • Finally on button btnSendMessage on click, we handle the logic for this app. We get the sender (current user) name from the shared preferences, validate it. We also get the recipient name and message from the user inputs on screen. We build a new notification object and pass in all the needed parameters and we send this to the backend. The backend handles routing the message to the correct user with the username provided.

We are done with the messaging service but are not ready to test this yet. Remember we created our own service for handling notifications but we did not write any code for this yet. Add/edit the following code to the messaging service - MyMessagingService.java:

    @Override
    public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
        Log.d("messagingService", "Message received");

        sendNotification(remoteMessage);
    }

    private void sendNotification(RemoteMessage remoteMessage) {

        Intent intent = new Intent(this, MainActivity.class);
        Map<String, String> map = remoteMessage.getData();
        String screeName = map.get("screen");
        if (screeName != null) {
            if (screeName.equalsIgnoreCase("messages")) {
                intent = new Intent(this, MessagingActivity.class);
            }
        }
        intent.putExtra("load_data", "true");
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.notification_icon)
                .setContentTitle(remoteMessage.getNotification().getTitle())
                .setContentText(remoteMessage.getNotification().getBody())
                .setPriority(NotificationCompat.PRIORITY_DEFAULT) // set more content than the one liner
                .setContentIntent(pendingIntent)
                .setShowWhen(true) // hide timestamp
                .setAutoCancel(true); // dismiss when tapped
        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
        notificationManager.notify(0, builder.build());
    }

This is what this code does:

  • onMessageReceived - this is called automatically when the app is in the foreground. We call the method sendNotification with the remoteMessage object passed in from firebase. This message contains all the data we set from the backend in the respective apis
  • sendNotification - in this message we create a new intent and from the data object in remoteMessage, we get the value of the item screen. The screen is passed from the backend. The logic here is that if you have multiple kinds of notifications, you can use this screen value to load the appropriate activities. For us, we only have the messaging one. After handling the intent, we create a notification and pass the intent as a pendingIntent to the contentIntent. This means when the user taps the notification, they will be navigated to the correct screen.

With this, we can finally test notifications/messages from the server. Here you require 2 devices, one for sending the messages (preferably an emulator) and one for receiving the messages (preferably a physical device). Clear the database on the backend, and create accounts on both devices, note down the username assigned from each device and send messages from both sides. This is the power of firebase cloud messaging!!

Topic subscription

In the previous section we sent targeted messages to users using their username/token combination but what if we wanted to send a notification to all devices in a certain category without knowing their username/tokens beforehand? That’s where topics come in. In the backend we already handle saving tokens on our database (needed for our logic and not a requirement for fcm) and sending messages to this topic. Let us create the screens/logic for this.

We already created a weather screen, update the contents of the activity_weather.xml file to this:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:id="@+id/coordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".WeatherActivity">

    <include layout="@layout/toolbar"/>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <CheckBox
            android:id="@+id/checkBoxSubscribe"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:text="Subscribe"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

This is a simple layout with the toolbar and a checkbox.

For the WeatherActivity.java file update the contents to match this:

public class WeatherActivity extends BaseActivity {

    CheckBox subscribeCheckBox;
    RetrofitClient client;
    CoordinatorLayout coordinatorLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        subscribeCheckBox = findViewById(R.id.checkBoxSubscribe);
        coordinatorLayout = findViewById(R.id.coordinator);
        client = getClient();

        boolean subscribed = getSharedPrefs().getBoolean(Constants.SUBSCRIBED, false);
        subscribeCheckBox.setChecked(subscribed);

        subscribeCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                handleSubscription(isChecked);
            }
        });
    }

    private void handleSubscription(boolean subscribe) {
        String userName = getSharedPrefs().getString(Constants.USER_NAME, null);
        if (userName == null) {
            Toast.makeText(this, "User name is null", Toast.LENGTH_SHORT).show();
            return;
        }

        Subscription subscription = new Subscription();
        subscription.setSubscribe(subscribe);
        subscription.setUserName(userName);
        SharedPreferences.Editor editor = getSharedPrefsEditor();

        client.getApi().subscribeToWeather(subscription).enqueue(new Callback<Subscription>() {
            @Override
            public void onResponse(Call<Subscription> call, Response<Subscription> response) {
                if (response.isSuccessful()) {
                    Subscription sub = response.body();
                    if (sub.isSuccess()) {
                        Snackbar snackbar = null;
                        if (subscribe) {
                            snackbar = Snackbar.make(coordinatorLayout, "You have subscribed successfully", BaseTransientBottomBar.LENGTH_LONG);
                        } else {
                            snackbar = Snackbar.make(coordinatorLayout, "You have un-subscribed successfully", BaseTransientBottomBar.LENGTH_LONG);
                        }
                        editor.putBoolean(Constants.SUBSCRIBED, subscribe);
                        editor.apply();

                        snackbar.show();
                    } else {
                        Toast.makeText(WeatherActivity.this, sub.getErrorMessage(), Toast.LENGTH_SHORT).show();
                    }
                } else {
                    try {
                        Toast.makeText(WeatherActivity.this, response.errorBody().string(), Toast.LENGTH_SHORT).show();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

            @Override
            public void onFailure(Call<Subscription> call, Throwable t) {
                Toast.makeText(WeatherActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }


}

We setup the hooks as we did before and on the checkboxonchange listener, we add our logic. We get the username from sharedpreferences and validate it. We then create a subscription model and pass in the boolean of the checkbox to it as well as passing in the username. We then send this to the backend for subscription. If you remember, if the checkbox is checked (meaning the user wants to subscribe),  the backend takes the user and saves them to a weather table, then the user token is sent to fcm with the topic name for subscription. If they uncheck the box, we send an unsubscription request to fcm with the token and topic.

Run the code at this point and everything should work successfully. To test out sending a message to a topic subscribe on one of the two devices then go to firebase console and open your project (the project we have been using for spring and android). Navigate to the following screen:

Here you are able to send messages either to a device or a topic. Set a notification title and text and hit next. Under target, choose topic and enter 'weather' as this is the topic we are working with from spring. Under scheduling choose send now. Hit review and publish and you should see the notification on the device that was subscribed to the topic.

Muting Notifications (bonus)

We can take this walkthrough further and handle muting notifications for a user. A user can turn off notifications (only push from the server and not local) and turn them back on again. We do this with the settings menu and the official way of handling user settings in an android app according to google.

Create a new preference xml file with the following parameters set. Add a filename that you will use to reference these settings.

Replace the contents of the file you create with this:

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
    <PreferenceCategory app:title="@string/sync_header">

        <SwitchPreferenceCompat
            app:key="sync"
            app:defaultValue="true"
            app:title="@string/sync_title" />

    </PreferenceCategory>

</PreferenceScreen>

I will not go into more details about what is happening here. You can get more information on settings in android here . We are just adding a switch to the screen with a key of sync and a default value of true.

Move to the settings xml file (the one attached to the settings activity) created earlier and edit the contents to match this:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/toolbar"/>

    <FrameLayout
        android:id="@+id/settings"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="60dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

We include the toolbar created earlier and we add a frame layout. The frame layout is needed to display the settings. For the settings activity, edit the contents to match this:

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.settings, new SettingsFragment())
                .commit();
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
    }

    public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey);
            getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
        }


        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if (sharedPreferences.contains(key)) {
                updateSetting(sharedPreferences.getBoolean(key, false));
            }
        }

        private void updateSetting(boolean value) {
            RetrofitClient client = RetrofitClient.getInstance();
            String userName = getContext()
                    .getApplicationContext().getSharedPreferences(Constants.SHARED_PREFS_NAME, 0).getString(Constants.USER_NAME, null);
            User user = new User();
            user.setNotifications(value);
            user.setUserName(userName);
            client.getApi().handleNotifications(user).enqueue(new Callback<User>() {
                @Override
                public void onResponse(Call<User> call, Response<User> response) {
                    if (response.isSuccessful()) {
                        User user1 = response.body();
                        if (user1.isSuccess()) {
                            if (value) {
                                Toast.makeText(getContext(), "Notifications back on. Re-subscribe to any topics you want", Toast.LENGTH_SHORT).show();
                            } else {
                                SharedPreferences.Editor editor = getContext().getApplicationContext().getSharedPreferences(Constants.SHARED_PREFS_NAME, 0).edit();
                                editor.putBoolean(Constants.SUBSCRIBED, false);
                                editor.apply();
                                Toast.makeText(getContext(), "Notifications are off", Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            Toast.makeText(getContext(), user1.getErrorMessage(), Toast.LENGTH_SHORT).show();
                        }
                    } else {
                        Toast.makeText(getContext(), response.errorBody().toString(), Toast.LENGTH_SHORT).show();
                    }
                }

                @Override
                public void onFailure(Call<User> call, Throwable t) {
                    Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            onBackPressed();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Let us go through what is happening in this screen:

  • in the onCreate, we replace the frame layout with the settings fragment that we create within this same file. We also set the DisplayHomeAsUpEnabled as true to make sure we can navigate back to any activity that calls this menu
  • we create a SettingsFragment that extends PreferenceFragmentCompat and implements SharedPreferences.OnSharedPreferenceChangeListener. This listener is needed for use to take action when a preference changes in the settings
  • in the onCreate of the fragment, we set the xml file we created above to be the preferences and here we also set the listener
  • onSharedPreferenceChanged is the listener we use. Since this listener is called only when the user changes the switch above, we pick the new value and send this to the backend with the username. In the backend, we unsubscribe the user from the topic (weather), delete the user from the weather table and edit the user table to false in the notifications column. With this, the user will not receive any push notifications from the server.
  • onOptionsItemSelected we use this to navigate the user back without caring about the activity that called the settings activity.

The settings screen should look like this:

Push notification settings
Push notification settings

On any of the devices, test out by turning off the setting and trying to send a direct message from the other devices. If this setting is off, a notification will not be received. If it is on, a setting will come as expected.

Android 8+ channels

If you have been testing this code on a device running android 8+ you will see that no notification has been received in all this testing. This is because from android 8, a notification has to be part of a channel. A channel is basically a category of related notifications. From all the code we have been writing, we have actually been including the channel_id string when creating a notification and the only thing missing is actually creating and registering this channel with the android OS.

We can do this in the login screen as the user journey has to pass through this screen. Add the following code to the LoginActivity:

private void createNotificationChannel() {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = getString(R.string.channel_name);
            String description = getString(R.string.channel_description);
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }

This method checks if the build version is greater or equal to android O (8). We then build a channel with the id, name, importance and description then finally using the notification manager we create the channel. Remember to call this method from within the onCreate method. Creating a channel is idempotent meaning a channel is created only once, no matter how many times you call the method.

With this new update, you should be able to receive and view notification in android 8+. The following screenshots show the channel name in the notifications settings for the app and also the options a user has for the channel. PS: we created a channel name - Just a channel.

Channel under the categories of notifications
Channel under the categories of notifications

Conclusion

With this we come to the end of this set of push notification walkthroughs. We created the spring project for handling and routing notifications through fcm to the app and we created a simple (maybe not practical) application that handles different scenarios for push notifications and local notifications.

The GitHub repo for this project can be found here,

Links to the other projects are:
Part 1 - Spring boot
Part 2 - Android

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.