Test Firebase Cloud Messaging in Android Using ADB

Firebase Cloud Messaging (FCM) is the most popular choice for sending push notifications to Android devices. Unfortunately, there is no official way to test the integration of FCM into your app “locally”. Therefore, if you’ll want to test pushes behavior when developing your Android app, you’ll need to send an actual push message using FCM web console. That’s time-consuming, cumbersome and can lead to an accidental “test” push notifications sent in production.

In this article I’ll share a quick hack that will allow you to send test pushes to your app using ADB.

FirebaseInstanceIdReceiver

FCM library for Android uses a BroadcastReceiver called FirebaseInstanceIdReceiver to receive messages inside the application. That’s the declaration of this receiver in FCM library’s manifest:

<receiver
     android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
     android:exported="true"
     android:permission="com.google.android.c2dm.permission.SEND" >
     <intent-filter>
         <action android:name="com.google.android.c2dm.intent.RECEIVE" />
     </intent-filter>

     <meta-data
         android:name="com.google.android.gms.cloudmessaging.FINISHED_AFTER_HANDLED"
         android:value="true" />
 </receiver>

Note that this BroadcastReceiver is protected by com.google.android.c2dm.permission.SEND permission. Neither non-Google apps nor ADB can get this permission, so this receiver isn’t accessible “from outside” under normal circumstances.

Replace FirebaseInstanceIdReceiver Configuration

One of the build steps of an Android app is called “manifest-merging”. During this step, the manifests of the third-party libs are merged into your app’s manifest. The final result is a final merged manifest that goes into the APK (or AAB) archive. The declaration of FirebaseInstanceIdReceiver that you saw earlier is merged into the final manifest during this step.

Fortunately for us, Android provides a way to override the merged manifest declarations. So, we’ll take advantage of this capability and alter the declaration of FirebaseInstanceIdReceiver to open it to the entire world. To achieve that, add this to your app’s manifest:

<receiver
    android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
    android:exported="true"
    android:permission="@null"
    tools:replace="android:permission"/>

Note how I set the required permission to @null and declare that this permission should replace the one inherited from the FCM library. This will remove the original protection from this receiver, so anyone will be able to send broadcasts to it.

Important: you shouldn’t release your application with this hack because it’s a security hole. So, make sure that the above code doesn’t make it into your release build. [I wonder whether Google Play checks for this kind of vulnerabilities when you upload new artifacts?]

Use ADB to Send Broadcasts to FirebaseInstanceIdReceiver

Once FirebaseInstanceIdReceiver is stripped of its permission protection, this component will start accepting broadcasts from ADB. For example, this command will simulate a push message with title and body string extras (replace com.yourapp.android with your app’s application ID):

adb shell 'am broadcast -a com.google.android.c2dm.intent.RECEIVE -n com.yourapp.android/com.google.firebase.iid.FirebaseInstanceIdReceiver --es "gcm.n.e" "1" --es "gcm.n.title" "Test title" --es "gcm.n.body" "Test message"'

The full list of supported extras can be found in FCM library’s Constants.MessageNotificationKeys class. For your convenience, attaching its current version here:

  /**
   * Keys used by Google Play services in bundle representing a Remote Message, to describe a
   * Notification that should be rendered by the client.
   */
  public static final class MessageNotificationKeys {

    public static final String RESERVED_PREFIX = "gcm.";

    public static final String NOTIFICATION_PREFIX = RESERVED_PREFIX + "n.";

    // TODO(morepork) Remove this once the server is updated to only use the new prefix
    public static final String NOTIFICATION_PREFIX_OLD = RESERVED_PREFIX + "notification.";

    /** Parameter to "enable" the display notification */
    public static final String ENABLE_NOTIFICATION = NOTIFICATION_PREFIX + "e";

    /**
     * Parameter to disable Android Q's "proxying" feature. Notifications with this set will never
     * be proxied.
     */
    public static final String DO_NOT_PROXY = NOTIFICATION_PREFIX + "dnp";

    /**
     * Parameter to make this into a fake notification that is only used for enabling analytics for
     * a control group. No notification is shown, nor any service callbacks. notification nor enable
     * any service callbacks.
     */
    public static final String NO_UI = NOTIFICATION_PREFIX + "noui";

    public static final String TITLE = NOTIFICATION_PREFIX + "title";
    public static final String BODY = NOTIFICATION_PREFIX + "body";
    public static final String ICON = NOTIFICATION_PREFIX + "icon";
    public static final String IMAGE_URL = NOTIFICATION_PREFIX + "image";
    public static final String TAG = NOTIFICATION_PREFIX + "tag";
    public static final String COLOR = NOTIFICATION_PREFIX + "color";
    public static final String TICKER = NOTIFICATION_PREFIX + "ticker";
    public static final String LOCAL_ONLY = NOTIFICATION_PREFIX + "local_only";
    public static final String STICKY = NOTIFICATION_PREFIX + "sticky";
    public static final String NOTIFICATION_PRIORITY =
        NOTIFICATION_PREFIX + "notification_priority";
    public static final String DEFAULT_SOUND = NOTIFICATION_PREFIX + "default_sound";
    public static final String DEFAULT_VIBRATE_TIMINGS =
        NOTIFICATION_PREFIX + "default_vibrate_timings";
    public static final String DEFAULT_LIGHT_SETTINGS =
        NOTIFICATION_PREFIX + "default_light_settings";
    public static final String NOTIFICATION_COUNT = NOTIFICATION_PREFIX + "notification_count";
    public static final String VISIBILITY = NOTIFICATION_PREFIX + "visibility";
    public static final String VIBRATE_TIMINGS = NOTIFICATION_PREFIX + "vibrate_timings";
    public static final String LIGHT_SETTINGS = NOTIFICATION_PREFIX + "light_settings";
    public static final String EVENT_TIME = NOTIFICATION_PREFIX + "event_time";

    /**
     * KEY_SOUND_2: can be null, "default" or the NAME of the R.raw.NAME resource to play. This key
     * has been added in Urda. Before Urda we used "sound" = null / "default"
     */
    public static final String SOUND_2 = NOTIFICATION_PREFIX + "sound2";

    // TODO(dgiorgini): clean SOUND/SOUND_2. Remove old key and rename current one.

    // FOR THE SERVER:
    //  - if sound is not provided : don't send anything
    //  - if sound is provided : send "sound2" = provided-string
    //                           AND send "sound" = "default" for backward compatibility < Urda

    /** DEPRECATED: use SOUND_2. this is used for backward compatibility < Urda */
    public static final String SOUND = NOTIFICATION_PREFIX + "sound";

    public static final String CLICK_ACTION = NOTIFICATION_PREFIX + "click_action";

    /** Deep link into the app that will be opened on click */
    public static final String LINK = NOTIFICATION_PREFIX + "link";

    /** Android override for the deep link */
    public static final String LINK_ANDROID = NOTIFICATION_PREFIX + "link_android";

    /** Android notification channel id */
    public static final String CHANNEL = NOTIFICATION_PREFIX + "android_channel_id";

    /**
     * Activity Intent extra key that holds the analytics data (in the form of a bundle) attached to
     * a notification open event.
     */
    public static final String ANALYTICS_DATA = NOTIFICATION_PREFIX + "analytics_data";

    /**
     * For l10n of text parameters (e.g. title & body) a string resource can be specified instead of
     * a raw string. The name of that resource would be passed in the bundle under the key named:
     * <parameter> + suffix (e.g: _loc_key)
     */
    public static final String TEXT_RESOURCE_SUFFIX = "_loc_key";

    /**
     * For l10n of text parameters (e.g. title & body) a string containing the localization
     * parameters can be specified. This would be present in the bundle under the key named:
     * <parameter> + suffix (e.g: _loc_args)
     */
    public static final String TEXT_ARGS_SUFFIX = "_loc_args";

    // don't instantiate me.
    private MessageNotificationKeys() {}
  }

After you compose your test ADB command(s) and verify that it works, I recommend wrapping it in a shell script and committing to the repo. This way, you won’t need to repeat this task again and will be able to share the test command with your teammates. You’ll also have the history of the evolution of the command if you ever change it in the future.

Conclusion

That’s it, now you can test the FCM logic inside your app locally using ADB, without going through the FCM web console. This can spare you a lot of time, but don’t forget to test the end-to-end flow before the release.

Check out my premium

Android Development Courses

Leave a Comment