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 14 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# android-map-notification

## 1단계 - Splash Screen
### 기능 요구 사항
- 초기 진입 화면을 추가한다.
- Firebase의 Remote Config를 설정한다.
- 서비스 상태를 나타내는 매개변수를 아래와 같이 각각 등록한다.
- 매개변수 이름:serviceState, serviceMessage
- 매개변수 serviceState 값이 ON_SERVICE일 때만 초기 진입 화면이 지도 화면으로 넘어간다.
- 매개변수 serviceState 값이 ON_SERVICE이 아닌 경우에는 serviceMessage 값을 초기 진입 화면 하단에 표시하고 지도 화면으로 진입하지 않는다.
### 프로그래밍 요구 사항
- 서버 상태, UI 로딩 등에 대한 상태 관리를 한다.
- 새로 추가되는 부분에도 MVVM 아키텍처 패턴을 적용한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.

## 2단계 - 푸시 알림
### 기능 요구 사항
- Firebase Cloud Message를 설정한다.
- 테스트 메시지를 보낸다.
- 앱이 백그라운드 상태일 경우 FCM 기본 값을 사용하여 Notification을 발생한다.
- 앱이 포그라운드 상태일 경우 커스텀 Notification을 발생한다.
- Notification 창을 터치하면 초기 진입 화면이 호출된다.
### 프로그래밍 요구 사항
- 코드 컨벤션을 준수하며 프로그래밍한다.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ android {
}

dependencies {
implementation("androidx.core:core-splashscreen:1.0.1")
val fragment_version = "1.8.1"
debugImplementation("androidx.fragment:fragment-testing-manifest:$fragment_version")
debugImplementation("androidx.fragment:fragment-testing:$fragment_version")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import campus.tech.kakao.map.view.SearchFragment
import org.hamcrest.core.AllOf.allOf
import org.junit.Test
import org.junit.runner.RunWith
Expand Down
19 changes: 16 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,28 @@
android:supportsRtl="true"
android:theme="@style/Theme.Map"
tools:targetApi="31">
<service
android:name=".MapFirebaseMessagingService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<activity
android:name=".MapActivity"
android:exported="true">
android:name=".view.SplashScreen"
android:exported="true"
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".view.MapActivity"
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


import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

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

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

}
65 changes: 65 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/Notification.kt
Original file line number Diff line number Diff line change
@@ -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 androidx.core.app.NotificationCompat
import campus.tech.kakao.map.view.SplashScreen

class Notification {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍을 좀 달리하면 좋겠네요. Notification을 띄우고 Notification Channel을 생성하는 거니 Notification ~~ (Manager 등) 으로 변경하심 좋겠습니다. 그리고 단순히 기능만 제공하니 object 클래스로 구현하셔도 괜찮을 것 같네요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경했습니다.

private lateinit var notificationManager: NotificationManager
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())
}



companion object {
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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package campus.tech.kakao.map.module

import campus.tech.kakao.map.Notification
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NotificationModule {
@Provides
@Singleton
fun provideNotification(): Notification {
return Notification()
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 주입까지 잘 하셨는데 사실 구현하신 Notification은 Service가 동작할 시에만 필요한 것이기에 굳이 의존성 주입 할 필요가 있나라는 생각도 드네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신 대로 굳이 필요한 것 같지 않아 삭제했습니다.

179 changes: 179 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package campus.tech.kakao.map.view

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import campus.tech.kakao.map.viewModel.MainViewModel
import campus.tech.kakao.map.R
import campus.tech.kakao.map.databinding.ActivityMapBinding
import campus.tech.kakao.map.databinding.BottomSheetBinding
import campus.tech.kakao.map.databinding.MapErrorBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.kakao.vectormap.KakaoMap
import com.kakao.vectormap.KakaoMapReadyCallback
import com.kakao.vectormap.LatLng
import com.kakao.vectormap.MapLifeCycleCallback
import com.kakao.vectormap.camera.CameraUpdate
import com.kakao.vectormap.camera.CameraUpdateFactory
import com.kakao.vectormap.label.LabelOptions
import com.kakao.vectormap.label.LabelStyle
import com.kakao.vectormap.label.LabelStyles
import dagger.hilt.android.AndroidEntryPoint
import java.lang.Exception

@AndroidEntryPoint
class MapActivity : AppCompatActivity() {
private var map: KakaoMap? = null
private lateinit var model: MainViewModel
lateinit var placeName:String
lateinit var addressName:String
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private var longitude:Double = 0.0
private var latitude:Double = 0.0
private lateinit var binding: ActivityMapBinding
private lateinit var bottomBinding: BottomSheetBinding
companion object{
const val MARKER_WIDTH = 100
const val MARKER_HEIGHT = 100
const val MARKER_TEXT_SIZE = 40
const val ZOOM_LEVEL = 17
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProvider(this)[MainViewModel::class.java]
getMapInfo()
binding = ActivityMapBinding.inflate(layoutInflater)
binding.map = this
val view = binding.root
setContentView(view)
bottomBinding = BottomSheetBinding.inflate(layoutInflater)
model.documentClickedDone()
binding.mapView.start(object : MapLifeCycleCallback() {
override fun onMapDestroy() {

}

override fun onMapError(p0: Exception?) {
val errorBinding = MapErrorBinding.inflate(layoutInflater)
setContentView(errorBinding.root)
errorBinding.mapErrorText.text = p0?.message
}

}, object: KakaoMapReadyCallback() {
override fun onMapReady(kakaoMap: KakaoMap) {
map = kakaoMap
}

override fun getPosition(): LatLng {
return LatLng.from(latitude, longitude)
}

override fun getZoomLevel(): Int {
return ZOOM_LEVEL
}
})
binding.searchBar.setOnClickListener {
onSearchBarClicked()
}
initBottomSheet()
documentClickedObserve()
}
private fun onSearchBarClicked(){
val fragmentManager = supportFragmentManager
val searchFragment = SearchFragment()
val transaction = fragmentManager.beginTransaction()
transaction.replace(binding.activityMap.id, searchFragment)
transaction.addToBackStack(null)
binding.activityMapFrameLayout.setOnTouchListener(View.OnTouchListener { v, event -> true })
transaction.commit()
}

override fun onResume() {
super.onResume()
binding.mapView.resume()
}
private fun documentClickedObserve(){
model.documentClicked.observe(this){documentClicked->
if(documentClicked){
makeMarker()
setBottomSheet()
val cameraUpdate: CameraUpdate = CameraUpdateFactory.newCenterPosition(LatLng.from(latitude, longitude))
map?.moveCamera(cameraUpdate)
}
else{
map?.labelManager?.layer?.removeAll()
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}

override fun onPause() {
super.onPause()
binding.mapView.pause()
}

private fun getMapInfo(){
model.getMapInfo()
model.mapInfo.observe(this) { mapInfo ->
if (!mapInfo.isNullOrEmpty()) {
latitude = mapInfo[0].toDouble()
longitude = mapInfo[1].toDouble()
placeName = mapInfo[2]
addressName = mapInfo[3]
}
else{
Log.e("testt", "getMapInfo: mapInfo is null")
}
}
}

private fun makeMarker(){
val bitmapImage = BitmapFactory.decodeResource(resources, R.drawable.marker)
val markerImage = Bitmap.createScaledBitmap(bitmapImage, MARKER_WIDTH, MARKER_HEIGHT, true)
val styles = map?.labelManager?.addLabelStyles(LabelStyles.from(LabelStyle.from(markerImage).setTextStyles(
MARKER_TEXT_SIZE, Color.BLACK)))
if(styles != null){
val options = LabelOptions.from(LatLng.from(latitude, longitude)).setStyles(styles).setTexts(placeName)
map?.labelManager?.layer?.removeAll()
map?.labelManager?.layer?.addLabel(options)
}
else{
Log.e("MapActivity", "makeMarker: styles is null")
}
}

private fun initBottomSheet(){
bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomSheetInclude.bottomSheet)
bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback(){
override fun onStateChanged(bottomSheet: View, newState: Int) {
}

override fun onSlide(bottomSheet: View, slideOffset: Float) {
}

})
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}

private fun setBottomSheet(){
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
binding.invalidateAll()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && supportFragmentManager.backStackEntryCount > 0) {
model.documentClickedDone()
supportFragmentManager.popBackStack()
return true
}

return super.onKeyDown(keyCode, event)
}
}
Loading