Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT/#114] FCM 알림 #117

Merged
merged 14 commits into from
Jan 30, 2024
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
3 changes: 3 additions & 0 deletions .github/workflows/pr_checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Create Local Properties
run: touch local.properties

- name: Access Firebase Service
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json

- name: Access Local Properties
env:
base_url: ${{ secrets.BASE_URL }}
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"
android:minSdkVersion="33" />

<application
android:name=".MyApp"
Expand All @@ -24,6 +26,14 @@
android:theme="@style/Theme.TeumTeum"
android:usesCleartextTraffic="true"
tools:targetApi="tiramisu">
<service
android:name=".config.TeumMessagingService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".presentation.familiar.topic.TopicActivity"
android:exported="false" />
Expand Down Expand Up @@ -95,6 +105,10 @@
<activity
android:name=".presentation.group.join.JoinFriendListActivity"
android:exported="false" />

<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/teum_notification_channel_id" />
</application>

</manifest>
100 changes: 100 additions & 0 deletions app/src/main/java/com/teumteum/teumteum/config/TeumMessagingService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.teumteum.teumteum.config

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.teumteum.data.model.request.toDeviceToken
import com.teumteum.data.service.UserService
import com.teumteum.domain.TeumTeumDataStore
import com.teumteum.teumteum.R
import com.teumteum.teumteum.presentation.splash.SplashActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class TeumMessagingService : FirebaseMessagingService() {

@Inject
lateinit var dataStore: TeumTeumDataStore

@Inject
lateinit var userService: UserService

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

dataStore.deviceToken = token

if (dataStore.userToken != "") {
GlobalScope.launch {
kotlin.runCatching {
userService.patchDeviceToken(
token.toDeviceToken()
)
}.onFailure {
Timber.e(it.message)
userService.postDeviceToken(
token.toDeviceToken()
)
}
}
}
}

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

if (dataStore.isLogin) {
if (message.data.isNotEmpty() && message.data["title"].toString() != EMPTY) {
sendNotificationAlarm(
Message(message.data["title"].toString(), message.data["content"].toString())
)
}
else {
message.notification?.let {
sendNotificationAlarm(Message(it.title.toString(), it.body.toString()))
}
}
}
}

private fun sendNotificationAlarm(message: Message) {
val requestCode = (System.currentTimeMillis() % 10000).toInt()
val intent = Intent(this, SplashActivity::class.java)
intent.putExtra("isFromAlarm", true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent =
PendingIntent.getActivity(
this, requestCode, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE
)
val channelId = getString(R.string.teum_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(this, channelId).setSmallIcon(R.mipmap.ic_teum_teum_launcher_round)
.setContentTitle(message.title).setContentText(message.body)
.setPriority(NotificationCompat.PRIORITY_HIGH).setAutoCancel(true)
.setContentIntent(pendingIntent)

val channel = NotificationChannel(channelId, "teumteum", NotificationManager.IMPORTANCE_HIGH)
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)

notificationManager.run {
createNotificationChannel(channel)
notify(requestCode, notificationBuilder.build())
}
}

companion object {
const val EMPTY = "null"
}

private data class Message(var title: String, var body: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,36 @@ package com.teumteum.teumteum.presentation

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.teumteum.base.BindingActivity
import com.teumteum.base.util.extension.boolExtra
import com.teumteum.base.util.extension.intExtra
import com.teumteum.teumteum.R
import com.teumteum.teumteum.databinding.ActivityMainBinding
import com.teumteum.teumteum.presentation.home.HomeFragmentDirections
import com.teumteum.teumteum.util.callback.CustomBackPressedCallback
import com.teumteum.teumteum.presentation.signin.SignInViewModel
import com.teumteum.teumteum.presentation.splash.SplashViewModel
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main) {
private val id by intExtra()
private val isFromAlarm by boolExtra()

private val viewModel by viewModels<SignInViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

checkAskedNotification()

val navHostFragment = supportFragmentManager.findFragmentById(R.id.fl_main) as NavHostFragment
val navController = navHostFragment.navController
Expand All @@ -29,6 +40,12 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
if (id != -1) {
moveRecommendDetail()
}

if (isFromAlarm) {
val action = HomeFragmentDirections.actionHomeFragmentToFragmentFamiliar()
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fl_main) as NavHostFragment
navHostFragment.navController.navigate(action)
}
}

fun hideBottomNavi() {
Expand All @@ -48,14 +65,44 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
navHostFragment.navController.navigate(action)
}

private fun checkAskedNotification() {
if (!viewModel.getAskedNotification()) {
requestNotificationPermission()
viewModel.setAskedkNotification(true)
}
}

private val requestNotificationPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { _ ->
}

private fun requestNotificationPermission() {
if (
ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) {
} else {
requestNotificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
}
}

override fun finish() {
super.finish()
closeActivitySlideAnimation()
}

companion object {
fun getIntent(context: Context, id: Int) = Intent(context, MainActivity::class.java).apply {
fun getIntent(context: Context, id: Int, isFromAlarm: Boolean = false) = Intent(context, MainActivity::class.java).apply {
putExtra("id", id)
putExtra("isFromAlarm", isFromAlarm)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.teumteum.teumteum.presentation.signin

import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import com.teumteum.base.BindingActivity
import com.teumteum.base.util.extension.setOnSingleClickListener
import com.teumteum.teumteum.R
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.teumteum.domain.repository.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -39,6 +38,12 @@ class SignInViewModel @Inject constructor(
_memberState.value = SignInUiState.Failure(socialLoginResult.messages!!)
}
}

fun getAskedNotification(): Boolean = repository.getAskedNotification()

fun setAskedkNotification(didAsk: Boolean) {
repository.setAskedNotification(didAsk)
}
}

sealed interface SignInUiState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ class SignUpViewModel @Inject constructor(
.onSuccess {
// setAutoLogin에 회원가입 이후 유저토큰 전달
authRepository.setAutoLogin(it.accessToken, it.refreshToken)
postDeviceTokens()
// userInfo에 임시로 넣어뒀던 아이디 -> 서버에서 받은 id 값으로 변경 -> 필요 시 사용
// it.id로 아이디 사용해서 내 정보 얻어오기
_userInfoState.value = UserInfoUiState.Success
Expand All @@ -317,6 +318,15 @@ class SignUpViewModel @Inject constructor(
}
}

private fun postDeviceTokens() {
val deviceToken = authRepository.getDeviceToken()
if (deviceToken.isNotBlank()) {
viewModelScope.launch {
authRepository.postDeviceToken(deviceToken)
}
}
}

private fun combineDateStrings(): String {
// 숫자를 두 자리로 맞추기 위해 %02d 형식을 사용
val formattedMonth = String.format("%02d", birthMonth.value.toInt())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ class SplashActivity
: BindingActivity<ActivitySplashBinding>(R.layout.activity_splash) {

private val viewModel by viewModels<SplashViewModel>()
private var isFromAlarm = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

isFromAlarm = intent.getBooleanExtra(IS_FROM_ALARM, false)
checkNetwork()
setUpObserver()
}
Expand Down Expand Up @@ -105,13 +108,15 @@ class SplashActivity
}

private fun startHomeScreen() {
startActivity(Intent(this, MainActivity::class.java))
startActivity(MainActivity.getIntent(this, -1, isFromAlarm = isFromAlarm))
finish()
}

companion object {
const val IS_FIRST_AFTER_INSTALL = 0
const val IS_AUTO_LOGIN = 1
const val HAVE_TO_SIGN_IN = 2

const val IS_FROM_ALARM = "isFromAlarm"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class SplashViewModel @Inject constructor(
repository.getMyInfoFromServer()
.onSuccess {
saveUserInfo(userInfo = it)
patchDeviceToken()
_myInfo.value = it
_myInfoState.value = MyInfoUiState.Success
}
Expand All @@ -60,6 +61,25 @@ class SplashViewModel @Inject constructor(
}

fun getIsFirstAfterInstall(): Boolean = authRepository.getIsFirstAfterInstall()

private fun postDeviceToken() {
val deviceToken = authRepository.getDeviceToken()
if (deviceToken.isNotBlank()) {
viewModelScope.launch {
authRepository.postDeviceToken(deviceToken)
}
}
}

private fun patchDeviceToken() {
val deviceToken = authRepository.getDeviceToken()
if (deviceToken.isNotBlank()) {
viewModelScope.launch {
val result = authRepository.patchDeviceToken(deviceToken)
if (!result) postDeviceToken()
}
}
}
}

sealed interface MyInfoUiState {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/item_onboarding_viewpager.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
android:layout_height="wrap_content"
android:text="@string/onboarding_tv_namecard_title"
android:layout_marginTop="20dp"
android:textAppearance="@style/ta.headline.2"
android:textAppearance="@style/ta.headline.3"
android:lineSpacingExtra="3sp"
android:textColor="?attr/color_text_headline_primary"
app:layout_constraintStart_toStartOf="parent"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,6 @@
<string name="review_finish_title">리뷰 남기기가\n완료되었습니다.</string>
<string name="review_go_home">홈으로 돌아가기</string>

<string name="teum_notification_channel_id">teum_notification_channel</string>

</resources>
Loading
Loading