업데이트 후 달렸던 리뷰의 변화입니다.
다행히 이전 문제가 해결된 것 같습니다.
별점은 그대로 1점이네요..
하지만 다른 문제들이 계속해서 발생하였습니다.
다음은 발생한 비정상 종료 이벤트 목록입니다.
이전에 인앱 업데이트 기능을 구현해 뒀었고, 최근까지 문제가 생길 때마다 업데이트를 진행했었습니다.
업데이트 창이 자주 뜰 수록 사용자들이 불편을 느낄 것 같다는 생각이 들었습니다.
검색 할당량 문제로 인해 추후에 업데이트를 해야 했기 때문에, 그 때까지 기다려야 했습니다.
첫 번째 오류
exoPlayer의 객체가 초기화되지 않았을 때, 접근해서 생긴 문제입니다.
exoPlayer에 대한 흐름은 다음과 같습니다.
<Activity.kt>
class Activity: AppCompatActivity() {
lateinit var exoPlayer: ExoPlayer
// 서비스 바인딩
private val bindConnection = object: ServiceConnection{
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
// 서비스로 부터 exoPlayer 객체를 얻어 연결해주기
val b = p1 as VideoService.VideoServiceBinder
videoService = b.getService()
exoPlayer = b.getExoPlayerInstance()
}
override fun onServiceDisconnected(p0: ComponentName?) {
videoService = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindService(Intent(this, VideoService::class.java), bindConnection, BIND_AUTO_CREATE)
}
}
<VideoService.kt>
class VideoService: Service() {
lateinit var exoPlayer: ExoPlayer
override fun onCreate() {
super.onCreate()
exoPlayer = ExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.setSeekForwardIncrementMs(10000)
.setSeekBackIncrementMs(10000)
.build()
}
inner class VideoServiceBinder: Binder(){
fun getService(): VideoService {
return this@VideoService
}
fun getExoPlayerInstance() = exoPlayer
}
}
액티비티에 초기화되지 않은 exoPlayer 변수가 위치합니다.
서비스 실행 시 exoPlayer의 인스턴스를 생성하며, 액티비티는 서비스가 바인드 되었을 때 exoPlayer를 연결해 줍니다.
<PlayerFragment.kt>
class PlayerFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
initListener()
}
private fun initListener(){
playerView = binding.playerView
player = activity.exoPlayer
playerView.player = player
activity.videoService!!.playVideo(videoData)
}
}
그 후 비디오를 선택하면 playerFragment가 실행되며, PlayerView와 액티비티 내의 exoPlayer를 연결해 줍니다.
저는 사실 해당 오류가 왜 생기는지에 대한 확신이 없습니다.
서비스가 바인드 되기 전 playerFragment가 실행되었기 때문이라고 추측은 하고 있습니다.
하지만 서비스가 바인드 되는데 그렇게 느린 것도 아니고,
또 데이터를 받아와야지만 playerFragment를 실행시킬 수 있기 때문에,
playerFragment 실행시키는 시점이 서비스가 바인드 되는 시점보다 빠를 수가 없다고 생각했습니다.
여러 기기로 실험도 해봤고요..
일단 제가 아는 선에서 수정을 해봤습니다.
<Activity.kt>
val isServiceBound: MutableLiveData<Boolean> = MutableLiveData<Boolean>().apply { value = false }
private val bindConnection = object: ServiceConnection{
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
val b = p1 as VideoService.VideoServiceBinder
videoService = b.getService()
exoPlayer = b.getExoPlayerInstance()
isServiceBound.value = true
}
override fun onServiceDisconnected(p0: ComponentName?) {
videoService = null
isServiceBound.value = false
}
}
서비스가 바인드 되었는지 여부를 저장하는 LiveData 변수를 생성하였습니다.
<PlayerFragment.kt>
private fun initListener(){
playerView = binding.playerView
activity.isServiceBound.observe(viewLifecycleOwner) { isBound ->
if (isBound) {
activity.videoService?.initPlayerFragment(this)
player = activity.exoPlayer
playerView.player = player
activity.videoService!!.playVideo(videoData)
}
}
}
해당 변수를 observe 하여 bind 상태에 따라 exoPlayer의 사용 여부를 결정해 주었습니다.
업데이트를 해보고, 해결된 것이 맞는지 확인해봐야 할 것 같습니다.
두 번째 오류
뷰 바인딩 오류였습니다.
onDestroyView() 매소드에서 binding을 null 처리해줘야 하지만,
onDestroy() 매소드에 해뒀었습니다.
모두 수정해주었습니다.
세 번째 오류
android.app.ForegroundServiceStartNotAllowedException
처음 보는 예외였습니다.
안드로이드 9.0(API 레벨 28) 이상에서 발생할 수 있는 예외라고 합니다.
앱이 백그라운드 실행 중일 때 startForegroundService() 메서드를 통해 포그라운드 서비스를 시작하려 할 때 발생한다고 합니다.
startForegroundService() 매서드를 호출한 후, 빠른 시간 이내에 startForeground() 매서드를 호출해야 한다고 합니다.
<VideoService.kt>
exoPlayer.addListener(object: Player.Listener{
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState){
Player.STATE_READY -> {
if (exoPlayer.isPlaying){
requestAudioFocus()
}
playerFragment.settingBottomPlayButton()
startForegroundService()
}
../
}
fun startForegroundService() {
startForeground(NOTIFICATION_ID, createNotification())
}
exoPlayer에 리스너를 등록하는 부분입니다.
playbackState가 바뀔 때마다 startForegroundService() 매소드를 통해 Notification을 실행시키고 있습니다.
해당 매소드를 다음과 같이 수정했습니다.
fun startForegroundService() {
val serviceIntent = Intent(this, VideoService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
}
startForeground(NOTIFICATION_ID, createNotification())
}
API 레벨 26 이상에서만 startForeGroundService(intent) 매소드를 사용한 후,
startForeground()를 호출시키도록 수정했습니다.
네 번째 오류
검색에 드는 할당량 최적화를 위해, Room 라이브러리를 사용했었습니다.
외래 키로 설정된 칼럼의 값이 참조하고 있는 다른 테이블의 행에 존재하지 않는 값을 가질 때 발생하는 오류입니다.
해당 최적화 알고리즘은 나중에 포스팅을 올리겠습니다.
@Entity
data class CashedKeyword(
@PrimaryKey val searchKeyword: String,
@ColumnInfo val savedTime: Long
)
@Entity(
foreignKeys = [
ForeignKey(
entity = CashedKeyword::class,
parentColumns = arrayOf("searchKeyword"),
childColumns = arrayOf("keyWord"),
onDelete = ForeignKey.CASCADE
)
]
)
data class YoutubeCashedData(
@PrimaryKey(autoGenerate = true) val dataId: Int,
val searchVideoData: VideoData,
val keyWord: String
)
CashedKeyword 테이블과 YoutubeCashedData 테이블이 있습니다.
YoutubeCashedData 테이블이 CashedKeyWord 테이블의 searchKeyword를 외래키로 참조하고 있습니다.
@Insert (onConflict = OnConflictStrategy.IGNORE)
fun insertCashedData(vararg youtubeCashedData: YoutubeCashedData)
@Insert (onConflict = OnConflictStrategy.IGNORE)
fun insertKeyword(vararg cashedKeyword: CashedKeyword)
@Query("DELETE FROM CashedKeyword WHERE searchKeyword = (:searchKeyword)")
fun deleteAllBySearchKeyword(searchKeyword: String)
insert 및 delete 코드입니다.
<SearchResultFragment.kt>
if (list.size < videoDataList.size - 1){
cashedDataDao.deleteAllBySearchKeyword(searchWord)
val cashedKeyword = CashedKeyword(searchWord, System.currentTimeMillis())
cashedDataDao.insertKeyword(cashedKeyword)
for (index in 0 until videoDataList.size - 1){
val youtubeCashedData = YoutubeCashedData(0, videoDataList[index], searchWord)
cashedDataDao.insertCashedData(youtubeCashedData)
}
}
문제가 되는 부분입니다. (해당 코드는 Coroutine Scope 내에 존재합니다.)
CashedKeyword 테이블에서 데이터를 지우면, 이를 참조하고 있는 YoutubeCashedData 데이터 또한 지워집니다.
CashedKeyword 테이블에서 특정 키워드를 삭제한 후, 데이터를 다시 추가해 주는 역할을 하는 코드입니다.
왜 오류가 발생하는지 몰랐습니다. 제가 생각하기에는 틀린 곳이 없었거든요. 실험도 다 잘 되고..
공식 문서를 뒤져보며 예시를 하나 보게 되었습니다.
Transaction 링크를 타고 들어갔습니다.
Room은 한 번에 최대 하나의 트랜잭션만 수행하며, 추가 트랜잭션은 대기하고 선착순으로 실행됩니다.
다음 문장을 공식문서에서 확인할 수 있었습니다.
이전에 학교 전공 시간에 트랜잭션에 대해 배운 적이 있었는데,
와 혹시 저 delete와 insert 연산 간의 트랜잭션이 별개라, db 결과가 반영이 안돼서 그런 것인가 하는 의문이 들었습니다.
확실하진 않지만, 위 예시 코드처럼 각 연산을 하나의 코드로 묶어주었습니다.
@Transaction
fun insertData(searchKeyword: String, cashedKeyword: CashedKeyword, youtubeCashedDataList: List<YoutubeCashedData>) {
deleteAllBySearchKeyword(searchKeyword)
insertKeyword(cashedKeyword)
youtubeCashedDataList.forEach { it ->
insertCashedData(it)
}
}
<SearchResultFragment.kt>
if (list.size < videoDataList.size - 1){
val cashedKeyword = CashedKeyword(searchWord, System.currentTimeMillis())
videoDataList.removeLast()
val youtubeDataList = videoDataList.map{YoutubeCashedData(0, it, searchWord)}
cashedDataDao.insertData(searchWord,cashedKeyword, youtubeDataList)
}
다음과 같이 코드를 수정하였습니다.
오류를 고친 후 업데이트를 진행하였습니다.
경과를 지켜보고, 해결이 되었는지를 포스팅 하겠습니다.
'안드로이드 프로젝트 > 유튜브 음정 조절 어플리케이션' 카테고리의 다른 글
[Android] #11 Youtube data api 할당량 최적화 (데이터 캐싱) (0) | 2023.06.27 |
---|---|
[Android] # 문제 해결 - 4 앱 이슈 해결 (0) | 2023.06.11 |
[Android] #문제 해결 -2 앱이 자주 팅기는 이슈 (0) | 2023.05.26 |
[Android] #문제 해결 -1 SeekBar 관련 이슈 (0) | 2023.05.26 |
[Android] #10-2 리사이클러뷰 헤더 설정 (0) | 2023.05.21 |
댓글