Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pr_checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,11 @@ jobs:
echo "base.url=\"https://temp.example.com\"" > local.properties
fi

- name: Create google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
echo "$GOOGLE_SERVICES_JSON" > app/google-services.json

- name: Build Debug APK
run: ./gradlew :app:assembleDebug
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.externalNativeBuild
.cxx
local.properties
google-services.json
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.google.services)
}

val properties = Properties().apply {
Expand Down Expand Up @@ -95,4 +96,9 @@ dependencies {
// Kakao
implementation(libs.kakao.user)
implementation(libs.kakao.map)

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.firebase.messaging)
}
18 changes: 18 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<queries>
<intent>
Expand Down Expand Up @@ -56,6 +57,23 @@
android:scheme="kakao${KAKAO_NATIVE_APP_KEY}" />
</intent-filter>
</activity>
<service
android:name=".core.fcm.MoiveFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/img_logo_white_bg" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="false" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />

Comment thread
vahkjsdf marked this conversation as resolved.
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.moive.app.core.fcm

import com.google.firebase.installations.FirebaseInstallations
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume

class FirebaseMessagingManager @Inject constructor() {

//서버 연결
suspend fun getFcmToken(): String? = suspendCancellableCoroutine { continuation ->
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (continuation.isActive) {
if (!task.isSuccessful) {
Timber.tag(TAG).e("FCM Token 불러오기 실패했습니다. ${task.exception}")
continuation.resume(null)
return@addOnCompleteListener
}

val token = task.result
Timber.tag(TAG).d("토큰 : $token")
Comment thread
vahkjsdf marked this conversation as resolved.
continuation.resume(token)
}
}
}

//디바이스 ID
suspend fun getInstallationId(): String? = suspendCancellableCoroutine { continuation ->
FirebaseInstallations.getInstance().id.addOnCompleteListener { task ->
if (continuation.isActive) {
if (!task.isSuccessful) {
Timber.tag(TAG).e("FireBaseInstallations id 불러오기 실패했습니다 : ${task.exception}")
continuation.resume(null)
return@addOnCompleteListener
}

val installationId = task.result
Timber.tag(TAG).d("device ID : $installationId")
continuation.resume(installationId)
}
}
}

companion object {
private const val TAG = "FCM"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.moive.app.core.fcm

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.moive.app.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import timber.log.Timber

class MoiveFirebaseMessagingService: FirebaseMessagingService() {

private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

override fun onNewToken(token: String) {
super.onNewToken(token)

Timber.tag(TAG).d("token: $token")
//Todo: 서버 토큰 전달
Comment thread
vahkjsdf marked this conversation as resolved.
}

override fun onDestroy() {
serviceScope.cancel()
super.onDestroy()
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)

val title = remoteMessage.data[MESSAGE_TITLE] ?: return
val body = remoteMessage.data[MESSAGE_BODY]
val meetingId = remoteMessage.data[MESSAGE_MEETING_ID]?.toLongOrNull()

showNotification(title, body, meetingId)
}

private fun showNotification(title: String, body: String?, meetingId: Long?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH,
).apply {
enableVibration(true)
}
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}

val granted = ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) == PackageManager.PERMISSION_GRANTED
if (!granted) return

val notificationId = System.currentTimeMillis().toInt()

val contentIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
meetingId?.let { putExtra(MESSAGE_MEETING_ID, it) }
} ?: return
val pendingIntent = PendingIntent.getActivity(
this,
notificationId,
contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.img_logo_white_bg)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()

NotificationManagerCompat.from(this).notify(notificationId, notification)
}

companion object {
private const val TAG = "FCM"
const val MESSAGE_MEETING_ID = "meetingId"
private const val MESSAGE_TITLE = "title"
private const val MESSAGE_BODY = "body"
private const val CHANNEL_ID = "moive_default_channel"
private const val CHANNEL_NAME = "일반 알림"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import com.moive.app.core.designsystem.theme.MoiveTheme
import com.moive.app.core.fcm.MoiveFirebaseMessagingService
import com.moive.app.core.network.token.AuthManager
import com.moive.app.data.meeting.repository.MeetingRepository
import com.moive.app.presentation.login.navigation.navigateToLogin
import com.moive.app.presentation.meeting.detail.navigation.navigateToMeetingDetail
import com.moive.app.presentation.notification.NotificationPermissionManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
Expand All @@ -29,11 +31,17 @@ class MainActivity : ComponentActivity() {

private val pendingInviteCode = mutableStateOf<String?>(null)

private val pendingMeetingId = mutableStateOf<Long?>(null)

private val notificationPermissionManager = NotificationPermissionManager(this)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
pendingInviteCode.value = intent.extractInviteCode()
setIntent(Intent())
pendingMeetingId.value = intent.extractMeetingId()
?: savedInstanceState?.getLong(KEY_PENDING_MEETING_ID, -1L)?.takeIf { it != -1L }
intent = Intent()
Comment thread
vahkjsdf marked this conversation as resolved.
setContent {
MoiveTheme {
val appState = rememberMainAppState()
Expand All @@ -44,6 +52,11 @@ class MainActivity : ComponentActivity() {
}
}

LaunchedEffect(Unit) {
appState.isSignedIn.first { it }
notificationPermissionManager.askNotificationPermission()
}

LaunchedEffect(pendingInviteCode.value) {
val inviteCode = pendingInviteCode.value ?: return@LaunchedEffect

Expand All @@ -60,6 +73,15 @@ class MainActivity : ComponentActivity() {
pendingInviteCode.value = null
}

LaunchedEffect(pendingMeetingId.value) {
val meetingId = pendingMeetingId.value ?: return@LaunchedEffect

appState.isSignedIn.first { it }

appState.navController.navigateToMeetingDetail(meetingId)
pendingMeetingId.value = null
}

MainScreen(
appState = appState,
)
Expand All @@ -70,13 +92,26 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
pendingInviteCode.value = intent.extractInviteCode()
pendingMeetingId.value = intent.extractMeetingId()
setIntent(Intent())
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
pendingMeetingId.value?.let { outState.putLong(KEY_PENDING_MEETING_ID, it) }
}

companion object {
private const val TAG = "Invite"
private const val KEY_PENDING_MEETING_ID = "pendingMeetingId"
}
}

private fun Intent.extractInviteCode(): String? =
takeIf { it.action == Intent.ACTION_VIEW }?.data?.lastPathSegment

private fun Intent.extractMeetingId(): Long? {
val meetingId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID, -1L)
if (meetingId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID)
return meetingId.takeIf { it != -1L }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.moive.app.presentation.notification

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import timber.log.Timber

class NotificationPermissionManager(
private val activity: ComponentActivity
) {

private val requestPermissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (!isGranted) {
Timber.tag(TAG).d(NOTIFICATION_MESSAGE)
}
}

fun askNotificationPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return

val granted = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.POST_NOTIFICATIONS,
) == PackageManager.PERMISSION_GRANTED

if (!granted) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}

companion object {
private const val TAG = "Notification"
private const val NOTIFICATION_MESSAGE = "알림을 표시할 수 없습니다."
}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.google.services) apply false
}
11 changes: 10 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ timber = "5.0.1"
kakaoUser = "2.24.0"
kakaoMap = "2.15.1"

# Firebase
googleServices = "4.5.0"
firebaseBom = "34.19.0"

# Test
junit = "4.13.2"
junitVersion = "1.3.0"
Expand Down Expand Up @@ -96,6 +100,11 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakaoUser" }
kakao-map = { group = "com.kakao.maps.open", name = "android", version.ref = "kakaoMap" }

# Firebase
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }

# Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand All @@ -109,7 +118,7 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }

google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }

[bundles]
androidx-core = [
Expand Down
Loading