fix(main): 오디오 알림 이동과 미니 플레이어를 연결한다

This commit is contained in:
2026-07-06 14:00:23 +09:00
parent 5b9b7eafa7
commit e3d81792bf
3 changed files with 235 additions and 2 deletions

View File

@@ -1,9 +1,12 @@
package kr.co.vividnext.sodalive.v2.main package kr.co.vividnext.sodalive.v2.main
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@@ -39,6 +42,7 @@ import kr.co.vividnext.sodalive.audition.AuditionActivity
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
@@ -72,6 +76,7 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
private var mediaControllerFuture: ListenableFuture<MediaController>? = null private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() } private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
private val audioContentReceiver = AudioContentReceiver()
private var playerStateJob: Job? = null private var playerStateJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -82,11 +87,16 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
trackAppLaunchIfNeeded() trackAppLaunchIfNeeded()
pushTokenUpdate() pushTokenUpdate()
handler.postDelayed({
if (!handleAudioNotificationRoute(intent) && isLoggedIn()) {
executeDeeplink(intent)
}
}, 1000)
if (isLoggedIn()) { if (isLoggedIn()) {
updatePidAndGaid() updatePidAndGaid()
getEventPopup() getEventPopup()
observePlayerState() observePlayerState()
handler.postDelayed({ executeDeeplink(intent) }, 1000)
} }
} }
@@ -114,14 +124,22 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
return return
} }
if (isLoggedIn()) { if (!handleAudioNotificationRoute(intent) && isLoggedIn()) {
executeDeeplink(intent) executeDeeplink(intent)
} }
} }
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
getMemberInfo() getMemberInfo()
val intentFilter = IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(audioContentReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(audioContentReceiver, intentFilter)
}
startService( startService(
Intent(this, AudioContentPlayService::class.java).apply { Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.INIT.name action = AudioContentPlayService.MusicAction.INIT.name
@@ -129,6 +147,11 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
) )
} }
override fun onPause() {
unregisterReceiver(audioContentReceiver)
super.onPause()
}
override fun onDestroy() { override fun onDestroy() {
deInitMiniPlayer() deInitMiniPlayer()
playerStateJob?.cancel() playerStateJob?.cancel()
@@ -405,6 +428,39 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
playerFragment.show(supportFragmentManager, playerFragment.tag) playerFragment.show(supportFragmentManager, playerFragment.tag)
} }
private fun handleAudioNotificationRoute(intent: Intent): Boolean {
val route = intent.getStringExtra(EXTRA_AUDIO_NOTIFICATION_ROUTE) ?: return false
intent.removeExtra(EXTRA_AUDIO_NOTIFICATION_ROUTE)
return when (route) {
ROUTE_AUDIO_PLAYER -> {
if (isLoggedIn()) {
showPlayerFragment()
} else {
showLoginActivity()
}
true
}
ROUTE_AUDIO_DETAIL -> {
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
intent.removeExtra(Constants.EXTRA_AUDIO_CONTENT_ID)
if (contentId > 0 && isLoggedIn()) {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
} else if (!isLoggedIn()) {
showLoginActivity()
}
contentId > 0
}
else -> false
}
}
private fun checkPermissions() { private fun checkPermissions() {
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO) val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
@@ -734,8 +790,90 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10 return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
} }
inner class AudioContentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
val title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE)
val nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME)
val coverImageUrl = intent?.getStringExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL
)
val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false)
val isShowing = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_SHOWING, false)
if (isShowing == true) {
binding.clMiniPlayer.visibility = View.VISIBLE
if (contentId != null && contentId > 0) {
binding.clMiniPlayer.setOnClickListener {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
)
}
}
if (isPlaying == true) {
binding.ivPlayerPlayOrPause.setImageResource(R.drawable.ic_noti_pause)
binding.ivPlayerPlayOrPause.setOnClickListener {
startService(
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.PAUSE.name
}
)
}
} else {
binding.ivPlayerPlayOrPause.setImageResource(R.drawable.ic_noti_play)
binding.ivPlayerPlayOrPause.setOnClickListener {
startService(
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.PLAY.name
}
)
}
}
binding.ivPlayerStop.setOnClickListener {
startService(
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.STOP.name
}
)
}
if (!title.isNullOrBlank()) {
binding.tvPlayerTitle.text = title
}
if (!nickname.isNullOrBlank()) {
binding.tvPlayerNickname.text = nickname
}
if (!coverImageUrl.isNullOrBlank()) {
binding.ivPlayerCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
}
} else {
handler.post {
binding.ivPlayerPlayOrPause.setImageResource(0)
binding.ivPlayerCover.setImageResource(0)
binding.tvPlayerTitle.text = ""
binding.tvPlayerNickname.text = ""
binding.clMiniPlayer.visibility = View.GONE
binding.ivPlayerPlayOrPause.setOnClickListener {}
}
}
}
}
companion object { companion object {
private const val EXTRA_CHAT_FILTER: String = "extra_chat_filter" private const val EXTRA_CHAT_FILTER: String = "extra_chat_filter"
const val EXTRA_AUDIO_NOTIFICATION_ROUTE: String = "extra_audio_notification_route"
const val ROUTE_AUDIO_PLAYER: String = "audio_player"
const val ROUTE_AUDIO_DETAIL: String = "audio_detail"
fun newChatDmIntent(context: Context): Intent { fun newChatDmIntent(context: Context): Intent {
return Intent(context, MainV2Activity::class.java).apply { return Intent(context, MainV2Activity::class.java).apply {

View File

@@ -91,6 +91,34 @@ class MainV2ActivitySourceTest {
assertBefore(openContentSource, "changeFragment(MainV2Tab.CONTENT)", "?.selectAllTab(type, sort)") assertBefore(openContentSource, "changeFragment(MainV2Tab.CONTENT)", "?.selectAllTab(type, sort)")
} }
@Test
fun `MainV2Activity는 기존 오디오 서비스 브로드캐스트로 미니 플레이어를 표시한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
assertTrue(source.contains("import android.content.BroadcastReceiver"))
assertTrue(source.contains("import android.content.IntentFilter"))
assertTrue(source.contains("private val audioContentReceiver = AudioContentReceiver()"))
assertTrue(source.contains("IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)"))
assertTrue(source.contains("registerReceiver(audioContentReceiver, intentFilter"))
assertTrue(source.contains("unregisterReceiver(audioContentReceiver)"))
assertTrue(source.contains("inner class AudioContentReceiver : BroadcastReceiver()"))
val receiverSource = source.substringFrom("inner class AudioContentReceiver : BroadcastReceiver()")
assertTrue(receiverSource.contains("Constants.EXTRA_AUDIO_CONTENT_SHOWING"))
assertTrue(receiverSource.contains("Constants.EXTRA_AUDIO_CONTENT_ID"))
assertTrue(receiverSource.contains("Constants.EXTRA_AUDIO_CONTENT_TITLE"))
assertTrue(receiverSource.contains("Constants.EXTRA_NICKNAME"))
assertTrue(receiverSource.contains("Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL"))
assertTrue(receiverSource.contains("Constants.EXTRA_AUDIO_CONTENT_PLAYING"))
assertTrue(receiverSource.contains("binding.clMiniPlayer.visibility = View.VISIBLE"))
assertTrue(receiverSource.contains("AudioContentDetailActivity::class.java"))
assertTrue(receiverSource.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)"))
assertTrue(receiverSource.contains("AudioContentPlayService.MusicAction.PAUSE.name"))
assertTrue(receiverSource.contains("AudioContentPlayService.MusicAction.PLAY.name"))
assertTrue(receiverSource.contains("AudioContentPlayService.MusicAction.STOP.name"))
assertTrue(receiverSource.contains("binding.clMiniPlayer.visibility = View.GONE"))
}
private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) { private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) {
val beforeIndex = source.indexOf(expectedBefore) val beforeIndex = source.indexOf(expectedBefore)
val afterIndex = source.indexOf(expectedAfter, beforeIndex) val afterIndex = source.indexOf(expectedAfter, beforeIndex)

View File

@@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.v2.main
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class MainV2AudioNotificationRouteSourceTest {
private val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt"
).readText()
@Test
fun `오디오 알림 route는 플레이어와 상세페이지로 분기한다`() {
assertTrue(source.contains("const val EXTRA_AUDIO_NOTIFICATION_ROUTE"))
assertTrue(source.contains("const val ROUTE_AUDIO_PLAYER"))
assertTrue(source.contains("const val ROUTE_AUDIO_DETAIL"))
assertTrue(source.contains("handleAudioNotificationRoute"))
assertTrue(source.contains("showPlayerFragment()"))
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("Constants.EXTRA_AUDIO_CONTENT_ID"))
assertTrue(source.contains("removeExtra(EXTRA_AUDIO_NOTIFICATION_ROUTE)"))
}
@Test
fun `오디오 알림 route는 채팅 필터 이후 딥링크 이전에 처리된다`() {
val onNewIntentSource = source.substringFrom("override fun onNewIntent(intent: Intent)")
assertBefore(onNewIntentSource, "if (intent.hasExtra(EXTRA_CHAT_FILTER))", "handleAudioNotificationRoute(intent)")
assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "isLoggedIn()")
assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)")
val onCreateSource = source.substringFrom("override fun onCreate(savedInstanceState: Bundle?)")
assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "isLoggedIn()")
assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)")
}
@Test
fun `오디오 상세 route는 유효한 content id만 상세페이지로 이동한다`() {
val routeSource = source.substringFrom("private fun handleAudioNotificationRoute(intent: Intent): Boolean")
assertTrue(routeSource.contains("ROUTE_AUDIO_DETAIL ->"))
assertTrue(routeSource.contains("val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)"))
assertTrue(routeSource.contains("intent.removeExtra(Constants.EXTRA_AUDIO_CONTENT_ID)"))
assertTrue(routeSource.contains("if (contentId > 0"))
assertBefore(routeSource, "if (contentId > 0", "AudioContentDetailActivity::class.java")
}
private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) {
val beforeIndex = source.indexOf(expectedBefore)
val afterIndex = source.indexOf(expectedAfter, beforeIndex)
assertTrue("Missing source: $expectedBefore", beforeIndex >= 0)
assertTrue("Missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex)
}
private fun String.substringFrom(marker: String): String {
val startIndex = indexOf(marker)
assertTrue("Missing function: $marker", startIndex >= 0)
val nextFunctionIndex = indexOf("\n private fun ", startIndex + marker.length).takeIf { it >= 0 } ?: length
return substring(startIndex, nextFunctionIndex)
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath")
}
}