はじめに
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
本件は In-App Messaging version 19.0.7 で改修されました
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Android で Firebase In-App Messaging を表示させてようとしても、アプリの作り次第では、 表示されない or すぐに消えてしまう問題 が発生することが確認されました。
全てのケースで解決しないかも知れませんが、意図通りに表示できる workaround を見つけたので記しておきます。
Firebase In-App Messaging 導入時の参考になれば幸いです。
※調査した技術内容が多めです
※キャンペーン情報の取得完了のタイミングや、メッセージを表示したい Activity の lifecycle の状態次第では上手く行かないケースが存在するかも知れません
→ 技術的背景を理解した上で、最適な workaround を使うことをオススメします。
本記事での用語
用語 | 意味 |
---|---|
LaunchActivity |
AndroidManifest.xml で android.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(...)
が呼び出されています。
※ Fiam
は Firebase In-App Messaging のことです
特筆すべきは、最前面にある Activity かどうかは考慮していないということです。
(つまり、最前面にない Activity が destroy されることを SDK 側が考慮できていません)
workaround
MainActivity
にて、 LaunchActivity
が onDestroy されるまで In-App Messaging の表示を遅らせる。
と、端的に言っても、いくつかステップがあります。
- In-App Message の表示を抑制しておく
-
MainActivity
にて、LaunchActivity
が onDestroy されたことを検出する - In-App Message の表示を抑制を解除して、In-App Message の表示処理を呼び出す
です。
1. 表示の抑制方法
FirebaseInAppMessaging.getInstance().setMessagesSuppressed(true)
を呼び出せば、In-App Message の表示を抑制することができます。
/**
* 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 です。
もうちょっとやりようはあるかも知れませんが…
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)
...
}
MainActivity
で LaunchActivity is destroyed && MainActivity is resumed の状態になったコールバックを受け取ることができます。
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
から呼び出されています。
@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 通知ではない
これは、単に勘違いしていただけですが、一応記載。
は
こんな感じで 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 の表示処理を実行することができそうです。
リンク先が変わるかも知れないので、コードを引用しておきます。
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 の表示処理の発動
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 を使ってるんですね!)
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 で表示されることもある)