Navigating through Firebase In-App Messaging Limitations

Why am i writing this….

This post is going to depart a little from my usual posts on game development. I’ve decided to share my experience with using Firebase’s In-App Messaging.
For those of you who are not aware of what this is, it’s basically a cloud messaging feature that should be trivial to integrate in Android and iOS platforms. It also comes with it’s own default UI implementation and basically a plug-and-play kind of thing.
If you want to know more, here’s the link official documentation on it.

In the game I’m currently working on, we’ve decided to keep users engaged by feeding them relevant news about the events and happening around the company and game. Firebase’s In-App Messaging looked the best fit for it and for most part, it definitely is.
We decided to implement a custom in-game UI instead of UI provided provided by the game by intercepting the message. That’s where we ran into a couple of dark corners that were enlightened when surfing through the API’s open-source code.
So, let’s get into it and explore Firebase In-App Messaging Limitations.

This post applies to Firebase In-App Messaing v20.4.2.
I will first write about the Android version and take some time between to add iOS version.

Test on Device….. How to make it reliable

When running “Test on Device”, there’s something documentation doesn’t mention. There’s a hidden counter in Java code that locks you out of receiving any test messages once you exceed it.
The code below is taken from github Repo (InAppMessageStreamManager.java) and checks whether it should ignore cache by checking if your device is in Test Mode.
If it’s a test device, it force fetches from server. That is how the “Test on Device” is working currently on Android.

//InAppMessageStreamManage.java 
private boolean shouldIgnoreCache(String event) {
    if (testDeviceHelper.isAppInstallFresh()) {
      return isAppForegroundEvent(event);
    }
    return testDeviceHelper.isDeviceInTestMode();
}


//TestDeviceHelper.java
  public void processCampaignFetch(FetchEligibleCampaignsResponse response) {
    // We only care about this logic if we are not already a test device.
    if (!isTestDevice) {
      updateFreshInstallStatus();
      List<CampaignProto.ThickContent> messages = response.getMessagesList();
      for (CampaignProto.ThickContent message : messages) {
        if (message.getIsTestCampaign()) {
          setTestDeviceStatus(true);   <- This marks device as in test mode and we can continue to ignore cache and retrieve test messages from server as expected
          Logging.logi("Setting this device as a test device");
          return;
        }
      }
    }
  }

  /** Increments the fetch count which is used to determine if an app install is fresh. */
  private void updateFreshInstallStatus() {
    // We only care about this logic if we are a fresh install.
    if (isFreshInstall) {
      fetchCount += 1;
      if (fetchCount >= MAX_FETCH_COUNT) {
        setFreshInstallStatus(false);
      }
    }
  }

If you navigate to TestDeviceHelper.java on github repo, there’s a curious static variable MAX_FETCH_COUNT = 5. What this does is basically force fetches campaigns from server 5 times to try and see if a test message is sent through “Test on Device”. If it finds receives a test message, it marks the device as in “TestMode” and we can continue using “Test on Device” functionality reliably.
The pitfall here is, if we’ve unexpectedly go beyond MAX_FETCH_COUNT. The app doesn’t do anymore fetches from the server till the next day and we are locked out of using “Test on Device” feature. Wish they’ve documented this bit as it led to some embarrassment when trying to demo my implementation.

Can we force fetch Campaigns from server?

This one’s more of a hack, but the campaigns and impressions are stored in device’s internal storage. The file names can be viewed in ProtoStorageClientModule.java

public static final String CAMPAIGN_CACHE_FILE = “fiam_eligible_campaigns_cache_file”;
public static final String IMPRESSIONS_STORE_FILE = “fiam_impressions_store_file”;
public static final String RATE_LIMIT_STORE_FILE = “rate_limit_store_file”;

Check ProtoStorageClient.java to try and get the exact location. Those files can be deleted to make the application force-fetch from the server.
Don’t tell anyone….shhhh!!

It most certainly should not be done on live code. Only recommended for debug builds.

Why does Firebase UI Takeover Sometimes!!???

You can implement your custom display component extending “FirebaseInAppMessagingDisplay.java”. You need to assign it to FirebaseInAppMessaging.java and the campaigns from server will be redirected to your component.
It seems to work perfectly fine at first….. until you suspend/resume your application.
The Default implementation of FirebaseInAppMessagingDisplay.java hijacks the display component of FirebaseinAppMessaging.java and you’ll start seeing the default Firebase’s UI!!!

  @Override
  public void onActivityResumed(Activity activity) {
    super.onActivityResumed(activity);
    bindFiamToActivity(activity);
  }

  /**
   * Clear FIAM listener on activity paused
   *
   * @hide
   */
  @Override
  public void onActivityPaused(Activity activity) {
    unbindFiamFromActivity(activity);
    headlessInAppMessaging.removeAllListeners();
    super.onActivityPaused(activity);
  }

The only way to get around this is to hijack the it again yourself by registering your display component to Android lifecycle callbacks using “registerActivityLifecycleCallbacks(this)” and setting the MessageDisplayComponent again.
I wish this was documented too……

Hm….. Is Click Count Reliable??

I don’t really have reliable proof for this, but the click count on the Firebase Campaigns dashboard is pretty unreliable. If I send multiple impressions/clicks in succession(~10 sec in-between), noticed that not all of them get registered on the dashboard.
The events does show up in the debug view though.

Well, I certainly hope you take everything here with a pinch-of-salt. Google’s constantly updating the SDK and I hope this post ages poorly and google also decides to update documentation with more valuable information for developer.
Certainly hope this saved you a couple of hours of head-scratching.
Do visit again!