diff --git a/README.md b/README.md index 89d97279..e17c8ca1 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,11 @@ - `serviceState` == `ON_SERVICE` - 초기 진입 화면에서 지도 화면으로 이동 - `serviceState` != `ON_SERVICE` - - `serviceMessage` 값을 초기 진입 화면 하단에 표시 + 지도 화면으로 이동 X \ No newline at end of file + - `serviceMessage` 값을 초기 진입 화면 하단에 표시 + 지도 화면으로 이동 X + +### 2단계 - 푸시 알림 +- Firebase의 Cloud Messaging 설정 +- 테스트 메시지 전송 + - 백그라운드 상태 -> FCM 기본 값을 사용해 Notification 발생 + - 포그라운드 상태 -> Custom Notification 발생 +- Notification 창 선택 시 초기 진입 화면 호출 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 802f05a5..e15b9962 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { id("kotlin-parcelize") id("kotlin-kapt") id("com.google.dagger.hilt.android") - //id("com.google.gms.google-services") + id("com.google.gms.google-services") } android { @@ -77,6 +77,7 @@ dependencies { implementation("com.google.firebase:firebase-analytics-ktx") implementation("com.google.firebase:firebase-config-ktx:22.0.0") implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") + implementation ("com.google.firebase:firebase-messaging-directboot:20.2.0") testImplementation("androidx.room:room-testing:2.6.1") testImplementation("junit:junit:4.13.2") testImplementation("io.mockk:mockk-android:1.13.11") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1935805b..0889e2de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + @@ -18,8 +19,18 @@ android:supportsRtl="true" android:theme="@style/Theme.Map" tools:targetApi="31"> + + + + + + diff --git a/app/src/main/java/campus/tech/kakao/map/KakaoMapApplication.kt b/app/src/main/java/campus/tech/kakao/map/KakaoMapApplication.kt index eadf820f..b3e75d28 100644 --- a/app/src/main/java/campus/tech/kakao/map/KakaoMapApplication.kt +++ b/app/src/main/java/campus/tech/kakao/map/KakaoMapApplication.kt @@ -1,14 +1,47 @@ package campus.tech.kakao.map import android.app.Application -import campus.tech.kakao.map.BuildConfig +import android.util.Log +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.remoteConfigSettings import com.kakao.vectormap.KakaoMapSdk import dagger.hilt.android.HiltAndroidApp + @HiltAndroidApp class KakaoMapApplication : Application() { + override fun onCreate() { super.onCreate() + initRemoteConfig() + fetchRemoteConfig() KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY) } + + private fun initRemoteConfig() { + remoteConfig = FirebaseRemoteConfig.getInstance() + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = 0 + } + remoteConfig.setConfigSettingsAsync(configSettings) + } + + private fun fetchRemoteConfig() { + remoteConfig.fetchAndActivate().addOnCompleteListener { task -> + if (task.isSuccessful) { + val serviceState = remoteConfig.getString("serviceState") + val serviceMessage = remoteConfig.getString("serviceMessage") + Log.d("RemoteConfig", "Service State: $serviceState, Service Message: $serviceMessage") + } + else { + val serviceMessage = remoteConfig.getString("serviceMessage") + val e = task.exception + Log.e("RemoteConfig", "Fetch and activate failed", e) + } + } + } + + companion object { + lateinit var remoteConfig: FirebaseRemoteConfig + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapFirebaseMessagingService.kt b/app/src/main/java/campus/tech/kakao/map/MapFirebaseMessagingService.kt new file mode 100644 index 00000000..59bd1afe --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapFirebaseMessagingService.kt @@ -0,0 +1,65 @@ +package campus.tech.kakao.map + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import campus.tech.kakao.map.ui.SplashActivity +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +class MapFirebaseMessagingService : FirebaseMessagingService() { + override fun onMessageReceived(remoteMessage: RemoteMessage) { + Log.d("notification", "From: ${remoteMessage.from}") + + remoteMessage.notification?.let { + Log.d("notification", "Message Notification Body: ${it.body}") + } + + if (remoteMessage.notification != null) { + createNotificationChannel() + sendNotification() + } + } + + private fun sendNotification() { + val intent = Intent(this, SplashActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle("[중요] 포그라운드 알림") + .setContentText("앱이 실행 중입니다.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText("앱이 실행 중일 때는 포그라운드 알림이 발생합니다.") + ) + .setAutoCancel(true) + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + private fun createNotificationChannel() { + val name = getString(R.string.channel_name) + val descriptionText = getString(R.string.channel_description) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance) + channel.description = descriptionText + val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + + companion object { + private const val NOTIFICATION_ID = 1 + private const val CHANNEL_ID = "notification_default_channel" + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/KakaoLocalDTO.kt b/app/src/main/java/campus/tech/kakao/map/data/SearchResponseDTO.kt similarity index 100% rename from app/src/main/java/campus/tech/kakao/map/data/KakaoLocalDTO.kt rename to app/src/main/java/campus/tech/kakao/map/data/SearchResponseDTO.kt diff --git a/app/src/main/java/campus/tech/kakao/map/data/SplashModule.kt b/app/src/main/java/campus/tech/kakao/map/data/SplashModule.kt new file mode 100644 index 00000000..f0caf150 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SplashModule.kt @@ -0,0 +1,26 @@ +package campus.tech.kakao.map.data + +import com.google.firebase.Firebase +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.remoteConfig +import com.google.firebase.remoteconfig.remoteConfigSettings +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SplashModule { + @Provides + @Singleton + fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig { + val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = 0 + } + remoteConfig.setConfigSettingsAsync(configSettings) + return remoteConfig + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/LoadingActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/LoadingActivity.kt deleted file mode 100644 index f30671dc..00000000 --- a/app/src/main/java/campus/tech/kakao/map/ui/LoadingActivity.kt +++ /dev/null @@ -1,44 +0,0 @@ -package campus.tech.kakao.map.ui - -import android.content.Intent -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import campus.tech.kakao.map.R -import campus.tech.kakao.map.databinding.LoadingLayoutBinding -import com.google.firebase.Firebase -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.remoteConfig -import com.google.firebase.remoteconfig.remoteConfigSettings - -class LoadingActivity : AppCompatActivity() { - private lateinit var loadingBinding: LoadingLayoutBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.loading_layout) - loadingBinding = DataBindingUtil.setContentView(this, R.layout.loading_layout) - - val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig - val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = 0 // 개발용 - } - remoteConfig.setConfigSettingsAsync(configSettings) - - remoteConfig.fetchAndActivate() - .addOnCompleteListener(this) { - val serviceState = remoteConfig.getString("serviceState") - if (serviceState == "ON_SERVICE") { - val intent = Intent(this, MapActivity::class.java) - startActivity(intent) - } - else { - val serviceMessage = remoteConfig.getString("serviceMessage") - loadingBinding.tvError.text = serviceMessage - } - } - - - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt index f21fac8a..d238019a 100644 --- a/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt @@ -1,20 +1,29 @@ package campus.tech.kakao.map.ui import android.app.Activity +import android.app.AlertDialog import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings +import android.util.Log import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.Observer import campus.tech.kakao.map.R import campus.tech.kakao.map.databinding.ErrorLayoutBinding import campus.tech.kakao.map.databinding.MapLayoutBinding import campus.tech.kakao.map.domain.Place +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging import com.kakao.vectormap.KakaoMap import com.kakao.vectormap.KakaoMapReadyCallback import com.kakao.vectormap.LatLng @@ -57,6 +66,8 @@ class MapActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + askNotificationPermission() + mapBinding = DataBindingUtil.setContentView(this, R.layout.map_layout) mapBinding.mapView.start(lifeCycleCallback, readyCallback) activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -134,4 +145,43 @@ class MapActivity : AppCompatActivity() { modal.show(supportFragmentManager, "modalBottomSheet") } + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission(), ) { isGranted: Boolean -> + if (isGranted) { + Toast.makeText(this, "Notifications permission granted", Toast.LENGTH_SHORT).show() + } + else { + Toast.makeText(this, "FCM can't post notifications without POST_NOTIFICATIONS permission", Toast.LENGTH_LONG).show() + } + } + + private fun askNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) { + showNotificationPermissionDialog() + } + else { + requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } + } + } + } + + private fun showNotificationPermissionDialog() { + AlertDialog.Builder(this@MapActivity).apply { + setTitle("알림 권한 설정") + setMessage( + String.format("다양한 알림 소식을 받기 위해 권한을 허용하시겠어요?\n(알림에서 %s의 알림 권한을 허용해주세요.)", getString(R.string.app_name)) + ) + setPositiveButton("네") { _, _ -> + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", packageName, null) + intent.data = uri + startActivity(intent) + } + setNegativeButton("아니요") { _, _ -> } + show() + } + } + } diff --git a/app/src/main/java/campus/tech/kakao/map/domain/PlaceUiModel.kt b/app/src/main/java/campus/tech/kakao/map/ui/PlaceUiModel.kt similarity index 68% rename from app/src/main/java/campus/tech/kakao/map/domain/PlaceUiModel.kt rename to app/src/main/java/campus/tech/kakao/map/ui/PlaceUiModel.kt index 747bf85d..616316b2 100644 --- a/app/src/main/java/campus/tech/kakao/map/domain/PlaceUiModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/ui/PlaceUiModel.kt @@ -1,4 +1,6 @@ -package campus.tech.kakao.map.domain +package campus.tech.kakao.map.ui + +import campus.tech.kakao.map.domain.Place data class PlaceUiModel( val placeList: List, diff --git a/app/src/main/java/campus/tech/kakao/map/ui/PlaceViewModel.kt b/app/src/main/java/campus/tech/kakao/map/ui/PlaceViewModel.kt index 63309945..b963cf70 100644 --- a/app/src/main/java/campus/tech/kakao/map/ui/PlaceViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/ui/PlaceViewModel.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope import campus.tech.kakao.map.data.NetworkRepository import campus.tech.kakao.map.data.PlaceRepository import campus.tech.kakao.map.domain.Place -import campus.tech.kakao.map.domain.PlaceUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SplashActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/SplashActivity.kt new file mode 100644 index 00000000..0883fc31 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SplashActivity.kt @@ -0,0 +1,42 @@ +package campus.tech.kakao.map.ui + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import campus.tech.kakao.map.KakaoMapApplication +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.SplashLayoutBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SplashActivity : AppCompatActivity() { + + private lateinit var splashBinding: SplashLayoutBinding + private val splashDisplayLength: Long = 1000 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + splashBinding = DataBindingUtil.setContentView(this, R.layout.splash_layout) + + val remoteConfig = KakaoMapApplication.remoteConfig + val serviceState = remoteConfig.getString("serviceState") + val serviceMessage = remoteConfig.getString("serviceMessage") + + CoroutineScope(Dispatchers.Main).launch { + delay(splashDisplayLength) + if (serviceState == "ON_SERVICE") { + val intent = Intent(this@SplashActivity, MapActivity::class.java) + startActivity(intent) + finish() + } + else { + splashBinding.tvError.text = serviceMessage + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/loading_layout.xml b/app/src/main/res/layout/splash_layout.xml similarity index 96% rename from app/src/main/res/layout/loading_layout.xml rename to app/src/main/res/layout/splash_layout.xml index de82b9e2..33bb7e40 100644 --- a/app/src/main/res/layout/loading_layout.xml +++ b/app/src/main/res/layout/splash_layout.xml @@ -6,7 +6,7 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.LoadingActivity"> + tools:context=".ui.SplashActivity"> 검색어를 입력해 주세요. 검색 결과가 없습니다. https://dapi.kakao.com + notification + defalut_notification \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 76c951ae..49f2a696 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false - //id("com.google.gms.google-services") version "4.4.2" apply false + id("com.google.gms.google-services") version "4.4.2" apply false } allprojects {