2021/06/22

[Jetpack Compose]Analytics / Crashlytics / FCMを導入

jetpack-composefirebasekotlin

概要

以下対応しました。

  • Google Analyticsの導入
  • Firebase Crashlyticsの導入
  • Firebase Cloud Messaging(FCM)を導入して、プッシュ通知をタップすることで特定の画面を開くようにする

準備

  • rootのbuild.gradleに以下の行を追加します
buildscript {
    dependencies {
        // 以下の2行
        classpath "com.google.gms:google-services:4.3.8"
        classpath "com.google.firebase:firebase-crashlytics-gradle:2.7.0"
    }
}
  • 次にappのbuild.gradeに以下の行を追加します
plugins {
    // 以下の2行
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
}

dependencies {
    // 以下の4行
    implementation platform("com.google.firebase:firebase-bom:28.1.0")
    implementation "com.google.firebase:firebase-analytics-ktx"
    implementation "com.google.firebase:firebase-crashlytics-ktx"
    implementation "com.google.firebase:firebase-messaging-ktx"
}
  • Firebaseコンソールからダウンロードしてきたgoogle-services.jsonapp配下のディレクトリ直下に配置します

Crashlyticsのテスト

Crashlyticsは上記の作業をしただけですが、導入が完了しています。

Firebaseコンソール上で、「Crashlyticsを有効化」みたいなボタンを押さないとうまく動作しなかった気がしますが、押さなくてもできるかもしれません。

動作確認には、以下のように強制的にクラッシュするボタンを配置して実施しました。

@Composable
fun CrashlyticsTest(modifier: Modifier = Modifier) {
    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        Button(
            onClick = {
                throw RuntimeException()
            },
            colors = ButtonDefaults.buttonColors(
                backgroundColor = Color.Black,
                contentColor = Color.White
            ),
            shape = Shapes.medium,
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
            contentPadding = PaddingValues(16.dp),
        ) {
            Text(text = "Force Crash!!")
        }
    }
}

Analyticsを有効化

以下のように、MainActivityFirebaseAnalyticsのインスタンスを設定しておきます。

import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase

class MainActivity : ComponentActivity() {

    private lateinit var firebaseAnalytics: FirebaseAnalytics

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        firebaseAnalytics = Firebase.analytics
    }
}

プッシュ通知の設定

プッシュ通知を受け取って、特定の画面(私のアプリの場合は記事詳細画面)を開くことができるように設定しました。

プッシュ通知の準備

以下のリソースを定義しておきます。

  1. プッシュ通知のステータスを表示する小さいアイコン(drawable/ic_stat_ic_notificationとして定義)
  2. プッシュ通知アイコンの色(@color/lightBlue_500として定義)
  3. プッシュ通知に利用するチャンネルID
  4. プッシュ通知からアクティビティを起動するために使うDeepLink用のURL

上記の3と4はstring resourceとして以下のように定義しました。

<resources>
    <string name="android_scheme" translatable="false">https</string>
    <string name="android_host" translatable="false">kumano-te.com</string>
    <string name="default_notification_channel_id" translatable="false">com.kumanote.portfolio.fcm.default</string>
    <string name="new_articles_notification_channel_id" translatable="false">com.kumanote.portfolio.fcm.new_articles</string>
</resources>

AndroidManifestの編集

以下のように編集しました

    <application ...>
        <!-- 以下の3つの meta-data を追加 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_stat_ic_notification" />
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/lightBlue_500" />
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="@string/default_notification_channel_id" />
        
        <activity
            android:name=".MainActivity"
            ...>
            <!-- 以下の intent-filter を追加して DeepLink からアプリの特定の画面を開けるようにする -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="@string/android_scheme" android:host="@string/android_host" />
            </intent-filter>
        </activity>

        <!-- 最後にpush通知をさばくためのサービス(中身は後述)を追加しておきます -->
        <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>

Composable側でのDeepLinkの設定

この記事を参考にしました

@Composable
fun MainView() {
    // baseとなるurlを定義
    val baseUrl = "${stringResource(id = R.string.android_scheme)}://${stringResource(id = R.string.android_host)}"
    val navController = rememberNavController()
    Scaffold(...) {
         NavHost(
            navController = navController,
            startDestination = "home",
        ) {
            composable(
                route = "home",
                deepLinks = listOf(navDeepLink { uriPattern = baseUrl })  // この行を追加
            ) { backStackEntry ->
                Home()
            }
            composable(
                route = "activities/{slug}",
                deepLinks = listOf(navDeepLink { uriPattern = "$baseUrl/activities/{slug}" })  // この行を追加
            ) { backStackEntry ->
                val arguments = requireNotNull(backStackEntry.arguments)
                val slug = arguments.getString("slug")
                ActivityDetail(
                    slug = slug!!,
                    upPress = {
                        navController.navigateUp()
                    }
                )
            }
        }
    }
}

MyFirebaseMessagingServiceを定義

class MyFirebaseMessagingService : FirebaseMessagingService() {

    companion object {
        private const val TAG = "FCM"
        private const val DEFAULT_NOTIFICATION_ID = 0
    }

    override fun onNewToken(token: String) {
        Log.i(TAG, "new FCM token created: $token")
        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        // 通知チャンネルが(なければ)作成しておきます。
        initNotificationChannel(notificationManager = notificationManager)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        Log.d(TAG, "From: " + remoteMessage.from)

        val title = remoteMessage.notification?.title
        val body = remoteMessage.notification?.body
        val data = remoteMessage.data

        // プッシュ通知のデータからどの種類のプッシュ通知かを取得して、どのチャンネルに通知すれば良いか判定します。
        val notificationType = NotificationType.values().firstOrNull {
            it.type == data["type"]
        } ?: NotificationType.DEFAULT

        // プッシュ通知をクリックして開く画面を決めるDeepLinkを生成します。
        // プッシュ通知のデータにpathというデータを含めておき、任意のURLで開けるようにしておきます。
        // サーバーからはNavHostで定義してあるDeepLinkに対応しているpathのみを通知に含めるようにします。
        val baseUrl = "${getString(R.string.android_scheme)}://${getString(R.string.android_host)}"
        val deepLinkUri = if (data.containsKey("path")) {
            val path = data["path"]?.removePrefix("/")
            Uri.parse("$baseUrl/$path")
        } else {
            Uri.parse(baseUrl)
        }

        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        var notificationBuilder = if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
            val channelId = when(notificationType) {
                NotificationType.NEW_ARTICLES -> getString(R.string.new_articles_notification_channel_id)
                else -> getString(R.string.default_notification_channel_id)
            }
            NotificationCompat.Builder(applicationContext, channelId)
        }
        else {
            NotificationCompat.Builder(applicationContext)
        }
        notificationBuilder = notificationBuilder
            .setSmallIcon(R.drawable.ic_stat_ic_notification)
            .setColor(ContextCompat.getColor(applicationContext, R.color.lightBlue_500))
            .setContentTitle(title)
            .setContentText(body)
            .setAutoCancel(true)

        // create intent used on tapped.
        val intent = Intent(
            Intent.ACTION_VIEW,
            deepLinkUri,
            applicationContext,
            MainActivity::class.java
        )
        val pendingIntent: PendingIntent? = TaskStackBuilder.create(applicationContext).run {
            addNextIntentWithParentStack(intent)
            getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
        }
        notificationBuilder.setContentIntent(pendingIntent)

        initNotificationChannel(notificationManager = notificationManager)
        notificationManager.notify(DEFAULT_NOTIFICATION_ID, notificationBuilder.build());
    }

    private fun initNotificationChannel(notificationManager: NotificationManager) {
        if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
            notificationManager.createNotificationChannelIfNotExists(
                channelId = getString(R.string.default_notification_channel_id),
                channelName = "Default",
            )
            notificationManager.createNotificationChannelIfNotExists(
                channelId = getString(R.string.new_articles_notification_channel_id),
                channelName = "New articles",
            )
        }
    }
}

@RequiresApi(Build.VERSION_CODES.O)
fun NotificationManager.createNotificationChannelIfNotExists(
    channelId: String,
    channelName: String,
    importance: Int = NotificationManager.IMPORTANCE_DEFAULT,
) {
    var channel = this.getNotificationChannel(channelId)
    if (channel == null) {
        channel = NotificationChannel(
            channelId,
            channelName,
            importance
        )
        this.createNotificationChannel(channel)
    }
}

enum class NotificationType(
    val type: String,
) {
    DEFAULT(""),
    NEW_ARTICLES("new-articles"),
}

プッシュ通知のテスト

以下のpythonスクリプトで通知を試しました。(問題なく動きました。)

プッシュ通知用のトークンは、onNewToken内で出力したログからあらかじめ取得しておきます。

import firebase_admin
from firebase_admin import messaging

default_app = firebase_admin.initialize_app()

token = "YOUR-FCM-TOKEN-RETREIVED-BY-LOG-INSIDE-onNewToken"
title = "This is test..."
subtitle = "This is subtitle..."
body = "New article has been published!"
notification_data = {
  "type": "new-articles",
  "title": title,
  "subtitle": subtitle,
  "path": "/activities/jetpack-compose-shimmer-card-components"
}

message = messaging.Message(
    notification=messaging.Notification(
        title=title,
        body=body,
    ),
    data=notification_data,
    token=token,
)

response = messaging.send(message)
print('Successfully sent message:', response)

参考資料

以上になります。