fix(main): 오디오 알림 이동과 미니 플레이어를 연결한다
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user