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] 푸쉬알림 / 알림권한 요청 #231

Merged
merged 59 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4662a19
[add] gitignore google-services.json
Sangwook123 Nov 13, 2023
2b1738e
[add] Dependencies, Plugins, Versions
Sangwook123 Nov 13, 2023
0dcb0a2
[feat] firebase gradle setting
Sangwook123 Nov 13, 2023
32fd741
[feat] datastore에 디바이스 토큰 저장 로직
Sangwook123 Nov 13, 2023
a1ce97b
[feat] WineyMessagingService
Sangwook123 Nov 13, 2023
f6c64f1
[add] #226 service lifecycleScope dependencies
Sangwook123 Nov 19, 2023
6f52257
[mod] #226 파이어베이스 서비스 코드 수정
Sangwook123 Nov 20, 2023
73fa15c
[add] #226 아이콘 리소스 추가
Sangwook123 Nov 20, 2023
d90a1b5
[feat] #226 알림설정 스위치 모션 레이아웃 구현
Sangwook123 Nov 20, 2023
c3cdaa0
[feat] #226 스위치 상태 처리
Sangwook123 Nov 20, 2023
01918bb
[chore] #226 ResponseGetUserDto 변경
Sangwook123 Nov 20, 2023
9eebf99
[feat] #226 알림설정 Dto 구현
Sangwook123 Nov 20, 2023
1869607
[chore] #226 domain User 엔터티 알림동의여부 추가
Sangwook123 Nov 20, 2023
46949f1
[feat] #226 알림동의 여부 변경 service
Sangwook123 Nov 20, 2023
a40c575
[feat] #226 알림동의 여부 변경 datasource
Sangwook123 Nov 20, 2023
6228ae9
[feat] #226 알림동의 여부 변경 repository
Sangwook123 Nov 20, 2023
768942c
[chore] #226 알림동의여부 변경 response dto
Sangwook123 Nov 20, 2023
4f3623a
[feat] #226 알림동의 여부변경 함수 구현
Sangwook123 Nov 20, 2023
30f3eab
[feat] #226 알림동의여부 변경 ui 구현
Sangwook123 Nov 20, 2023
2c35766
[feat] fcm토큰 패치 dto
Sangwook123 Dec 5, 2023
9ea1a46
[feat] fcm token patch 서비스
Sangwook123 Dec 5, 2023
38b24d3
[feat] fcm 토큰 패치 datasource
Sangwook123 Dec 5, 2023
8f30fc2
[feat] fcm 토큰 패치 repository
Sangwook123 Dec 5, 2023
52c2089
[feat] patchFcmToken 뷰모델
Sangwook123 Dec 5, 2023
173d49f
[mod] firebase messaging service 수정
Sangwook123 Dec 5, 2023
cb34398
[feat] 알림 분기처리
Sangwook123 Dec 5, 2023
01d7885
[feat] 앱이 foreground인지 판별 로직
Sangwook123 Dec 5, 2023
39ba5d9
[chore] 토큰 로직 개선
Sangwook123 Dec 5, 2023
bc1835a
[chore] ktlintformat
Sangwook123 Dec 5, 2023
fa5332a
Merge branch 'develop' into feature/feat-fcm
Sangwook123 Dec 5, 2023
4235bb2
[chore] string 리소스
Sangwook123 Dec 5, 2023
643ef55
[chore] prchecker google service.json 추가
Sangwook123 Dec 5, 2023
a9c5f7a
[chore] pr checker 수정
Sangwook123 Dec 5, 2023
c8ee570
[chore] prchecker
Sangwook123 Dec 5, 2023
727adc5
[add] manifest 알림권한 요청
Sangwook123 Dec 11, 2023
6a91cc0
[feat] 알림권한 요청
Sangwook123 Dec 11, 2023
a4312b0
[refactor] activitylifecyclecallbacks 클래스화
Sangwook123 Dec 21, 2023
c098203
[refactor] wineymessagingservice 함수 세분화
Sangwook123 Dec 21, 2023
a66700f
[refactor] wineyApplication 콜백 클래스화
Sangwook123 Dec 21, 2023
dd0c643
[feat] notificationtype enum으로 관리
Sangwook123 Dec 21, 2023
bc88e5f
[mod] 코드리뷰 반영
Sangwook123 Dec 21, 2023
473505b
Merge branch 'feature/feat-fcm' into feature/feat-fcm-permission
Sangwook123 Dec 21, 2023
c11b3fa
[mod] request permission
Sangwook123 Dec 21, 2023
f6337af
[chore] ktlint
Sangwook123 Dec 21, 2023
d119f8a
[mod] 코드리뷰 반영
Sangwook123 Dec 27, 2023
8e3627b
Merge branch 'feature/feat-fcm' into feature/feat-fcm-permission
Sangwook123 Dec 27, 2023
6338412
[chore] 알림권한허용여부 스낵바
Sangwook123 Dec 27, 2023
732ac49
[chore] 분기처리
Sangwook123 Dec 29, 2023
70ff794
[chore] #231 마이페이지 진입시 토글 고정
Sangwook123 Jan 2, 2024
d9a1e09
[add] #231 스트링 리소스 추가
Sangwook123 Jan 3, 2024
a1f474d
[chore] #231 메인액티비티 스낵바 권한없을때만
Sangwook123 Jan 3, 2024
8a67701
[feat] #231 알림 로직 구현
Sangwook123 Jan 3, 2024
1163375
[chore] #231 문구 수정
Sangwook123 Jan 3, 2024
ea11f90
[add] #231 아이콘 리소스 추가
Sangwook123 Jan 9, 2024
f294b12
[add] #231 스트링 리소스 추가
Sangwook123 Jan 9, 2024
41977ec
[ui] #231 기기알림설정 ui
Sangwook123 Jan 9, 2024
40c8913
[feat] #231 기기 알림설정
Sangwook123 Jan 9, 2024
7667f64
[chore] #231 ktlintformat
Sangwook123 Jan 9, 2024
b8f383e
[chore] #231 코드리뷰 반영
Sangwook123 Jan 9, 2024
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
5 changes: 5 additions & 0 deletions .github/workflows/pr_checker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ jobs:
echo keyPassword=$KEY_PASSWORD >> ./local.properties
echo storePassword=$STORE_PASSWORD >> ./local.properties

- name: Create Google Services JSON File
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $GOOGLE_SERVICES_JSON > ./app/google-services.json

- name: Build debug APK
run: ./gradlew assembleDebug --stacktrace

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ captures/
*.pem

# Google Services (e.g. APIs or Firebase)
# google-services.json
google-services.json

# Android Patch
gen-external-apklibs
Expand Down
12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ plugins {
id(ModulePlugins.kotlinSerialization)
id(ModulePlugins.hilt)
id(ModulePlugins.oss)
id(ModulePlugins.googleService)
id(ModulePlugins.firebaseAppdistribution)
id(ModulePlugins.firebaseCrashlytics)
}

android {
Expand Down Expand Up @@ -102,6 +105,7 @@ dependencies {
implementation(lifecycleLiveDataKtx)
implementation(lifecycleViewModelKtx)
implementation(lifecycleJava8)
implementation(lifecycleService)
implementation(splashScreen)
implementation(pagingRuntime)
implementation(workManager)
Expand Down Expand Up @@ -145,4 +149,12 @@ dependencies {
debugImplementation(flipperLeakCanary)
debugImplementation(soloader)
}

FirebaseDependencies.run {
implementation(platform(bom))
implementation(messaging)
implementation(analytics)
implementation(crashlytics)
implementation(remoteConfig)
}
}
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

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

<application
android:name=".WineyApplication"
Expand All @@ -16,6 +19,14 @@
android:theme="@style/Theme.Winey"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<service
android:name=".configuration.WineyMessagingService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<activity
android:name=".presentation.splash.SplashActivity"
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/org/go/sopt/winey/ActivityLifecycleHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.go.sopt.winey

import android.app.Activity
import android.app.Application
import android.os.Bundle

class ActivityLifecycleHandler(private val application: Application) :
Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
}

override fun onActivityStarted(p0: Activity) {
}

override fun onActivityResumed(p0: Activity) {
isAppInForeground = true
}

override fun onActivityPaused(p0: Activity) {
isAppInForeground = false
}

override fun onActivityStopped(p0: Activity) {
}

override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
}

override fun onActivityDestroyed(p0: Activity) {
}

companion object {
var isAppInForeground = false
}
}
1 change: 1 addition & 0 deletions app/src/main/java/org/go/sopt/winey/WineyApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class WineyApplication : Application() {
setupTimber()
setupKakaoSdk()
preventDarkMode()
registerActivityLifecycleCallbacks(ActivityLifecycleHandler(this))
}

private fun setupTimber() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.go.sopt.winey.configuration

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.go.sopt.winey.ActivityLifecycleHandler
import org.go.sopt.winey.R
import org.go.sopt.winey.domain.repository.DataStoreRepository
import org.go.sopt.winey.presentation.splash.SplashActivity
import javax.inject.Inject

@AndroidEntryPoint
class WineyMessagingService : FirebaseMessagingService() {

@Inject
lateinit var dataStoreRepository: DataStoreRepository

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

CoroutineScope(Dispatchers.IO).launch { dataStoreRepository.saveDeviceToken(token) }
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
if (remoteMessage.data.isNotEmpty() && !ActivityLifecycleHandler.isAppInForeground) {
sendNotification(remoteMessage)
}
}

private fun createNotificationIntent(remoteMessage: RemoteMessage): Intent {
return Intent(this, SplashActivity::class.java).apply {
putExtra(KEY_NOTI_TYPE, remoteMessage.data[KEY_NOTI_TYPE])
putExtra(KEY_FEED_ID, remoteMessage.data[KEY_FEED_ID])
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}

private fun createPendingIntent(intent: Intent, uniqueIdentifier: Int): PendingIntent {
return PendingIntent.getActivity(
this,
uniqueIdentifier,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
}

private fun getSoundUri(): Uri {
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}

private fun generateUniqueIdentifier(): Int {
return (System.currentTimeMillis() / 7).toInt()
}

private fun createNotificationBuilder(remoteMessage: RemoteMessage, pendingIntent: PendingIntent): NotificationCompat.Builder {
val soundUri = getSoundUri()

return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(remoteMessage.data[KEY_TITLE])
.setContentText(remoteMessage.data[KEY_MESSAGE])
.setAutoCancel(true)
.setSound(soundUri)
.setContentIntent(pendingIntent)
}

private fun showNotification(notificationBuilder: NotificationCompat.Builder, uniqueIdentifier: Int) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}

notificationManager.notify(uniqueIdentifier, notificationBuilder.build())
}

private fun sendNotification(remoteMessage: RemoteMessage) {
val uniqueIdentifier = generateUniqueIdentifier()
val intent = createNotificationIntent(remoteMessage)
val pendingIntent = createPendingIntent(intent, uniqueIdentifier)
val notification = createNotificationBuilder(remoteMessage, pendingIntent)

showNotification(notification, uniqueIdentifier)
}

companion object {
private const val KEY_FEED_ID = "feedId"
private const val KEY_NOTI_TYPE = "notiType"
private const val KEY_TITLE = "title"
private const val KEY_MESSAGE = "message"
private const val CHANNEL_NAME = "Notice"
private const val CHANNEL_ID = "channel"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ class AuthInterceptor @Inject constructor(
dataStoreRepository.saveAccessToken(accessToken, refreshToken)
}

private fun handleTokenExpired(chain: Interceptor.Chain, originalRequest: Request, headerRequest: Request): Response {
private fun handleTokenExpired(
chain: Interceptor.Chain,
originalRequest: Request,
headerRequest: Request
): Response {
val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody())
.url("$AUTH_BASE_URL/auth/token")
.addHeader(REFRESH_TOKEN, runBlocking(Dispatchers.IO) { getRefreshToken() })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.go.sopt.winey.data.model.remote.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class RequestPatchAllowedNotificationDto(
@SerialName("allowedPush")
val allowedPush: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.go.sopt.winey.data.model.remote.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class RequestPatchFcmTokenDto(
@SerialName("token")
val fcmToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ data class ResponseGetUserDto(
@SerialName("userId")
val userId: Int,
@SerialName("userLevel")
val userLevel: String
val userLevel: String,
@SerialName("fcmIsAllowed")
val fcmIsAllowed: Boolean
)

fun toUser(): User {
Expand All @@ -46,6 +48,7 @@ data class ResponseGetUserDto(
return User(
nickname = userResponseUserDto?.nickname.orEmpty(),
userLevel = userResponseUserDto?.userLevel.orEmpty(),
fcmIsAllowed = userResponseUserDto?.fcmIsAllowed ?: false,
duringGoalAmount = data.userResponseGoalDto?.duringGoalAmount ?: 0,
duringGoalCount = data.userResponseGoalDto?.duringGoalCount ?: 0,
targetMoney = data.userResponseGoalDto?.targetMoney ?: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.go.sopt.winey.data.model.remote.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ResponsePatchAllowedNotificationDto(
@SerialName("isAllowed")
val isAllowed: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package org.go.sopt.winey.data.repository

import org.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto
import org.go.sopt.winey.data.model.remote.request.RequestLoginDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchAllowedNotificationDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchFcmTokenDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto
import org.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto
import org.go.sopt.winey.data.model.remote.response.ResponseLoginDto
Expand Down Expand Up @@ -60,4 +62,14 @@ class AuthRepositoryImpl @Inject constructor(
runCatching {
authDataSource.patchNickname(requestPatchNicknameDto).data
}

override suspend fun patchAllowedNotification(request: Boolean): Result<Boolean?> =
runCatching {
authDataSource.patchAllowedNotification(RequestPatchAllowedNotificationDto(allowedPush = request)).data?.isAllowed
}

override suspend fun patchFcmToken(token: String): Result<Unit> =
runCatching {
authDataSource.patchFcmToken(RequestPatchFcmTokenDto(fcmToken = token))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class DataStoreRepositoryImpl @Inject constructor(
}
}

override suspend fun saveDeviceToken(deviceToken: String) {
dataStore.edit {
it[DEVICE_TOKEN] = deviceToken
}
}

override suspend fun saveUserId(userId: Int) {
dataStore.edit {
it[USER_ID] = userId
Expand All @@ -51,6 +57,10 @@ class DataStoreRepositoryImpl @Inject constructor(
return getStringValue(REFRESH_TOKEN)
}

override suspend fun getDeviceToken(): Flow<String?> {
return getStringValue(DEVICE_TOKEN)
}

override suspend fun getStringValue(key: Preferences.Key<String>): Flow<String?> {
return dataStore.data
.catch { exception ->
Expand Down Expand Up @@ -119,6 +129,7 @@ class DataStoreRepositoryImpl @Inject constructor(
stringPreferencesKey("social_refresh_token")
private val ACCESS_TOKEN: Preferences.Key<String> = stringPreferencesKey("access_token")
private val REFRESH_TOKEN: Preferences.Key<String> = stringPreferencesKey("refresh_token")
private val DEVICE_TOKEN: Preferences.Key<String> = stringPreferencesKey("device_token")
private val USER_ID: Preferences.Key<Int> = intPreferencesKey("user_id")
private val USER_INFO: Preferences.Key<String> = stringPreferencesKey("user_info")
}
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/org/go/sopt/winey/data/service/AuthService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package org.go.sopt.winey.data.service

import org.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto
import org.go.sopt.winey.data.model.remote.request.RequestLoginDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchAllowedNotificationDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchFcmTokenDto
import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto
import org.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDto
import org.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto
import org.go.sopt.winey.data.model.remote.response.ResponseGetUserDto
import org.go.sopt.winey.data.model.remote.response.ResponseLoginDto
import org.go.sopt.winey.data.model.remote.response.ResponseLogoutDto
import org.go.sopt.winey.data.model.remote.response.ResponsePatchAllowedNotificationDto
import org.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto
import org.go.sopt.winey.data.model.remote.response.base.BaseResponse
import retrofit2.http.Body
Expand Down Expand Up @@ -53,4 +56,14 @@ interface AuthService {
suspend fun patchNickname(
@Body requestPatchNicknameDto: RequestPatchNicknameDto
): BaseResponse<Unit>

@PATCH("user/notification")
suspend fun patchAllowedNotification(
@Body requestPatchAllowedNotificationDto: RequestPatchAllowedNotificationDto
): BaseResponse<ResponsePatchAllowedNotificationDto>

@PATCH("user/fcmtoken")
suspend fun patchFcmToken(
@Body requestPatchFcmTokenDto: RequestPatchFcmTokenDto
): BaseResponse<Unit>
}
Loading