LoginSignup
3
1

More than 3 years have passed since last update.

Android で Firebase In-App Messaging が表示されない問題の workaround

Last updated at Posted at 2020-03-21

はじめに

■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
本件は In-App Messaging version 19.0.7 で改修されました :tada:
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

Android で Firebase In-App Messaging を表示させてようとしても、アプリの作り次第では、 表示されない or すぐに消えてしまう問題 が発生することが確認されました。
全てのケースで解決しないかも知れませんが、意図通りに表示できる workaround を見つけたので記しておきます。

Firebase In-App Messaging 導入時の参考になれば幸いです。

※調査した技術内容が多めです
※キャンペーン情報の取得完了のタイミングや、メッセージを表示したい Activity の lifecycle の状態次第では上手く行かないケースが存在するかも知れません
→ 技術的背景を理解した上で、最適な workaround を使うことをオススメします。

本記事での用語

用語 意味
LaunchActivity AndroidManifest.xmlandroid.intent.category.LAUNCHER が指定されている Activity
MainActivity LaunchActivity から起動される Activity
In-App Message Firebase In-App Messaging が表示するメッセージ
(com.google.firebase.inappmessaging.model.InAppMessage というクラスがあります)

再現条件

In-App Message を表示した後、何らかの Activity(最前面になくても良い)が destroy されると、表示されている In-App Message が消える。
(タイミングによっては、表示されたことに気付けない)

ありがちな例

LaunchActivity が、 onCreate で他の Activity を呼び出して、 LaunchActivity は即閉じるような場合には再現します。

実装例

class LaunchActivity : AppCompatActivity() {

    // Firebase In-App Messaging SDK はこの Activity にメッセージを表示させようとするが、
    // すぐに画面遷移してしまうため、メッセージが表示されない(or すぐ消える)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_launch)

        if (isLogin) {
            openMainActivity()
        } else {
            openLoginActivity()
        }
        // ...
    }

    private fun openMainActivity() {
        val intent = Intent(this, MainActivity::class.java)
        //finish() で閉じなくても、↓でも再現
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
        startActivity(intent)
    }
}

なぜ消えるのか?

Firebase In-App Messaging の内部では、 Application.ActivityLifecycleCallbacks を実装したクラス( com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay )が、アプリケーション内の Activity の Lifecycle を監視して、In-App Message の制御を行っています。

onActivityDestroyed の override 実装を見ると、
https://github.com/firebase/firebase-android-sdk/blob/master/firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/FirebaseInAppMessagingDisplay.java#L222-L228

@Override
public void onActivityDestroyed(Activity activity) {
  // clear all state scoped to activity and dismiss fiam
  headlessInAppMessaging.clearDisplayListener();
  imageLoader.cancelTag(activity.getClass());
  removeDisplayedFiam(activity);
  super.onActivityDestroyed(activity);
}

のようになっており、 Activity が destroy されたら、 removeDisplayedFiam(...) が呼び出されています。
FiamFirebase In-App Messaging のことです

特筆すべきは、最前面にある Activity かどうかは考慮していないということです。
(つまり、最前面にない Activity が destroy されることを SDK 側が考慮できていません)

workaround

MainActivity にて、 LaunchActivity が onDestroy されるまで In-App Messaging の表示を遅らせる。

と、端的に言っても、いくつかステップがあります。

  1. In-App Message の表示を抑制しておく
  2. MainActivity にて、 LaunchActivity が onDestroy されたことを検出する
  3. In-App Message の表示を抑制を解除して、In-App Message の表示処理を呼び出す

です。

1. 表示の抑制方法

FirebaseInAppMessaging.getInstance().setMessagesSuppressed(true) を呼び出せば、In-App Message の表示を抑制することができます。

FirebaseInAppMessaging.java
/**
 * Enable or disable suppression of Firebase In App Messaging messages
 *
 * <p>When enabled, no in app messages will be rendered until either you either disable
 * suppression, or the app restarts, as this state is not preserved over app restarts.
 *
 * <p>By default, messages are not suppressed.
 *
 * @param areMessagesSuppressed Whether messages should be suppressed
 */
@Keep
public void setMessagesSuppressed(@NonNull Boolean areMessagesSuppressed) {
  this.areMessagesSuppressed = areMessagesSuppressed;
}

実装を見てもわかるとおり、単に suppress するか否かのフラグの書き換えだけです。
suppress を false にしたからといって、そのタイミングで In-App Message の表示処理が実行されるわけではありません。
(JavaDoc コメントにもそのあたりの言及はありませんでした…)

2. LaunchActivity#onDestroy の検出

Application.ActivityLifecycleCallbacks を使えば検出が可能です。
後述しますが、In-App Messaging は最前面の Activity が resumed になったら表示処理が実行されるので、

  • LaunchActivity#onDestroy になった
  • MainActivity#onResume になった

の2つの条件を満たしたときに、 MainActivity で何らかの callback を受け取れるようにすれば OK です。

もうちょっとやりようはあるかも知れませんが…

InAppMessagingDelayHelper.kt
object InAppMessagingDelayHelper : Application.ActivityLifecycleCallbacks {

    private val targetActivityName: String = MainActivity::class.simpleName.orEmpty()
    private val backgroundActivityName: String = LaunchActivity::class.simpleName.orEmpty()

    private val destroyed = MutableLiveData<Boolean>().apply { value = false }
    private val resumed = MutableLiveData<Boolean>().apply { value = false }

    init {
        FirebaseInAppMessaging.getInstance().setMessagesSuppressed(true)
    }

    val canShow: LiveData<Boolean> = Transformations.distinctUntilChanged(
        MediatorLiveData<Boolean>().apply {
            value = false
            listOf(destroyed, resumed).forEach { liveData ->
                addSource(liveData) {
                    val isDestroyed = destroyed.value ?: false
                    val isResumed = resumed.value ?: false
                    value = isDestroyed && isResumed
                }
            }
        }
    )

    override fun onActivityDestroyed(activity: Activity?) {
        destroyed.value = (backgroundActivityName == activity?.localClassName)
    }

    override fun onActivityResumed(activity: Activity?) {
        resumed.value = (targetActivityName == activity?.localClassName)
    }

    override fun onActivityPaused(activity: Activity?) = Unit

    override fun onActivityStarted(activity: Activity?) = Unit

    override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) = Unit

    override fun onActivityStopped(activity: Activity?) = Unit

    override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) = Unit

}

を作って、 Application で registerActivityLifecycleCallbacks しておけば

override fun onCreate() {
    super.onCreate()
    registerActivityLifecycleCallbacks(InAppMessagingDelayHelper)
    ...
}

MainActivityLaunchActivity is destroyed && MainActivity is resumed の状態になったコールバックを受け取ることができます。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ....

    InAppMessagingDelayHelper.canShow.observe(this, Observer { canShow ->
        if (canShow) {
            // LaunchActivity is destroyed && MainActivity is resumed
            // TODO : In-App Message の表示処理の呼び出し
        }
    })
    ...
}

3. In-App Message の表示処理を呼び出す

In-App Message の表示処理は、 FirebaseInAppMessagingDisplay#onActivityResumed から呼び出されています。

FirebaseInAppMessagingDisplay.java
@Override
public void onActivityResumed(Activity activity) {
  super.onActivityResumed(activity);
  if (inAppMessage != null) {
    showActiveFiam(activity);
  }
}
...
private void showActiveFiam(@NonNull final Activity activity) {
  if (inAppMessage == null || headlessInAppMessaging.areMessagesSuppressed()) {
    Logging.loge("No active message found to render");
    return;
  }
  // 表示処理
}

showActiveFiam(...) は private ですが、呼び出し元の onActivityResumed(...) は public なので、
FirebaseInAppMessagingDisplay.getInstance() でインスタンスを取得すれば呼び出すことができます。

つまり、

InAppMessagingDelayHelper.canShow.observe(this, Observer { canShow ->
    if (canShow) {
-       // LauncherActivity is destroyed && MainActivity is resumed
-       // TODO : In-App Message の表示処理の呼び出し
+       FirebaseInAppMessaging.getInstance().setMessagesSuppressed(false) // 抑制解除
+       FirebaseInAppMessagingDisplay.getInstance().onActivityResumed(this) // 表示処理の呼び出し
    }
})

のようにすれば、In-App Message がちゃんと表示されるようになります。

アプリケーションによって、Landing する Activity までに、どんな Activity が表示されては消えるのかがまちまちだと思うので、それぞれのアプリケーションに応じた回避方法を採る必要があると考えています。
あくまでも参考程度にして下さい。

本件は Firebase Android SDK の GitHub Issue で報告しているので、あわよくば、将来的にはこの workaround は不要になるかも知れません。
(不要になることを祈ってます。)
https://github.com/firebase/firebase-android-sdk/issues/1324

 

それ以外に調べた事

調査したときにわかったことを、ついでなので記しておきます。

メッセージレイアウトのトップバナーは Push 通知ではない

これは、単に勘違いしていただけですが、一応記載。
スクリーンショット 2020-03-22 1.16.17(2).png

スクリーンショット 2020-03-22 1.16.21(2).png
こんな感じで Push 通知っぽく表示されますが、Push 通知ではありません。
つまり、アプリを開かないと表示されません。

アプリの起動を促進させるためには、Cloud Messaging を使う必要があります。
※In-App Messaging と Cloud Messaging を併用すれば、Push 通知経由でアプリを開いた場合のみに、特定の In-App Message を表示させることも可能です。

In-App Message を表示している状態で別 Activity を表示させたときの挙動

新たに開いた Activity の上に、In-App Message が新たに表示される。
(既に表示している In-App Message が、新たに開いた Activity に隠れる…と思ってましたが、大丈夫でした!)

In-App Message 表示させる方法

1. キャンペーンを作成して、テストデバイスに送る

https://firebase.google.com/docs/in-app-messaging/get-started?authuser=0&platform=android
によると、

電力を節約するため、Firebase アプリ内メッセージングはサーバーからのメッセージの取得を 1 日に 1 回だけ行います。この設定の場合、テストが困難になることがあるため、メッセージをオンデマンドで表示するテストデバイスを Firebase コンソールで指定できます。

と書かれています。
この手ももちろん使えます。
(でも、若干めんどくさい…)

2. コードでダミーのメッセージを生成して、メッセージの表示処理を呼び出す

Firebase Android SDK のソースコード を見てみると、どうやら、コードで In-App Messaging の表示処理を実行することができそうです。

リンク先が変わるかも知れないので、コードを引用しておきます。

com.example.firebase.fiamui.MainActivity.java
ModalMessage message =
    builder
        .setBackgroundHexColor(bodyBackgroundColorString)
        .setTitle(title)
        .setBody(body)
        .setImageData(imageData)
        .setAction(modalAction)
        .build(campaignMetadata, data);

FirebaseInAppMessagingDisplay.getInstance()
    .testMessage(this, message, new NoOpDisplayCallbacks());

これを LaunchActivity#onCreate(...) などで実行します。
この手を使うと、色んなタイミングで In-App Messaging の表示処理の呼び出しを再現することが可能です。
表示処理の呼び出しのタイミングを変えてみたり、表示時や表示中の Activity の状態を変えてみるには、この方法が手軽でオススメです。

具体的に、どんな場合に In-App Messaging の表示処理が呼び出されるのかは後述します。

In-App Message の表示処理の発動

InAppMessageStreamManager.java
public Flowable<TriggeredInAppMessage> createFirebaseInAppMessageStream() {
  return Flowable.merge(
          appForegroundEventFlowable,
          analyticsEventsManager.getAnalyticsEventsFlowable(),
          programmaticTriggerEventFlowable)
      .doOnNext(e -> Logging.logd("Event Triggered: " + e))
      .observeOn(schedulers.io())
      ...色んな処理... // キャンペーン取得の通信が絡めば時間が掛かる可能性がある
      .observeOn(schedulers.mainThread())

これを、 FirebaseInAppMessaging の constructor で subscribe しています。
(内部では RxJava を使ってるんですね!)

FirebaseInAppMessaging.java
public class FirebaseInAppMessaging {
  FirebaseInAppMessaging(...) {
    ...
    Disposable unused =
        inAppMessageStreamManager
            .createFirebaseInAppMessageStream()
            .subscribe(FirebaseInAppMessaging.this::triggerInAppMessage);
  }
  ...
  private void triggerInAppMessage(TriggeredInAppMessage inAppMessage) {
    if (this.fiamDisplay != null) {
      fiamDisplay.displayMessage(
          inAppMessage.getInAppMessage(),
          displayCallbacksFactory.generateDisplayCallback(
              inAppMessage.getInAppMessage(), inAppMessage.getTriggeringEvent()));
    }
  }
  • アプリが最前面に来たとき
  • Analytics の Event が発行されたとき
  • programmaticTriggerEvent が発行されたとき

に In-App Message の表示処理が開始されます。
ただし、バックグラウンド処理が完了してから、UI への表示処理の実行になるので、その間に Activity が遷移・終了している可能性も十分あり得ます。

しかしながら、UI への表示処理を行ったタイミングで最前面の Activity に表示されるようになっていました。
(逆に言えば、 MainActivity#onCreate でイベントを送信し、それをトリガーに In-App Messaging を表示するように設定しても、画面遷移をしてしまえば、別の Activity で表示されることもある)

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1