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

[강원대 안드로이드 주민철] 6주차 스텝2 #78

Open
wants to merge 20 commits into
base: joominchul
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# 카카오 테크 캠퍼스 스텝2 클론 코딩 프로젝트, 지도 검색 앱
- 제작 : 주민철
- 기간 : 24.07~08
- 사용 기술 : 코틀린, 안드로이드 스튜디오, 리사이클러뷰, Retrofit, LiveData, Mock, Hilt, MVVM, DataBinding, 코루틴, 파이어베이스, Room 데이터베이스.

## 앱 설명
![권한 설정](https://github.com/user-attachments/assets/e86063e0-6edb-4e99-84c3-05d8b5f6f28b)
앱을 처음 실행하면 스플래시 화면에서 알림 권한을 요청함.
![초기 화면](https://github.com/user-attachments/assets/e6fd2f98-211a-46a8-b5c3-dc9ce936269c)
초기 화면. 카카오 맵 API 사용. 검색창 클릭 시 검색 화면으로 이동.
![검색 화면](https://github.com/user-attachments/assets/d7c32e34-65f1-46ea-8c12-f3da512f268a)
검색 화면에서 검색어 입력 시 카카오 로컬 API를 사용해 검색 결과가 리사이클러뷰로 나옴.
![결과 클릭 시](https://github.com/user-attachments/assets/84de7d5d-311d-4bc7-a266-faac30a7d185)
검색 결과 클릭 시 지도에 장소를 표시함. BottomSheet로 간단한 정보도 표시. 이후 다시 검색창을 클릭 시
![다시 검색 화면](https://github.com/user-attachments/assets/e5cfe1de-476c-4640-ab31-a7f13518ae09)
클릭 했던 검색 결과가 기록으로 남음. 검색 기록은 Room 데이터베이스에 저장.
![알림 화면](https://github.com/user-attachments/assets/3580b023-5711-4088-91d9-fd507d72669f)
파이어베이스를 통해 앱에 알림을 보낼 수도 있음.

# android-map-notification

## 1단계 - Splash Screen
Expand All @@ -12,3 +31,13 @@
- 서버 상태, UI 로딩 등에 대한 상태 관리를 한다.
- 새로 추가되는 부분에도 MVVM 아키텍처 패턴을 적용한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.

## 2단계 - 푸시 알림
### 기능 요구 사항
- Firebase Cloud Message를 설정한다.
- 테스트 메시지를 보낸다.
- 앱이 백그라운드 상태일 경우 FCM 기본 값을 사용하여 Notification을 발생한다.
- 앱이 포그라운드 상태일 경우 커스텀 Notification을 발생한다.
- Notification 창을 터치하면 초기 진입 화면이 호출된다.
### 프로그래밍 요구 사항
- 코드 컨벤션을 준수하며 프로그래밍한다.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import campus.tech.kakao.map.view.MapActivity
import org.junit.Rule
import org.junit.Test


class MapActivityUITest {
Expand Down
14 changes: 11 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
android:supportsRtl="true"
android:theme="@style/Theme.Map"
tools:targetApi="31">
<service
android:name=".Notification.MapFirebaseMessagingService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<activity
android:name=".view.SplashScreen"
android:exported="true"
Expand All @@ -28,8 +37,7 @@
</activity>
<activity
android:name=".view.MapActivity"
android:exported="true" >
</activity>
android:exported="true" />
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package campus.tech.kakao.map.Notification


import campus.tech.kakao.map.Notification.NotificationManager.createNotification
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class MapFirebaseMessagingService: FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
remoteMessage.notification?.let {remoteMessageContent->
createNotification(remoteMessageContent.title, remoteMessageContent.body, this)
}
}

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package campus.tech.kakao.map.Notification

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 campus.tech.kakao.map.R
import campus.tech.kakao.map.view.SplashScreen

object NotificationManager {
private lateinit var notificationManager: NotificationManager
private const val NOTIFICATION_ID = 222222
private const val CHANNEL_ID = "main_default_channel"
private const val CHANNEL_NAME = "main channelName"
private const val CHANNEL_DESCRIPTION = "main channelDescription"
private fun createNotificationChannel(context: Context) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = CHANNEL_DESCRIPTION
}
notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}


fun createNotification(title: String?, body: String?, context: Context){
createNotificationChannel(context)
val intent = Intent(context, SplashScreen::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(
context,
CHANNEL_ID
)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("[포그라운드] $title")
.setContentText("$body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText("앱이 실행 중일 때는 포그라운드 알림이 발생합니다.")
)
.setAutoCancel(true)

notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
81 changes: 80 additions & 1 deletion app/src/main/java/campus/tech/kakao/map/view/SplashScreen.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package campus.tech.kakao.map.view

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 androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import campus.tech.kakao.map.R
import campus.tech.kakao.map.databinding.ActivitySplashScreenBinding
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.Firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
Expand All @@ -16,14 +26,23 @@ import kotlinx.coroutines.launch

class SplashScreen : AppCompatActivity() {
var serviceMessage:String = ""
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
if (isGranted) {
getRemoteConfig()
} else {
getRemoteConfig()
}
}
private lateinit var splashScreenBinding: ActivitySplashScreenBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
splashScreenBinding = ActivitySplashScreenBinding.inflate(layoutInflater)
splashScreenBinding.splash = this
installSplashScreen()
setContentView(splashScreenBinding.root)
getRemoteConfig()
askNotificationPermission()
}

private fun moveMap(sec: Int) {
Expand Down Expand Up @@ -51,4 +70,64 @@ class SplashScreen : AppCompatActivity() {
}
}
}



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

}
}
}

private fun showNotificationPermissionDialog() {
AlertDialog.Builder(this@SplashScreen).apply {
setTitle(getString(R.string.ask_notification_permission_dialog_title))
setMessage(
String.format(
"다양한 알림 소식을 받기 위해 권한을 허용하시겠어요?\n(설정에서 %s의 알림 권한을 허용해주세요.)",
getString(R.string.app_name)
)
)
setPositiveButton(getString(R.string.yes)) { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
getRemoteConfig()
}
setNegativeButton(getString(R.string.deny_notification_permission)) { _, _ ->
getRemoteConfig()
}
show()
}
}

override fun onRestart() {
getRemoteConfig()
super.onRestart()
}

private fun getFcmToken(){
FirebaseMessaging.getInstance().token.addOnCompleteListener(
OnCompleteListener {
if(!it.isSuccessful){
return@OnCompleteListener
}
val token = it.result
Log.d("testt", token.toString())
}
)
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
<string name="serviceState">serviceState</string>
<string name="serviceState_default">ON_SERVICE</string>
<string name="serviceMessage">serviceMessage</string>
<string name="ask_notification_permission_dialog_title">notification 요청</string>
<string name="yes">yes</string>
<string name="deny_notification_permission">no</string>
<string name="fcm_channel_description">fcm_channel_description</string>
</resources>
3 changes: 2 additions & 1 deletion app/src/test/java/campus/tech/kakao/map/FunTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
Expand All @@ -40,7 +42,6 @@ class FunTest{
assert(actualQueryResult.any { it.placeName == query })
}


@Test
fun 검색어_저장_되는지_확인(){
val query = SearchWord(
Expand Down
Binary file added 실행 화면/검색 결과.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/검색 화면.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/결과 클릭 시.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/권한 설정.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/다시 검색 화면.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/실행 영상.mp4
Binary file not shown.
Binary file added 실행 화면/알림 화면.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 실행 화면/초기 화면.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.