DEV Community

senanur incekara
senanur incekara

Posted on

How to receive Firebase notifications in the background in Android?

I am developing an Android application that uses Firebase Cloud Messaging (FCM) to send notifications for incoming calls. I want to ensure that notifications are received even when the app is in the background. I have noticed that notifications only appear when the app is in the foreground. They do not show up when the app is running in the background

when android in backgorund . Logcat :

win=Window{f06d548 u0 com.example.tr/com.example.tr.MainActivity} destroySurfaces: appStopped=true cleanupOnResume=false win.mWindowRemovalAllowed=false win.mRemoveOnExit=false win.mViewVisibility=8 caller=com.android.server.wm.ActivityRecord.destroySurfaces:6862 com.android.server.wm.ActivityRecord.destroySurfaces:6843 com.android.server.wm.ActivityRecord.activityStopped:7528 com.android.server.wm.ActivityClientController.activityStopped:310 android.app.IActivityClientController$Stub.onTransact:702 com.android.server.wm.ActivityClientController.onTransact:175 android.os.Binder.execTransactInternal:1380
Enter fullscreen mode Exit fullscreen mode
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <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"
        android:usesCleartextTraffic="true">

        <!-- Firebase Messaging Service -->
        <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

         <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="incoming_calls" />


            <activity android:name=".IncomingCallActivity"></activity>




        <!-- Main Activity -->
        <activity
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
            android:name=".MainActivity"
            android:label="@string/title_activity_main"
            android:theme="@style/AppTheme.NoActionBarLaunch"
            android:launchMode="singleTask"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- File Provider for handling file URIs -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
            <meta-data
                android:name="com.google.firebase.messaging.default_notification_icon"
                android:resource="@mipmap/ic_launcher_round" />
            <meta-data
                android:name="com.google.firebase.messaging.default_notification_color"
                android:resource="@color/colorAccent" />
        </provider>

    </application>

    <!-- Permissions -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.VIBRATE" />

    <!-- Incoming Call Permissions Start -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <permission
        android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
        android:protectionLevel="signature" />
    <!-- Incoming Call Permissions End -->



</manifest>
Enter fullscreen mode Exit fullscreen mode

MyFirebaseMessagingService.java



package com.example.tr;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.content.Context;
import android.os.Vibrator;
import androidx.core.app.NotificationCompat;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private static final String TAG = "MyFirebaseMsgService";
    private static final int NOTIFICATION_ID = 0;
    private static final int MISSED_CALL_NOTIFICATION_ID = 1;

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        if (remoteMessage.getData().size() > 0) {
            sendNotification(remoteMessage);
        }

         if (remoteMessage.getNotification() != null) {
            sendNotification(remoteMessage);
        }

    }

    private void sendNotification(RemoteMessage remoteMessage) {
        String title = "INCOMING CALL ";
        String body = remoteMessage.getData().get("callerName") + "Sizi arıyor ";


        //-->
       Intent intent = new Intent(this, IncomingCallActivity.class);

        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra("callerName", remoteMessage.getData().get("callerName"));
        intent.putExtra("callId", remoteMessage.getData().get("callId"));

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        Intent acceptIntent = new Intent(this, IncomingCallActivity.class);

        acceptIntent.putExtra("callAction", "accept");
        acceptIntent.putExtra("callerName", remoteMessage.getData().get("callerName"));
        acceptIntent.putExtra("callId", remoteMessage.getData().get("callId"));

        Intent declineIntent = new Intent(this, IncomingCallActivity.class);
        declineIntent.putExtra("callAction", "decline");
        declineIntent.putExtra("callerName", remoteMessage.getData().get("callerName"));
        declineIntent.putExtra("callId", remoteMessage.getData().get("callId"));

       PendingIntent acceptPendingIntent = PendingIntent.getActivity(this, 1, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
       PendingIntent declinePendingIntent = PendingIntent.getActivity(this, 2, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "incoming_calls")
                .setContentTitle(title)
                .setContentText(body)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setAutoCancel(true)
                .addAction(R.drawable.ic_accept, "Answer", acceptPendingIntent)
                .addAction(R.drawable.ic_decline, "Decline", declinePendingIntent)
                .setContentIntent(pendingIntent)
                .setPriority(NotificationCompat.PRIORITY_MAX)
                .setCategory(NotificationCompat.CATEGORY_CALL)
                .setVibrate(new long[]{0, 1000, 800})
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setFullScreenIntent(pendingIntent, true);

        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("incoming_calls", "Incoming Calls", NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(channel);
        }

        notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());

        Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        if (vibrator != null) {
            vibrator.vibrate(new long[]{0, 1000, 800}, 0);
        }

        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                notificationManager.cancel(NOTIFICATION_ID);
                sendMissedCallNotification(remoteMessage.getData().get("callerName"));

                if (vibrator != null) {
                    vibrator.cancel();
                }
            }
        }, 3000);
    }

    private void sendMissedCallNotification(String callerName) {
        String title = "Missed Call";
        String body = "You missed a call from " + callerName;

        NotificationCompat.Builder missedCallBuilder = new NotificationCompat.Builder(this, "missed_calls")
                .setContentTitle(title)
                .setContentText(body)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setAutoCancel(true)
                .setPriority(NotificationCompat.PRIORITY_HIGH);

        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("missed_calls", "Missed Calls", NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(channel);
        }

        notificationManager.notify(MISSED_CALL_NOTIFICATION_ID, missedCallBuilder.build());
    }
}
Enter fullscreen mode Exit fullscreen mode


package com.example.tr;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
import android.os.Bundle; 
import com.getcapacitor.BridgeActivity;
import com.google.firebase.FirebaseApp; 

public class MainActivity extends BridgeActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    FirebaseApp.initializeApp(this);

    createNotificationChannel();
  }

  private void createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      NotificationChannel channel = new NotificationChannel(
        "incoming_calls", // Channel ID
        "Incoming Calls",  // Channel name
        NotificationManager.IMPORTANCE_HIGH 
      );
      channel.setDescription("Channel for incoming call notifications");
      NotificationManager manager = getSystemService(NotificationManager.class);
      if (manager != null) {
        manager.createNotificationChannel(channel);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
IncomingCallActivity.java


package com.example.tr;

import android.widget.Button;
import android.widget.TextView;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Vibrator;
import android.util.Log;

public class IncomingCallActivity extends AppCompatActivity {

    private Vibrator vibrator;

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

        Intent intent = getIntent();
        String callerName = intent.getStringExtra("callerName");
        String callId = intent.getStringExtra("callId");
        String callAction = intent.getStringExtra("callAction");

        TextView callerNameTextView = findViewById(R.id.caller_name);
        callerNameTextView.setText(callerName);

        vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

        Button acceptButton = findViewById(R.id.answer_button);
        acceptButton.setOnClickListener(v -> {
            Log.d("IncomingCallActivity", "Kabul butonuna tıklandı");
            // finish();
            if (vibrator != null) {
                vibrator.cancel();
            }
        });

        Button declineButton = findViewById(R.id.decline_button);
        declineButton.setOnClickListener(v -> {
            Log.d("IncomingCallActivity", "Reddet butonuna tıklandı");
            // finish();
            if (vibrator != null) {
                vibrator.cancel();
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
api.service.ts:

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  static sendNotification(deviceToken: string) {
    throw new Error("Method not implemented.");
  }
  private apiUrl = '';
  private accessToken =""
      constructor(private http: HttpClient) {}


  sendNotification(token: string ): Observable<any> {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.accessToken}`
    });

    console.log("send push noti token ->",token)
    const notificationData = {
      token: token, 
      // notification: {
      //   title: 'Incoming Calls',
      //   body: 'aramanız varr',
      // },
      data: {
        title: 'Incoming Call',
        body: 'Cevapsız aramanız var ',
        callType: 'incoming',
        callerName: 'John Doe',
        callId: '12345',
        priority: 'high'
      },
      android: {
        priority: 'high',
        notification: {
          channel_id: 'incoming_calls',
          sound: 'default',
          // full_screen_intent: true,  
        },
      },
    };


    const body = {
      message: notificationData
    };

    return this.http.post<any>(this.apiUrl, body, { headers });
  }


}```




`Home.page.ts`

``
import { Component } from "@angular/core";
import {
  FirebaseMessaging,
  GetTokenOptions,
} from "@capacitor-firebase/messaging";
import { Capacitor } from "@capacitor/core";
import { IonicModule } from "@ionic/angular";
import { environment } from "src/environments/environment";

import { NotificationService } from '../services/notification.service';
import { ApiService } from "../api/api.service";

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"],
  standalone: true,
  imports: [IonicModule],
})
export class HomePage {
  public token = "";

  constructor(
    private notificationService: NotificationService,
    private apiService: ApiService  
  ) {

    FirebaseMessaging.addListener("notificationReceived", (event) => {
      console.log("notificationReceived: ", { event });
    });
    FirebaseMessaging.addListener("notificationActionPerformed", (event) => {
      console.log("notificationActionPerformed: ", { event });
    });
    if (Capacitor.getPlatform() === "web") {
      navigator.serviceWorker.addEventListener("message", (event: any) => {
        console.log("serviceWorker message: ", { event });
        const notification = new Notification(event.data.notification.title, {
          body: event.data.notification.body,
        });
        notification.onclick = (event) => {
          console.log("notification clicked: ", { event });
        };
      });
    }
  }

  public async requestPermissions(): Promise<void> {
    const result = await FirebaseMessaging.requestPermissions();
    if (result.receive === 'granted') {
      console.log('Push notifications permission granted');
    }
  }


  public async getToken(): Promise<void> {
    const options: GetTokenOptions = {
      vapidKey: environment.firebase.vapidKey,
    };
    if (Capacitor.getPlatform() === "web") {
      options.serviceWorkerRegistration =
        await navigator.serviceWorker.register("firebase-messaging-sw.js");
    }
    const { token } = await FirebaseMessaging.getToken(options);
    this.token = token;
  }



  sendPushNotification() {
    const deviceToken = "fIuEs9LlRwO6J06ahgWCtC:APA91bF5VnhsCYAyHxudm1de2s4sVYfUWkenKl8zvOxbjHa2_eOv5MlU2FNTogrPYu4bCJKKiNxM7LpQvc7w_iW59fKGoevE0OjYnYPrxHjj8x5T7uvqdW3_j3HXpTj3F_DrLYeBVpX4"
    console.log("Device token is here -> : ", deviceToken);

    if (!deviceToken) {
      console.error('No device token available. Unable to send notification.');
      return;
    }

    this.apiService.sendNotification(deviceToken).subscribe(
      (response: any) => {
        console.log('Push notification sent:', response);
      },
      (error: any) => {
        console.error('Error sending notification:', error);
        console.error('Full error response:', JSON.stringify(error.error));
      }
    );
  }


}
```
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
cubiclesocial profile image
cubiclesocial

Welcome to your new world of pain, misery, and eternal suffering. Android and Firebase notifications are not fun to deal with. I've worked with every iteration of Android push notifications (the OG C2DM, GCM, and FCM) plus services like AWS SNS. They were/are all awful-by-design to work with. C2DM was the worst of the lot. FCM is only marginally better in that it is similar to AWS SNS but happens to be free (for now). There's no unified standard, especially if you want to do push notifications on iOS as well. Firebase messaging for iOS exists but it's not particularly good, IMO. Push notification support for Android is subject to change on Google's whims, so you'll eventually reach a breaking point of "not caring" when a notification message doesn't deliver. Each time something breaks in the push notification universe, you have to dive back into the swamp to get it working again and then go to upload an APK/App Bundle to Google Play but have to first fill out 15 new forms before Google Play will even let you upload. In short, if it involves push notifications, whatever it is, it isn't worth the implementation time. That's my decade plus of experience in a nutshell. A good general rule of thumb is to avoid FCM like the plague until you absolutely have to work with it.

With that out of the way...

Looking over your code, I don't see anything blatantly obvious that you are missing. Some quick Google searching turns up a few posts related to metadata in the manifest XML, sending vs. not sending 'notification' to FCM, you're not sending network requests inside the notification handler, and you say you are getting foreground notifications. Based on the commented out code bits, it looks like you've already tried a few things (i.e. already ran into the same SO posts I've read). There's probably some obscure part of the FCM docs that you've overlooked that is relevant to your very specific use-case.

Your target API level affects a LOT of things, including permissions and how backgrounding works in Android for your app. Try to target as low an API level as allowed by Google Play and pull in the oldest version of Firebase that you can get your hands on that Gradle is happy with. Don't worry about things if Android Studio is "unhappy" as long as Gradle compiles fine. On Android, background is also not the same thing as sleep (i.e. the device is turned off). Sleep is subject to massive time delays in notification delivery to preserve battery. Since you are doing something related to calling permissions, there are even more restrictions in place. You should double-check each of your permissions in the XML manifest against the documentation. Sometimes you have to specifically ask the user for a permission even if your manifest says your app requires a permission. And users have the ability to deny permissions under Settings for any given app, so you can't just assume your app can call an API that requires any given permission without checking first.

Some additional thoughts/tips: Android likes to cache old Manifests/XML files. Even if you have modified your manifest, compiled, and pushed the new APK/App Bundle to the device/emulator, your app might actually load an older version of the manifest. You have to force stop the app and delete cache data from the Settings and occasionally just nuke the app off the device/emulator altogether when you are monkeying around with the manifest and other XML bits. And sometimes it happens on the Gradle end of things where you have to manually delete the entire build cache directory. Don't get me started on how awful-by-design Gradle/Ant/Maven is because we'll be here for a while. Make sure that your project environment is excluded from system malware scanners which do NOT play nice with the Gradle build system. If you are on a company-issued laptop/PC with something like CrowdStrike onboard, well, I feel sorry for you because you can't exclude your project directory and you'll just have to suffer with 5+ minute build times and Android Studio "helpfully" notifying you of your misery.

Dev.to isn't exactly the best place to get help with complex technical problems. It's more of a social networking and cheerleading site. I don't like StackOverflow as much as the next dev, but that's the best place to get help in this case. I doubt there are enough hardcore Android devs floating around here to answer your question.