Conversation
thatfiredev
left a comment
There was a problem hiding this comment.
Thanks for working on this @lehcar09 ! I have added some comments/suggestions below
...ng/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/MyFirebaseMessagingService.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
...ng/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/FirebaseMessagingViewModel.kt
Outdated
Show resolved
Hide resolved
...ng/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/FirebaseMessagingViewModel.kt
Show resolved
Hide resolved
...ng/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/FirebaseMessagingViewModel.kt
Outdated
Show resolved
Hide resolved
|
/gemini review |
There was a problem hiding this comment.
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>" |
There was a problem hiding this comment.
There's a stray double-quote character at the end of this line, which will cause an XML parsing error and fail the build.
| <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") |
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
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.
| var message = context.getString(R.string.msg_permission_granted) | ||
| if (!isGranted) { | ||
| message = context.getString(R.string.msg_permission_failed) | ||
| } |
There was a problem hiding this comment.
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.
| 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) | |
| } |
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)), |
There was a problem hiding this comment.
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),| fun MainAppViewPreview() { | ||
| FirebaseMessagingTheme { | ||
| MainScreen() | ||
| } |
There was a problem hiding this comment.
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.
| private var _token: MutableStateFlow<String> = MutableStateFlow("") | ||
| val token: MutableStateFlow<String> = _token |
There was a problem hiding this comment.
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.
| private var _token: MutableStateFlow<String> = MutableStateFlow("") | |
| val token: MutableStateFlow<String> = _token | |
| private val _token = MutableStateFlow("") | |
| val token: StateFlow<String> = _token |
Changes made:
EntryChoiceActivityMainActivityFirebaseMessagingViewModelfor business logicComposeMainActivityfor the Jetpack Compose UI