diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a3e847a..65777640 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,9 @@ + diff --git a/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt b/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt index 3db43455..259a0fba 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt @@ -1,12 +1,24 @@ package com.sesac.developer_study_platform +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.graphics.Bitmap +import android.os.Build import android.util.Log +import androidx.core.app.NotificationCompat +import com.bumptech.glide.Glide +import com.google.firebase.Firebase +import com.google.firebase.auth.auth import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.sesac.developer_study_platform.data.source.local.FcmTokenRepository +import com.sesac.developer_study_platform.ui.main.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.random.Random class MyFirebaseMessagingService : FirebaseMessagingService() { @@ -21,18 +33,76 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { } override fun onMessageReceived(remoteMessage: RemoteMessage) { - // TODO(developer): Handle FCM messages here. - // Not getting messages here? See why this may be: https://goo.gl/39bRNJ - Log.d("fcm", "From: ${remoteMessage.from}") + kotlin.runCatching { + if (remoteMessage.data.isNotEmpty()) { + val uid = remoteMessage.data.getValue("uid") - // Check if message contains a data payload. - if (remoteMessage.data.isNotEmpty()) { - Log.d("fcm", "Message data payload: ${remoteMessage.data}") + createNotificationChannel() + if (uid != Firebase.auth.uid) { + sendNotification(remoteMessage.data) + } + } + }.onFailure { + Log.e("MyFirebaseMessagingService-onMessageReceived", it.message ?: "error occurred.") } + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val id = getString(R.string.default_notification_channel_id) + val name = getString(R.string.app_name) + val descriptionText = getString(R.string.app_name) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(id, name, importance).apply { + description = descriptionText + } + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.createNotificationChannel(channel) + } + } + + + private fun sendNotification(data: Map) { + kotlin.runCatching { + val randomNumber = Random.nextInt() + val builder = getNotificationBuilder(data, randomNumber) + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.notify(randomNumber, builder.build()) + }.onFailure { + Log.e("MyFirebaseMessagingService-sendNotification", it.message ?: "error occurred.") + } + } + + private fun getNotificationBuilder( + data: Map, + randomNumber: Int + ): NotificationCompat.Builder { + return NotificationCompat.Builder(this, getString(R.string.default_notification_channel_id)) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setLargeIcon(getLargeIcon(data.getValue("imageUrl"))) + .setColor(getColor(R.color.white)) + .setContentTitle(data.getValue("title")) + .setContentText(data.getValue("text")) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(getPendingIntent(data, randomNumber)) + .setAutoCancel(true) + } + + private fun getLargeIcon(imageUrl: String): Bitmap { + return Glide.with(this) + .asBitmap() + .load(imageUrl) + .submit() + .get() + } - // Check if message contains a notification payload. - remoteMessage.notification?.let { - Log.d("fcm", "Message Notification Body: ${it.body}") + private fun getPendingIntent(data: Map, randomNumber: Int): PendingIntent? { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra("sid", data.getValue("sid")) } + return PendingIntent.getActivity(this, randomNumber, intent, PendingIntent.FLAG_IMMUTABLE) } } \ No newline at end of file diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt b/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt index 896cb275..e8912c49 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt @@ -10,5 +10,15 @@ data class FcmMessage( @Serializable data class FcmMessageData( val token: String = "", - val data: Map = mapOf(), + val data: FcmMessageContent, + val android: Map = mapOf() +) + +@Serializable +data class FcmMessageContent( + val uid: String? = "", + val sid: String? = "", + val title: String = "", + val text: String = "", + val imageUrl: String = "", ) \ No newline at end of file diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt index 6644ac11..7f6d2cd7 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt @@ -106,6 +106,14 @@ class StudyRepository { return studyService.getRegistrationIdList(sid) } + suspend fun deleteRegistrationId(sid: String, registrationId: String) { + studyService.deleteRegistrationId(sid, registrationId) + } + + suspend fun deleteNotificationKey(sid: String) { + studyService.deleteNotificationKey(sid) + } + fun getMessageList(sid: String): Flow> = flow { while (true) { kotlin.runCatching { diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyService.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyService.kt index 306fdfeb..d973d9f8 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyService.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyService.kt @@ -172,6 +172,17 @@ interface StudyService { @Path("sid") sid: String ): Map + @DELETE("studies/{sid}/registrationIds/{registrationId}.json") + suspend fun deleteRegistrationId( + @Path("sid") sid: String, + @Path("registrationId") registrationId: String + ) + + @DELETE("studies/{sid}/notificationKey.json") + suspend fun deleteNotificationKey( + @Path("sid") sid: String + ) + companion object { private const val BASE_URL = BuildConfig.FIREBASE_BASE_URL private val contentType = "application/json".toMediaType() diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/common/NotificationPermissionDialogViewModel.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/common/NotificationPermissionDialogViewModel.kt index 94dcf0e8..ddfd93db 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/common/NotificationPermissionDialogViewModel.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/common/NotificationPermissionDialogViewModel.kt @@ -26,54 +26,41 @@ class NotificationPermissionDialogViewModel(private val fcmTokenRepository: FcmT val moveToMessageEvent: LiveData> = _moveToMessageEvent fun checkNotificationKey(sid: String) { - viewModelScope.launch { - if (getNotificationKey(sid).isNullOrEmpty()) { - createNotificationKey(sid) - } else if (isRegistrationId(sid, fcmTokenRepository.getToken().first())) { - updateStudyGroup(sid) - } - } - } - - private fun createNotificationKey(sid: String) { viewModelScope.launch { val token = fcmTokenRepository.getToken().first() kotlin.runCatching { - fcmRepository.updateStudyGroup(StudyGroup("create", sid, listOf(token))) - }.onSuccess { - addNotificationKey(sid, it.values.first()) + val notificationKey = getNotificationKey(sid) + if (!notificationKey.isNullOrEmpty()) { + updateStudyGroup(sid, token, notificationKey) + } else { + createNotificationKey(sid, token) + } }.onFailure { Log.e( - "NotificationPermissionDialogViewModel-createNotificationKey", + "NotificationPermissionDialogViewModel-checkNotificationKey", it.message ?: "error occurred." ) } } } - private fun addNotificationKey(sid: String, notificationKey: String) { - viewModelScope.launch { + private suspend fun getNotificationKey(sid: String): String? { + return viewModelScope.async { kotlin.runCatching { - studyRepository.addNotificationKey(sid, notificationKey) - }.onSuccess { - addRegistrationId(sid, fcmTokenRepository.getToken().first()) + studyRepository.getNotificationKey(sid) }.onFailure { Log.e( - "NotificationPermissionDialogViewModel-addNotificationKey", + "NotificationPermissionDialogViewModel-getNotificationKey", it.message ?: "error occurred." ) - } - } + }.getOrNull() + }.await() } - private fun updateStudyGroup(sid: String) { + private fun updateStudyGroup(sid: String, token: String, notificationKey: String) { viewModelScope.launch { - val token = fcmTokenRepository.getToken().first() kotlin.runCatching { - val notificationKey = getNotificationKey(sid) - if (!notificationKey.isNullOrEmpty()) { - fcmRepository.updateStudyGroup(StudyGroup("add", sid, listOf(token), notificationKey)) - } + fcmRepository.updateStudyGroup(StudyGroup("add", sid, listOf(token), notificationKey)) }.onSuccess { addRegistrationId(sid, token) }.onFailure { @@ -85,47 +72,49 @@ class NotificationPermissionDialogViewModel(private val fcmTokenRepository: FcmT } } - private suspend fun getNotificationKey(sid: String): String? { - return viewModelScope.async { + private fun createNotificationKey(sid: String, token: String) { + viewModelScope.launch { kotlin.runCatching { - studyRepository.getNotificationKey(sid) + fcmRepository.updateStudyGroup(StudyGroup("create", sid, listOf(token))) + }.onSuccess { + addNotificationKey(sid, it.values.first()) }.onFailure { Log.e( - "NotificationPermissionDialogViewModel-getNotificationKey", + "NotificationPermissionDialogViewModel-createNotificationKey", it.message ?: "error occurred." ) - }.getOrNull() - }.await() + } + } } - private fun addRegistrationId(sid: String, registrationId: String) { + private fun addNotificationKey(sid: String, notificationKey: String) { viewModelScope.launch { kotlin.runCatching { - studyRepository.addRegistrationId(sid, registrationId) + studyRepository.addNotificationKey(sid, notificationKey) }.onSuccess { - _checkNotificationKeyEvent.value = Event(Unit) + addRegistrationId(sid, fcmTokenRepository.getToken().first()) }.onFailure { Log.e( - "NotificationPermissionDialogViewModel-addRegistrationId", + "NotificationPermissionDialogViewModel-addNotificationKey", it.message ?: "error occurred." ) } } } - private suspend fun isRegistrationId(sid: String, registrationId: String): Boolean { - return viewModelScope.async { + private fun addRegistrationId(sid: String, registrationId: String) { + viewModelScope.launch { kotlin.runCatching { - studyRepository.getRegistrationIdList(sid) - }.map { - it.containsKey(registrationId) + studyRepository.addRegistrationId(sid, registrationId) + }.onSuccess { + _checkNotificationKeyEvent.value = Event(Unit) }.onFailure { Log.e( - "NotificationPermissionDialogViewModel-isRegistrationId", + "NotificationPermissionDialogViewModel-addRegistrationId", it.message ?: "error occurred." ) - }.getOrDefault(false) - }.await() + } + } } fun moveToMessage(sid: String) { diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogFragment.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogFragment.kt index 442f272c..481336c1 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogFragment.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogFragment.kt @@ -31,7 +31,7 @@ class JoinStudyDialogFragment : DialogFragment() { private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { - updateStudyGroup() + checkNotificationKey() } else { Toast.makeText(context, getString(R.string.all_notification_info), Toast.LENGTH_SHORT).show() viewModel.moveToMessage(args.study.sid) @@ -106,7 +106,7 @@ class JoinStudyDialogFragment : DialogFragment() { requireContext(), Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED -> { - updateStudyGroup() + checkNotificationKey() } shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { @@ -118,13 +118,13 @@ class JoinStudyDialogFragment : DialogFragment() { } } } else { - updateStudyGroup() + checkNotificationKey() } } - private fun updateStudyGroup() { - viewModel.updateStudyGroup(args.study.sid) - viewModel.updateStudyGroupEvent.observe( + private fun checkNotificationKey() { + viewModel.checkNotificationKey(args.study.sid) + viewModel.checkNotificationKeyEvent.observe( viewLifecycleOwner, EventObserver { viewModel.moveToMessage(args.study.sid) diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogViewModel.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogViewModel.kt index fc377626..100684b5 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogViewModel.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/detail/JoinStudyDialogViewModel.kt @@ -24,8 +24,8 @@ class JoinStudyDialogViewModel(private val fcmTokenRepository: FcmTokenRepositor private val _addUserStudyEvent: MutableLiveData> = MutableLiveData() val addUserStudyEvent: LiveData> = _addUserStudyEvent - private val _updateStudyGroupEvent: MutableLiveData> = MutableLiveData() - val updateStudyGroupEvent: LiveData> = _updateStudyGroupEvent + private val _checkNotificationKeyEvent: MutableLiveData> = MutableLiveData() + val checkNotificationKeyEvent: LiveData> = _checkNotificationKeyEvent private val _moveToMessageEvent: MutableLiveData> = MutableLiveData() val moveToMessageEvent: LiveData> = _moveToMessageEvent @@ -64,14 +64,26 @@ class JoinStudyDialogViewModel(private val fcmTokenRepository: FcmTokenRepositor } } - fun updateStudyGroup(sid: String) { + fun checkNotificationKey(sid: String) { viewModelScope.launch { val token = fcmTokenRepository.getToken().first() kotlin.runCatching { val notificationKey = getNotificationKey(sid) if (!notificationKey.isNullOrEmpty()) { - fcmRepository.updateStudyGroup(StudyGroup("add", sid, listOf(token), notificationKey)) + updateStudyGroup(sid, token, notificationKey) + } else { + createNotificationKey(sid, token) } + }.onFailure { + Log.e("JoinStudyDialogViewModel-checkNotificationKey", it.message ?: "error occurred.") + } + } + } + + private fun updateStudyGroup(sid: String, token: String, notificationKey: String) { + viewModelScope.launch { + kotlin.runCatching { + fcmRepository.updateStudyGroup(StudyGroup("add", sid, listOf(token), notificationKey)) }.onSuccess { addRegistrationId(sid, token) }.onFailure { @@ -90,12 +102,36 @@ class JoinStudyDialogViewModel(private val fcmTokenRepository: FcmTokenRepositor }.await() } + private fun createNotificationKey(sid: String, token: String) { + viewModelScope.launch { + kotlin.runCatching { + fcmRepository.updateStudyGroup(StudyGroup("create", sid, listOf(token))) + }.onSuccess { + addNotificationKey(sid, it.values.first()) + }.onFailure { + Log.e("JoinStudyDialogViewModel-createNotificationKey", it.message ?: "error occurred.") + } + } + } + + private fun addNotificationKey(sid: String, notificationKey: String) { + viewModelScope.launch { + kotlin.runCatching { + studyRepository.addNotificationKey(sid, notificationKey) + }.onSuccess { + addRegistrationId(sid, fcmTokenRepository.getToken().first()) + }.onFailure { + Log.e("JoinStudyDialogViewModel-addNotificationKey", it.message ?: "error occurred.") + } + } + } + private fun addRegistrationId(sid: String, registrationId: String) { viewModelScope.launch { kotlin.runCatching { studyRepository.addRegistrationId(sid, registrationId) }.onSuccess { - _updateStudyGroupEvent.value = Event(Unit) + _checkNotificationKeyEvent.value = Event(Unit) }.onFailure { Log.e("JoinStudyDialogViewModel-addRegistrationId", it.message ?: "error occurred.") } diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/main/MainActivity.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/main/MainActivity.kt index ccb2a0b8..d01d5dca 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/main/MainActivity.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/main/MainActivity.kt @@ -1,6 +1,7 @@ package com.sesac.developer_study_platform.ui.main import android.animation.ObjectAnimator +import android.content.Intent import android.os.Bundle import android.view.View import android.view.animation.AnticipateInterpolator @@ -18,20 +19,29 @@ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var splashScreen: SplashScreen + private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { splashScreen = installSplashScreen() super.onCreate(savedInstanceState) - startSplash() +// startSplash() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val navHostFragment = supportFragmentManager.findFragmentById(R.id.fcv) as NavHostFragment - val navController = navHostFragment.navController + navController = navHostFragment.navController binding.bnv.setupWithNavController(navController) hideBottomNavigationView(navController) + + setNewIntent(intent) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + setNewIntent(intent) } private fun startSplash() { @@ -65,4 +75,14 @@ class MainActivity : AppCompatActivity() { } } } + + private fun setNewIntent(intent: Intent?) { + if (intent != null) { + val sid = intent.getStringExtra("sid") + if (!sid.isNullOrEmpty()) { + val action = MainActivityDirections.actionGlobalToMessage(sid) + navController.navigate(action) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageFragment.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageFragment.kt index f93557cb..a071ac72 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageFragment.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageFragment.kt @@ -1,13 +1,18 @@ package com.sesac.developer_study_platform.ui.message +import android.Manifest +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment @@ -20,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView import com.sesac.developer_study_platform.EventObserver import com.sesac.developer_study_platform.R import com.sesac.developer_study_platform.data.StudyMember +import com.sesac.developer_study_platform.data.source.local.FcmTokenRepository import com.sesac.developer_study_platform.data.source.remote.StudyService import com.sesac.developer_study_platform.databinding.FragmentMessageBinding import com.sesac.developer_study_platform.util.isNetworkConnected @@ -32,7 +38,9 @@ class MessageFragment : Fragment() { private val binding get() = _binding!! private val args by navArgs() private val messageAdapter = MessageAdapter() - private val viewModel by viewModels() + private val viewModel by viewModels { + MessageViewModel.create(FcmTokenRepository(requireContext())) + } private val service = StudyService.create() private var isBottom = true private val pickMultipleMedia = @@ -45,6 +53,14 @@ class MessageFragment : Fragment() { findNavController().navigate(action) } }) + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + addRegistrationId() + } else { + Toast.makeText(context, getString(R.string.all_notification_info), Toast.LENGTH_SHORT).show() + } + } override fun onCreateView( inflater: LayoutInflater, @@ -70,6 +86,8 @@ class MessageFragment : Fragment() { setSendButton() setExitButton() setExitButtonVisibility() + loadNotificationButtonState() + setNotificationButton() setNavigation() binding.isNetworkConnected = isNetworkConnected(requireContext()) } @@ -131,7 +149,7 @@ class MessageFragment : Fragment() { } private fun sendImage(uriList: List, timestamp: Long) { - viewModel.sendImage(args.studyId, uriList, timestamp) + viewModel.sendImage(args.studyId, uriList, timestamp, getString(R.string.chat_room_last_message_image)) viewModel.addMessageEvent.observe( viewLifecycleOwner, EventObserver { @@ -210,9 +228,69 @@ class MessageFragment : Fragment() { ) } + private fun loadNotificationButtonState() { + lifecycleScope.launch { + binding.ivNotification.isSelected = viewModel.isRegistrationId(args.studyId) + } + } + + private fun setNotificationButton() { + binding.ivNotification.setOnClickListener { + if (binding.ivNotification.isSelected) { + deleteRegistrationId() + } else { + askNotificationPermission() + } + } + } + + private fun deleteRegistrationId() { + viewModel.updateStudyGroup("remove", args.studyId) + viewModel.deleteRegistrationIdEvent.observe( + viewLifecycleOwner, + EventObserver { + binding.ivNotification.isSelected = false + } + ) + } + + private fun askNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED -> { + addRegistrationId() + } + + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { + viewModel.moveToNotificationPermissionDialog(args.studyId) + } + + else -> { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } else { + addRegistrationId() + } + } + + private fun addRegistrationId() { + viewModel.updateStudyGroup("add", args.studyId) + viewModel.addRegistrationIdEvent.observe( + viewLifecycleOwner, + EventObserver { + binding.ivNotification.isSelected = true + } + ) + } + private fun setNavigation() { moveToBack() moveToExitDialog() + moveToNotificationPermissionDialog() } private fun moveToBack() { @@ -234,6 +312,16 @@ class MessageFragment : Fragment() { ) } + private fun moveToNotificationPermissionDialog() { + viewModel.moveToNotificationPermissionDialogEvent.observe( + viewLifecycleOwner, + EventObserver { + val action = MessageFragmentDirections.actionGlobalToNotificationPermissionDialog(it) + findNavController().navigate(action) + } + ) + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageViewModel.kt b/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageViewModel.kt index 58cff01d..74b00b20 100644 --- a/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageViewModel.kt +++ b/app/src/main/java/com/sesac/developer_study_platform/ui/message/MessageViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import com.google.firebase.Firebase import com.google.firebase.auth.auth import com.google.firebase.storage.storage @@ -13,16 +15,21 @@ import com.sesac.developer_study_platform.Event import com.sesac.developer_study_platform.StudyApplication.Companion.fcmRepository import com.sesac.developer_study_platform.StudyApplication.Companion.studyRepository import com.sesac.developer_study_platform.data.FcmMessage +import com.sesac.developer_study_platform.data.FcmMessageContent import com.sesac.developer_study_platform.data.FcmMessageData import com.sesac.developer_study_platform.data.Message +import com.sesac.developer_study_platform.data.StudyGroup import com.sesac.developer_study_platform.data.StudyUser +import com.sesac.developer_study_platform.data.source.local.FcmTokenRepository import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -class MessageViewModel : ViewModel() { +class MessageViewModel(private val fcmTokenRepository: FcmTokenRepository) : ViewModel() { private val uid = Firebase.auth.uid + private var studyName = "" private val _studyNameEvent: MutableLiveData> = MutableLiveData() val studyNameEvent: LiveData> = _studyNameEvent @@ -36,12 +43,22 @@ class MessageViewModel : ViewModel() { private val _addUriListEvent: MutableLiveData>> = MutableLiveData() val addUriListEvent: LiveData>> = _addUriListEvent + private val _deleteRegistrationIdEvent: MutableLiveData> = MutableLiveData() + val deleteRegistrationIdEvent: LiveData> = _deleteRegistrationIdEvent + + private val _addRegistrationIdEvent: MutableLiveData> = MutableLiveData() + val addRegistrationIdEvent: LiveData> = _addRegistrationIdEvent + private val _moveToBackEvent: MutableLiveData> = MutableLiveData() val moveToBackEvent: LiveData> = _moveToBackEvent private val _moveToExitDialogEvent: MutableLiveData> = MutableLiveData() val moveToExitDialogEvent: LiveData> = _moveToExitDialogEvent + private val _moveToNotificationPermissionDialogEvent: MutableLiveData> = MutableLiveData() + val moveToNotificationPermissionDialogEvent: LiveData> = + _moveToNotificationPermissionDialogEvent + private val _isAdminEvent: MutableLiveData> = MutableLiveData(Event(false)) val isAdminEvent: LiveData> = _isAdminEvent @@ -51,6 +68,7 @@ class MessageViewModel : ViewModel() { studyRepository.getStudyName(sid) }.onSuccess { _studyNameEvent.value = Event(it) + studyName = it }.onFailure { Log.e("MessageViewModel-loadStudyName", it.message ?: "error occurred.") } @@ -119,7 +137,7 @@ class MessageViewModel : ViewModel() { }.await() } - fun sendImage(sid: String, uriList: List, timestamp: Long) { + fun sendImage(sid: String, uriList: List, timestamp: Long, text: String) { viewModelScope.launch { val message = getMessage(uid, sid).copy( images = uriList.map { it.toString() }, @@ -131,7 +149,7 @@ class MessageViewModel : ViewModel() { }.onSuccess { _addMessageEvent.value = Event(Unit) updateLastMessage(sid, message) - sendNotification(sid, message.message) + sendNotification(sid, getFcmMessageContent(sid, message, text)) }.onFailure { Log.e("MessageViewModel-sendImage", it.message ?: "error occurred.") } @@ -146,13 +164,19 @@ class MessageViewModel : ViewModel() { }.onSuccess { _addMessageEvent.value = Event(Unit) updateLastMessage(sid, message) - sendNotification(sid, message.message) + sendNotification(sid, getFcmMessageContent(sid, message, text)) }.onFailure { Log.e("MessageViewModel-sendMessage", it.message ?: "error occurred.") } } } + private fun getFcmMessageContent(sid: String, message: Message, text: String): FcmMessageContent { + val userId = message.studyUser?.userId + val imageUrl = message.studyUser?.image.toString() + return FcmMessageContent(uid, sid, studyName, "$userId : $text", imageUrl) + } + fun loadMessageList(sid: String) { viewModelScope.launch(SupervisorJob()) { studyRepository.getMessageList(sid).collect { @@ -261,16 +285,14 @@ class MessageViewModel : ViewModel() { } } - private fun sendNotification(sid: String, message: String) { + private fun sendNotification(sid: String, fcmMessageContent: FcmMessageContent) { viewModelScope.launch { kotlin.runCatching { val notificationKey = getNotificationKey(sid) if (!notificationKey.isNullOrEmpty()) { - fcmRepository.sendNotification( - FcmMessage( - FcmMessageData(notificationKey, mapOf("message" to message)) - ) - ) + val fcmMessageData = + FcmMessageData(notificationKey, fcmMessageContent, mapOf("direct_boot_ok" to true)) + fcmRepository.sendNotification(FcmMessage(fcmMessageData)) } }.onFailure { Log.e("MessageViewModel-sendNotification", it.message ?: "error occurred.") @@ -288,6 +310,111 @@ class MessageViewModel : ViewModel() { }.await() } + fun updateStudyGroup(operation: String, sid: String) { + viewModelScope.launch { + val token = fcmTokenRepository.getToken().first() + kotlin.runCatching { + val notificationKey = getNotificationKey(sid) + if (!notificationKey.isNullOrEmpty()) { + fcmRepository.updateStudyGroup(StudyGroup(operation, sid, listOf(token), notificationKey)) + } else { + createNotificationKey(sid, token) + } + }.onSuccess { + if (isRegistrationId(sid)) { + deleteRegistrationId(sid, token) + } else { + addRegistrationId(sid, token) + } + }.onFailure { + Log.e("MessageViewModel-updateStudyGroup", it.message ?: "error occurred.") + } + } + } + + private fun createNotificationKey(sid: String, token: String) { + viewModelScope.launch { + kotlin.runCatching { + fcmRepository.updateStudyGroup(StudyGroup("create", sid, listOf(token))) + }.onSuccess { + addNotificationKey(sid, it.values.first()) + }.onFailure { + Log.e("MessageViewModel-createNotificationKey", it.message ?: "error occurred.") + } + } + } + + private fun addNotificationKey(sid: String, notificationKey: String) { + viewModelScope.launch { + kotlin.runCatching { + studyRepository.addNotificationKey(sid, notificationKey) + }.onFailure { + Log.e("MessageViewModel-addNotificationKey", it.message ?: "error occurred.") + } + } + } + + suspend fun isRegistrationId(sid: String): Boolean { + return viewModelScope.async { + kotlin.runCatching { + studyRepository.getRegistrationIdList(sid) + }.map { + it.containsKey(fcmTokenRepository.getToken().first()) + }.onFailure { + Log.e("MessageViewModel-isRegistrationId", it.message ?: "error occurred.") + }.getOrDefault(false) + }.await() + } + + private fun deleteRegistrationId(sid: String, registrationId: String) { + viewModelScope.launch { + kotlin.runCatching { + studyRepository.deleteRegistrationId(sid, registrationId) + }.onSuccess { + if (isRegistrationIdListEmpty(sid)) { + deleteNotificationKey(sid) + } + _deleteRegistrationIdEvent.value = Event(Unit) + }.onFailure { + Log.e("MessageViewModel-deleteRegistrationId", it.message ?: "error occurred.") + } + } + } + + private suspend fun isRegistrationIdListEmpty(sid: String): Boolean { + return viewModelScope.async { + kotlin.runCatching { + studyRepository.getRegistrationIdList(sid) + }.map { + it.isEmpty() + }.onFailure { + Log.e("MessageViewModel-isRegistrationIdListEmpty", it.message ?: "error occurred.") + }.getOrDefault(true) + }.await() + } + + private fun deleteNotificationKey(sid: String) { + viewModelScope.launch { + kotlin.runCatching { + studyRepository.deleteNotificationKey(sid) + }.onFailure { + Log.e("MessageViewModel-deleteNotificationKey", it.message ?: "error occurred.") + } + } + } + + private fun addRegistrationId(sid: String, registrationId: String) { + viewModelScope.launch { + kotlin.runCatching { + studyRepository.addRegistrationId(sid, registrationId) + }.onSuccess { + _addRegistrationIdEvent.value = Event(Unit) + }.onFailure { + Log.e("MessageViewModel-addRegistrationId", it.message ?: "error occurred.") + } + } + } + fun moveToBack() { _moveToBackEvent.value = Event(Unit) } @@ -295,4 +422,16 @@ class MessageViewModel : ViewModel() { fun moveToExitDialog(sid: String) { _moveToExitDialogEvent.value = Event(sid) } + + fun moveToNotificationPermissionDialog(sid: String) { + _moveToNotificationPermissionDialogEvent.value = Event(sid) + } + + companion object { + fun create(fcmTokenRepository: FcmTokenRepository) = viewModelFactory { + initializer { + MessageViewModel(fcmTokenRepository) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..a224bdbd --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification_off.xml b/app/src/main/res/drawable/ic_notification_off.xml new file mode 100644 index 00000000..e5a7568a --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_notification.xml b/app/src/main/res/drawable/selector_notification.xml new file mode 100644 index 00000000..0744daa9 --- /dev/null +++ b/app/src/main/res/drawable/selector_notification.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index 2e4901cc..e47c2cd4 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -134,6 +134,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + 스터디 멤버가 퇴장하였습니다. 대화상대 채팅방 나가기 아이콘 + 알림 설정 북마크 모양의 아이콘 로그아웃