Skip to content

Adding compose code for FCM#1632

Open
lehcar09 wants to merge 5 commits intofirebase:composefrom
lehcar09:compose-fcm
Open

Adding compose code for FCM#1632
lehcar09 wants to merge 5 commits intofirebase:composefrom
lehcar09:compose-fcm

Conversation

@lehcar09
Copy link
Contributor

@lehcar09 lehcar09 commented Jun 10, 2024

Changes made:

  • Add Jetpack Compose option in EntryChoiceActivity
  • Removed business logic from MainActivity
  • Added FirebaseMessagingViewModel for business logic
  • Added ComposeMainActivity for the Jetpack Compose UI

@lehcar09 lehcar09 changed the title - Initial Commit Adding compose code for FCM Jun 10, 2024
@lehcar09 lehcar09 requested a review from thatfiredev July 19, 2024 12:27
Copy link
Member

@thatfiredev thatfiredev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @lehcar09 ! I have added some comments/suggestions below

@thatfiredev
Copy link
Member

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Jetpack Compose version of the FCM quickstart UI, refactoring the MainActivity to use a new FirebaseMessagingViewModel for business logic, which improves separation of concerns and modernizes the UI. A critical security concern is the logging of sensitive information, such as the FCM registration token and intent extras, to Logcat by FirebaseMessagingViewModel, which could lead to data leakage. These log statements should be removed or sanitized. Furthermore, several issues were identified, including a critical XML syntax error in strings.xml, incomplete Material Theming adoption in Compose, a ViewModel exposing mutable state flow, a likely Compose preview crash, and opportunities for LaunchedEffect and minor code style improvements.

<string name="msg_permission_granted">Notifications permission granted</string>
<string name="msg_permission_failed">FCM can\'t post notifications without POST_NOTIFICATIONS permission</string>

<string name="msg_setup_readme_instructions">See README for setup instructions</string>"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There's a stray double-quote character at the end of this line, which will cause an XML parsing error and fail the build.

Suggested change
<string name="msg_setup_readme_instructions">See README for setup instructions</string>"
<string name="msg_setup_readme_instructions">See README for setup instructions</string>

intent.extras?.let {
it.keySet().forEach { key ->
val value = intent.extras?.getString(key)
Log.d(TAG, "Key: $key Value: $value")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The logNotificationData function logs all extras from the incoming intent. Intent extras can contain sensitive information from FCM data payloads. Logging this data to Logcat can lead to information leakage, as Logcat is accessible to other applications and via ADB. It is recommended to only log specific, non-sensitive fields or to remove this logging in production builds.

viewModelScope.launch {
try {
_token.value = messaging.getToken().await()
Log.d(TAG, "FCM registration Token: ${_token.value}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The getToken function logs the FCM registration token. FCM tokens are sensitive credentials that identify a specific app instance and can be used to send targeted notifications. Logging them to Logcat is a security risk as it exposes these credentials to anyone with access to the device logs. This logging should be removed or restricted to debug builds only.

Comment on lines +87 to +90
var message = context.getString(R.string.msg_permission_granted)
if (!isGranted) {
message = context.getString(R.string.msg_permission_failed)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The message variable is assigned once and never changed within its scope. It can be declared as a val using an if-else expression, which is better for readability and immutability.

Suggested change
var message = context.getString(R.string.msg_permission_granted)
if (!isGranted) {
message = context.getString(R.string.msg_permission_failed)
}
val message = if (isGranted) {
context.getString(R.string.msg_permission_granted)
} else {
context.getString(R.string.msg_permission_failed)
}

Comment on lines +97 to +126
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
//Create Notification Channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
fcmViewModel.setNotificationChannel(
context,
context.getString(R.string.default_notification_channel_id),
context.getString(R.string.default_notification_channel_name)
)
}

intent?.let { intentData ->
fcmViewModel.logNotificationData(intentData)
}

scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.msg_setup_readme_instructions))
}

fcmViewModel.askNotificationPermission(context, requestPermissionLauncher)
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using DisposableEffect with Lifecycle.Event.ON_CREATE for one-time setup can lead to the code running multiple times, for example on configuration changes. A more idiomatic and safer way to perform one-time setup in Compose is to use LaunchedEffect(Unit). This ensures the code runs only once when the composable enters the composition.

    LaunchedEffect(Unit) {
        //Create Notification Channel
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            fcmViewModel.setNotificationChannel(
                context,
                context.getString(R.string.default_notification_channel_id),
                context.getString(R.string.default_notification_channel_name)
            )
        }

        intent?.let { intentData ->
            fcmViewModel.logNotificationData(intentData)
        }

        snackbarHostState.showSnackbar(context.getString(R.string.msg_setup_readme_instructions))

        fcmViewModel.askNotificationPermission(context, requestPermissionLauncher)
    }

.fillMaxSize(),
topBar = {
TopAppBar(
backgroundColor = colorResource(R.color.colorPrimary)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TopAppBar's background color is hardcoded using colorResource. To better support theming (including dark theme), you should use colors from MaterialTheme.colors. The Theme.kt file already defines FirebaseBlue as the primary color for the light theme.

                backgroundColor = MaterialTheme.colors.primary

.width(200.dp)
.wrapContentHeight()
.padding(0.dp, 20.dp, 0.dp, 0.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.colorPrimary)),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Button's background color is hardcoded using colorResource. To better support theming (including dark theme), you should use colors from MaterialTheme.colors. The Theme.kt file already defines FirebaseBlue as the primary color for the light theme. This also applies to the other button.

            colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary),

Comment on lines +236 to +239
fun MainAppViewPreview() {
FirebaseMessagingTheme {
MainScreen()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This preview will likely crash because MainScreen's default viewModel() call will try to instantiate FirebaseMessagingViewModel. The factory for this ViewModel calls FirebaseMessaging.getInstance(), which is not available in a preview context. To make previews work, you should provide a mock or fake implementation of the ViewModel.

Comment on lines +28 to +29
private var _token: MutableStateFlow<String> = MutableStateFlow("")
val token: MutableStateFlow<String> = _token

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _token MutableStateFlow is exposed publicly as mutable. To follow best practices for encapsulation and prevent unintended state modifications from outside the ViewModel, you should expose it as an immutable StateFlow. Also, _token can be a val since the MutableStateFlow instance itself is not reassigned.

Suggested change
private var _token: MutableStateFlow<String> = MutableStateFlow("")
val token: MutableStateFlow<String> = _token
private val _token = MutableStateFlow("")
val token: StateFlow<String> = _token

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants