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

부산대 Android 박정훈 6주차 과제 Step2 #72

Open
wants to merge 20 commits into
base: pjhn
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# android-map-notification

## 기능 요구 사항
- 초기 진입 화면을 추가한다
- Firebase의 Remote Config를 설정한다
- 매개변수 serviceState 값이 ON_SERVICE일 때만 초기 진입 화면이 지도 화면으로 넘어간다.
- 매개변수 serviceState 값이 ON_SERVICE이 아닌 경우에는
serviceMessage 값을 초기 진입 화면 하단에 표시하고 지도 화면으로 진입하지 않는다.
- Firebase Cloud Message를 설정한다.
- 테스트 메시지를 보낸다.
- 앱이 백그라운드 상태일 경우 FCM 기본 값을 사용하여 Notification을 발생한다.
- 앱이 포그라운드 상태일 경우 커스텀 Notification을 발생한다.
- Notification 창을 터치하면 초기 진입 화면이 호출된다.

## 프로그래밍 요구 사항
- 서버 상태, UI 로딩 등에 대한 상태 관리를 한다.
- 새로 추가되는 부분에도 MVVM 아키텍처 패턴을 적용한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.


16 changes: 12 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".PlaceApplication"
android:allowBackup="true"
Expand All @@ -15,9 +16,18 @@
android:supportsRtl="true"
android:theme="@style/Theme.Map"
tools:targetApi="31">
<service
android:name=".data.service.MyFirebaseMessagingService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<activity
android:name=".presentation.splash.SplashScreenActivity"
android:exported="true" >
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand All @@ -26,9 +36,7 @@
</activity>
<activity
android:name=".presentation.map.MapActivity"
android:exported="true">

</activity>
android:exported="true"></activity>
<activity
android:name=".presentation.search.SearchViewModel"
android:exported="false" />
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package campus.tech.kakao.map

import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.view.View
import com.kakao.vectormap.KakaoMapSdk
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.qualifiers.ApplicationContext


@HiltAndroidApp
class PlaceApplication: Application() {

override fun onCreate() {
super.onCreate()
appInstance = this

initKakaoMapSdk()
}
Expand All @@ -24,11 +26,9 @@ class PlaceApplication: Application() {
KakaoMapSdk.init(this, key)
}
companion object {
@Volatile
private lateinit var appInstance: PlaceApplication
fun isNetworkActive(): Boolean {
fun isNetworkActive(@ApplicationContext context: Context): Boolean {

Choose a reason for hiding this comment

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

static function 으로 선언되어있고, hilt를 통해 context가 전달되는 방식이 아니기 때문에 @ApplicationContext 어노테이션은 제거해도 괜찮을것 같습니다

val connectivityManager: ConnectivityManager =
appInstance.getSystemService(ConnectivityManager::class.java)
context.getSystemService(ConnectivityManager::class.java)
val network: Network = connectivityManager.activeNetwork ?: return false
val actNetwork: NetworkCapabilities =
connectivityManager.getNetworkCapabilities(network) ?: return false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package campus.tech.kakao.map.data

import android.content.Context
import android.content.SharedPreferences
import campus.tech.kakao.map.domain.model.Place
import javax.inject.Inject

class LastVisitedPlaceManager @Inject constructor(context: Context) {

private val sharedPreferences = context.getSharedPreferences("LastVisitedPlace", Context.MODE_PRIVATE)
class LastVisitedPlaceManager
@Inject constructor(private val sharedPreferences: SharedPreferences) {

fun saveLastVisitedPlace(place: Place) {
val editor = sharedPreferences.edit()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ open class PlaceLocalDataRepository @Inject constructor(
private val placeDao: PlaceDao,
) : PlaceRepository {

override suspend fun getPlaces(placeName: String): List<Place> {
override suspend fun getPlaces(placeName: String, page: Int): List<Place> {
return placeDao.getPlaces(placeName).map { it.toPlace() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@ class PlaceRemoteDataRepository @Inject constructor(
private val placeDao: PlaceDao,
private val kakaoApi: KakaoApi
) : PlaceLocalDataRepository(placeDao){
override suspend fun getPlaces(placeName: String): List<Place> {
override suspend fun getPlaces(placeName: String, page: Int): List<Place> {
return withContext(Dispatchers.IO) {
val resultPlaces = mutableListOf<Place>()
for (page in 1..3) {
val response = kakaoApi.getSearchKeyword(
key = BuildConfig.KAKAO_REST_API_KEY,
query = placeName,
size = 15,
page = page
)
if (response.isSuccessful) {
response.body()?.documents?.let { resultPlaces.addAll(it) }
} else throw RuntimeException("통신 에러 발생")
}
updatePlaces(resultPlaces)

val response = kakaoApi.getSearchKeyword(
key = BuildConfig.KAKAO_REST_API_KEY,
query = placeName,
size = 15,
page = page
)
Comment on lines +21 to +26

Choose a reason for hiding this comment

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

한번에 45개 정도로 많은 데이터를 가져오고 싶은 상황이라면, size를 page * 3 정도로 하거나 loadSize를 인자로 넘겨받는 방법도 괜찮을것 같습니다. 현재는 page 1~3 을 모두 호출해야하니 api 통신이 3회 이뤄지게 되는것이니, api 통신횟수를 줄이면서 원하는 양의 데이터를 가져오게 할 수 있을것 같네요

if (response.isSuccessful) {
response.body()?.documents?.let { resultPlaces.addAll(it) }
} else throw RuntimeException("통신 에러 발생")

resultPlaces
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import javax.inject.Inject
class RemoteConfigRepository
@Inject
constructor(val remoteConfig: FirebaseRemoteConfig): ConfigRepository {
init {
setRemoteConfig()
}

override fun getData(): ConfigData {
return ConfigData(
Expand All @@ -28,13 +25,6 @@ constructor(val remoteConfig: FirebaseRemoteConfig): ConfigRepository {
)
}

fun setRemoteConfig() {
val configSettings = remoteConfigSettings {
minimumFetchIntervalInSeconds = 0
}
remoteConfig.setConfigSettingsAsync(configSettings)
}

suspend fun getFetchedRemoteResult(): Result<ConfigData> {
return withContext(Dispatchers.IO){
try {
Expand All @@ -45,5 +35,4 @@ constructor(val remoteConfig: FirebaseRemoteConfig): ConfigRepository {
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package campus.tech.kakao.map.data.service

import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.util.Log
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import campus.tech.kakao.map.R
import campus.tech.kakao.map.presentation.splash.SplashScreenActivity

class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
private const val NOTIFICATION_ID = 222222
private const val CHANNEL_ID = "main_default_channel"
private const val CHANNEL_NAME = "main channelName"
}

private lateinit var notificationManager: NotificationManager

override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("myToken", "Token: ${token}")
}

// Foreground 에서 호출 됨
override fun onMessageReceived(remoteMessage: RemoteMessage) {

remoteMessage.notification?.let {
sendNotification(it.title, it.body)
}
}

private fun sendNotification(title: String?, message: String?) {
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannel()

val pendingIntent = setPendingIntent()
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.img_location)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setAutoCancel(true) //사용자가 알림을 클릭하면 해당 알림이 자동으로 사라짐

notificationManager.notify(NOTIFICATION_ID, builder.build())
}

private fun setPendingIntent(): PendingIntent{

val intent = Intent(this, SplashScreenActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}

return PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE // 생성 후 수정 x
)
}

private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "기본 채널"
}
notificationManager.createNotificationChannel(channel)
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/di/DatabaseModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package campus.tech.kakao.map.di

import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.data.database.PlaceDatabase
Expand Down Expand Up @@ -30,4 +31,10 @@ object DatabaseModule {
fun providePlaceDao(placeDatabase: PlaceDatabase): PlaceDao {
return placeDatabase.placeDao()
}

@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("LastVisitedPlace", Context.MODE_PRIVATE)
}
}
33 changes: 33 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/di/ListenerModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package campus.tech.kakao.map.di

import android.app.Activity
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.presentation.search.SearchActivity
import campus.tech.kakao.map.presentation.search.SearchActivityRecyclerviewListener
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped

@Module
@InstallIn(ActivityComponent::class)
object ListenerModule {

@Provides
@ActivityScoped
fun provideSearchActivityRecyclerviewListener(activity: Activity)
: SearchActivityRecyclerviewListener {
val searchActivity = activity as SearchActivity

return object : SearchActivityRecyclerviewListener {
override fun onPlaceClick(place: Place) {
searchActivity.handlePlaceClick(place)
}

override fun onLogDelBtnClick(logId: String) {
searchActivity.handleLogDelBtnClick(logId)
}
}
}

Choose a reason for hiding this comment

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

보통 Activity내부에서 사용하는 Listener까지 Hilt를 통해 주입받기 보다는 Activity내부에서 생성하는편입니다.
범용적으로 사용할 수 있는 클래스들 위주로만 제공해도 괜찮을것 같고, 혹시나 activity의 context가 필요한 상황이라면 @ActivityContext 를 붙여서 제공받는게 어떨까 합니다. (참고)

만약, 지금 SearchActivity 가 아닌 다른곳에서 주입받게 되면 해당 activity as SearchActivity 부분에서 에러가 발생하게 될것 같습니다.

}
11 changes: 10 additions & 1 deletion app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package campus.tech.kakao.map.di

import campus.tech.kakao.map.data.net.KakaoApi
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -14,6 +15,7 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object NetworkModule {
private const val BASE_URL = "https://dapi.kakao.com/"

@Singleton
@Provides
fun provideRetrofit(): Retrofit {
Expand All @@ -32,6 +34,13 @@ object NetworkModule {
@Singleton
@Provides
fun provideConfigInstance(): FirebaseRemoteConfig {
return FirebaseRemoteConfig.getInstance()
val firebaseInstance = FirebaseRemoteConfig.getInstance()
val configSettings = remoteConfigSettings {
minimumFetchIntervalInSeconds = 0
}

firebaseInstance.setConfigSettingsAsync(configSettings)

return firebaseInstance
}
}
17 changes: 12 additions & 5 deletions app/src/main/java/campus/tech/kakao/map/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package campus.tech.kakao.map.di

import android.content.Context
import android.content.SharedPreferences
import campus.tech.kakao.map.PlaceApplication
import campus.tech.kakao.map.data.*
import campus.tech.kakao.map.data.dao.PlaceDao
Expand All @@ -18,26 +19,32 @@ import javax.inject.Qualifier
import javax.inject.Singleton

@Module
@InstallIn(ViewModelComponent::class)
@InstallIn(SingletonComponent::class)
object ViewModelModule {
// SearchViewModel
@Singleton
@Provides
fun providePlaceRepository(
placeDao: PlaceDao, kakaoApi: KakaoApi): PlaceRepository {
return if (PlaceApplication.isNetworkActive()) {
@ApplicationContext context: Context,
placeDao: PlaceDao,
kakaoApi: KakaoApi
): PlaceRepository {
return if (PlaceApplication.isNetworkActive(context)) {
PlaceRemoteDataRepository(placeDao,kakaoApi)
} else {
PlaceLocalDataRepository(placeDao)
}
}

// MapViewModel
@Singleton
@Provides
fun provideLastVisitedPlaceManager(@ApplicationContext context: Context): LastVisitedPlaceManager{
return LastVisitedPlaceManager(context)
fun provideLastVisitedPlaceManager(sharedPreferences: SharedPreferences): LastVisitedPlaceManager{
return LastVisitedPlaceManager(sharedPreferences)
}

// SearchViewModel
@Singleton
@Provides
fun provideRemoteConfigRepository(remoteConfig: FirebaseRemoteConfig):RemoteConfigRepository{
return RemoteConfigRepository(remoteConfig)
Expand Down
Loading