refactor(preferences): DataStore 설정 저장 안정성을 높인다
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.AppPreferencesDataStoreProvider
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DataStoreRuntimeRegressionTest {
|
||||
|
||||
private val context: Context
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SharedPreferenceManager.init(context)
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SharedPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sharedPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
runBlocking {
|
||||
AppPreferencesDataStoreProvider.get(context).edit { preferences ->
|
||||
preferences[stringPreferencesKey(PREF_APP_LANGUAGE_CODE)] = "en"
|
||||
preferences[booleanPreferencesKey(Constants.PREF_IS_PLAYER_SERVICE_RUNNING)] = true
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferenceManager.resetForTest()
|
||||
SharedPreferenceManager.init(context)
|
||||
|
||||
assertEquals("en", SharedPreferenceManager.appLanguageCode)
|
||||
assertTrue(SharedPreferenceManager.isPlayerServiceRunning)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun chatRoomPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
val roomId = 13579L
|
||||
val visibleKey = "chat_bg_visible_room_$roomId"
|
||||
val imageIdKey = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
runBlocking {
|
||||
chatRoomDataStore().edit { preferences ->
|
||||
preferences[booleanPreferencesKey(visibleKey)] = false
|
||||
preferences[longPreferencesKey(imageIdKey)] = 777L
|
||||
}
|
||||
}
|
||||
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
|
||||
assertEquals(false, ChatRoomPreferenceManager.getBoolean(visibleKey, true))
|
||||
assertEquals(777L, ChatRoomPreferenceManager.getLong(imageIdKey, -1L))
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun chatRoomDataStore(): DataStore<Preferences> {
|
||||
val field = ChatRoomPreferenceManager::class.java.getDeclaredField("dataStore")
|
||||
field.isAccessible = true
|
||||
return field.get(ChatRoomPreferenceManager) as DataStore<Preferences>
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MiniPlayerConnectionGuardTest {
|
||||
|
||||
@Test
|
||||
fun mainActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { MainActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistDetailActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { AudioContentPlaylistDetailActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
private fun setPrivateField(target: Any, fieldName: String, value: Any?) {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
field.set(target, value)
|
||||
}
|
||||
|
||||
private fun getPrivateField(target: Any, fieldName: String): Any? {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
return field.get(target)
|
||||
}
|
||||
|
||||
private fun invokePrivateNoArg(target: Any, methodName: String) {
|
||||
val method = target.javaClass.getDeclaredMethod(methodName)
|
||||
method.isAccessible = true
|
||||
method.invoke(target)
|
||||
}
|
||||
|
||||
private fun <T> createOnMainThread(factory: () -> T): T {
|
||||
var instance: Any? = null
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
instance = factory()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return instance as T
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
@@ -40,6 +41,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
||||
|
||||
SodaLiveApplicationHolder.init(this)
|
||||
SharedPreferenceManager.init(applicationContext)
|
||||
ChatRoomPreferenceManager.init(applicationContext)
|
||||
|
||||
ImageLoaderProvider.init(applicationContext)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -13,6 +12,9 @@ import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
@@ -23,6 +25,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
||||
@@ -36,6 +40,9 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.random.Random
|
||||
|
||||
@UnstableApi
|
||||
@@ -60,25 +67,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
|
||||
private val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||
private var mediaController: MediaController? = null
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
handler.postDelayed(
|
||||
{
|
||||
initAndVisibleMiniPlayer()
|
||||
},
|
||||
1500
|
||||
)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||
private var playerStateJob: Job? = null
|
||||
|
||||
private fun initAndVisibleMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
@@ -94,32 +86,48 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
if (mediaController != null || mediaControllerFuture != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(applicationContext, componentName)
|
||||
val mediaControllerFuture =
|
||||
val controllerFuture =
|
||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
mediaControllerFuture = controllerFuture
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
try {
|
||||
if (mediaController != null) {
|
||||
controllerFuture.get().release()
|
||||
return@addListener
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
mediaController = controllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController?.isPlaying == true) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
it.play()
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Logger.e(throwable, "Failed to connect player service")
|
||||
} finally {
|
||||
mediaControllerFuture = null
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(applicationContext)
|
||||
@@ -163,7 +171,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
}
|
||||
|
||||
private fun deInitMiniPlayer() {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
mediaControllerFuture?.cancel(true)
|
||||
mediaControllerFuture = null
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
}
|
||||
@@ -180,18 +191,24 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
||||
|
||||
bindData()
|
||||
viewModel.getPlaylistDetail(playlistId)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
initAndVisibleMiniPlayer()
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
playerStateJob = lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||
if (isRunning) {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
playerStateJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -41,7 +39,6 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
private var characterId: Long = 0L
|
||||
private var profileUrl: String = ""
|
||||
|
||||
private val prefsName = "chat_room_prefs"
|
||||
private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
private lateinit var adapter: BgAdapter
|
||||
@@ -87,8 +84,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
|
||||
private fun loadData() {
|
||||
// 초기 선택: 저장된 이미지 ID 사용
|
||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
val savedId = prefs.getLong(bgImageIdKey(roomId), -1L)
|
||||
val savedId = ChatRoomPreferenceManager.getLong(bgImageIdKey(roomId), -1L)
|
||||
selectedId = if (savedId > 0) savedId else null
|
||||
items.clear()
|
||||
|
||||
@@ -124,12 +120,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun saveAndApply(item: BgItem) {
|
||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
||||
prefs.edit {
|
||||
if (item.id > 0) putLong(
|
||||
bgImageIdKey(roomId),
|
||||
item.id
|
||||
) else remove(bgImageIdKey(roomId))
|
||||
if (item.id > 0) {
|
||||
ChatRoomPreferenceManager.putLong(bgImageIdKey(roomId), item.id)
|
||||
} else {
|
||||
ChatRoomPreferenceManager.remove(bgImageIdKey(roomId))
|
||||
}
|
||||
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -55,12 +54,10 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
||||
private var characterInfo: CharacterInfo? = null
|
||||
|
||||
// 5.4 SharedPreferences (안내 메시지 접힘 상태 저장)
|
||||
private val prefs by lazy { getSharedPreferences("chat_room_prefs", MODE_PRIVATE) }
|
||||
private fun noticePrefKey(roomId: Long) = "chat_notice_hidden_room_${roomId}"
|
||||
private fun isNoticeHidden(): Boolean = prefs.getBoolean(noticePrefKey(roomId), false)
|
||||
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
||||
private fun setNoticeHidden(hidden: Boolean) {
|
||||
prefs.edit { putBoolean(noticePrefKey(roomId), hidden) }
|
||||
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
@@ -866,7 +863,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
}
|
||||
|
||||
fun applyBackgroundVisibility() {
|
||||
val visible = prefs.getBoolean("chat_bg_visible_room_$roomId", true)
|
||||
val visible = ChatRoomPreferenceManager.getBoolean("chat_bg_visible_room_$roomId", true)
|
||||
binding.ivBackgroundProfile.isVisible = visible
|
||||
binding.viewCharacterDim.isVisible = visible
|
||||
}
|
||||
@@ -885,7 +882,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
private fun bgImageIdPrefKey(): String = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
private fun getSavedBackgroundImageId(): Long? {
|
||||
val id = prefs.getLong(bgImageIdPrefKey(), -1L)
|
||||
val id = ChatRoomPreferenceManager.getLong(bgImageIdPrefKey(), -1L)
|
||||
return if (id > 0) id else null
|
||||
}
|
||||
|
||||
@@ -912,9 +909,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
"chat_bg_image_id_room_$roomId",
|
||||
noticePrefKey(roomId)
|
||||
)
|
||||
prefs.edit {
|
||||
keys.forEach { remove(it) }
|
||||
}
|
||||
ChatRoomPreferenceManager.removeAll(keys)
|
||||
}
|
||||
|
||||
fun onResetChatRequested() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,7 +10,6 @@ import android.widget.RelativeLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* 채팅방 우측 상단 더보기 버튼 클릭 시 표시되는 전체화면 다이얼로그.
|
||||
@@ -42,13 +40,12 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
|
||||
// 닫기 버튼
|
||||
view.findViewById<ImageView>(R.id.iv_close)?.setOnClickListener { dismiss() }
|
||||
|
||||
val prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val bgKey = bgPrefKey(roomId)
|
||||
|
||||
val switch = view.findViewById<SwitchMaterial>(R.id.sw_background)
|
||||
switch?.isChecked = prefs.getBoolean(bgKey, true)
|
||||
switch?.isChecked = ChatRoomPreferenceManager.getBoolean(bgKey, true)
|
||||
switch?.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit { putBoolean(bgKey, isChecked) }
|
||||
ChatRoomPreferenceManager.putBoolean(bgKey, isChecked)
|
||||
(activity as? ChatRoomActivity)?.applyBackgroundVisibility()
|
||||
}
|
||||
|
||||
@@ -76,7 +73,6 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_ROOM_ID = "arg_room_id"
|
||||
private const val PREFS_NAME = "chat_room_prefs"
|
||||
private fun bgPrefKey(roomId: Long) = "chat_bg_visible_room_$roomId"
|
||||
|
||||
fun newInstance(roomId: Long): ChatRoomMoreDialogFragment {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package kr.co.vividnext.sodalive.chat.talk.room
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object ChatRoomPreferenceManager {
|
||||
private const val LEGACY_PREFERENCES_NAME = "chat_room_prefs"
|
||||
private const val DATASTORE_FILE_NAME = "chat_room_preferences"
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val preferenceState = MutableStateFlow<Map<String, Any?>>(emptyMap())
|
||||
private val initLock = Any()
|
||||
private var observerJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
fun init(context: Context) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
val appContext = context.applicationContext
|
||||
if (!this::dataStore.isInitialized) {
|
||||
dataStore = androidx.datastore.preferences.core.PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(appContext, LEGACY_PREFERENCES_NAME)),
|
||||
produceFile = { appContext.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
val initialPreferences = runBlocking {
|
||||
dataStore.data.first()
|
||||
}
|
||||
updateState(initialPreferences)
|
||||
initialized = true
|
||||
|
||||
observerJob?.cancel()
|
||||
observerJob = appScope.launch {
|
||||
dataStore.data.collect { preferences ->
|
||||
updateState(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(preferences: Preferences) {
|
||||
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
|
||||
key.name to value
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(initLock) {
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
preferenceState.value = emptyMap()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? Boolean ?: defaultValue
|
||||
}
|
||||
|
||||
fun putBoolean(key: String, value: Boolean) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLong(key: String, defaultValue: Long): Long {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? Long ?: defaultValue
|
||||
}
|
||||
|
||||
fun putLong(key: String, value: Long) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[longPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String): String {
|
||||
ensureInitialized()
|
||||
return preferenceState.value[key] as? String ?: defaultValue
|
||||
}
|
||||
|
||||
fun putString(key: String, value: String) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable[key] = value
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
ensureInitialized()
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
mutable.remove(key)
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val prefKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return@edit
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll(keys: Collection<String>) {
|
||||
ensureInitialized()
|
||||
if (keys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
keys.forEach { key ->
|
||||
mutable.remove(key)
|
||||
}
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val mapKeys = preferences.asMap().keys
|
||||
keys.forEach { keyName ->
|
||||
val prefKey = mapKeys.firstOrNull { it.name == keyName } ?: return@forEach
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
check(initialized) { "ChatRoomPreferenceManager is not initialized." }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
|
||||
object AppPreferencesDataStoreProvider {
|
||||
private const val DATASTORE_FILE_NAME = "sodalive_default_preferences"
|
||||
private const val DEFAULT_SHARED_PREFERENCES_SUFFIX = "_preferences"
|
||||
|
||||
@Volatile
|
||||
private var dataStore: DataStore<Preferences>? = null
|
||||
|
||||
fun get(context: Context): DataStore<Preferences> {
|
||||
val existing = dataStore
|
||||
if (existing != null) {
|
||||
return existing
|
||||
}
|
||||
|
||||
return synchronized(this) {
|
||||
dataStore ?: createDataStore(context.applicationContext).also { dataStore = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDataStore(context: Context): DataStore<Preferences> {
|
||||
val legacyPreferencesName = "${context.packageName}$DEFAULT_SHARED_PREFERENCES_SUFFIX"
|
||||
return PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(
|
||||
// 기존 기본 SharedPreferences 값은 DataStore 첫 접근 시 자동 이관된다.
|
||||
SharedPreferencesMigration(context, legacyPreferencesName)
|
||||
),
|
||||
produceFile = { context.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(this) {
|
||||
dataStore = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,270 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object SharedPreferenceManager {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val preferenceState = MutableStateFlow<Map<String, Any?>>(emptyMap())
|
||||
private val initLock = Any()
|
||||
private var observerJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
val roleFlow: Flow<String> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_USER_ROLE] as? String ?: MemberRole.USER.name
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val isPlayerServiceRunningFlow: Flow<Boolean> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] as? Boolean ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun init(context: Context) {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun registerOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
dataStore = AppPreferencesDataStoreProvider.get(context.applicationContext)
|
||||
val initialPreferences = runBlocking {
|
||||
dataStore.data.first()
|
||||
}
|
||||
updateState(initialPreferences)
|
||||
initialized = true
|
||||
|
||||
fun clear() {
|
||||
sharedPreferences.edit { editor ->
|
||||
sharedPreferences.all.keys
|
||||
.filterNot { it == Constants.PREF_PUSH_TOKEN }
|
||||
.forEach { editor.remove(it) }
|
||||
observerJob?.cancel()
|
||||
observerJob = appScope.launch {
|
||||
dataStore.data.collect { preferences ->
|
||||
updateState(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
|
||||
val editor = this.edit()
|
||||
operation(editor)
|
||||
editor.apply()
|
||||
private fun updateState(preferences: Preferences) {
|
||||
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
|
||||
key.name to value
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun SharedPreferences.set(key: String, value: Any?) {
|
||||
when (value) {
|
||||
is String? -> edit { it.putString(key, value) }
|
||||
is Int -> edit { it.putInt(key, value) }
|
||||
is Boolean -> edit { it.putBoolean(key, value) }
|
||||
is Float -> edit { it.putFloat(key, value) }
|
||||
is Long -> edit { it.putLong(key, value) }
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(initLock) {
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
preferenceState.value = emptyMap()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val pushToken = state[Constants.PREF_PUSH_TOKEN]
|
||||
if (pushToken != null) {
|
||||
mapOf(Constants.PREF_PUSH_TOKEN to pushToken)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val keysToRemove = preferences.asMap().keys.filter { it.name != Constants.PREF_PUSH_TOKEN }
|
||||
keysToRemove.forEach { preferenceKey ->
|
||||
preferences.remove(preferenceKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
check(initialized) { "SharedPreferenceManager is not initialized." }
|
||||
}
|
||||
|
||||
private fun setPreference(key: String, value: Any?) {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
if (value == null) {
|
||||
mutable.remove(key)
|
||||
} else {
|
||||
mutable[key] = value
|
||||
}
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
when (value) {
|
||||
null -> removeByName(preferences, key)
|
||||
is String -> preferences[stringPreferencesKey(key)] = value
|
||||
is Int -> preferences[intPreferencesKey(key)] = value
|
||||
is Boolean -> preferences[booleanPreferencesKey(key)] = value
|
||||
is Float -> preferences[floatPreferencesKey(key)] = value
|
||||
is Long -> preferences[longPreferencesKey(key)] = value
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private operator fun <T> SharedPreferences.get(key: String, defaultValue: T? = null): T {
|
||||
private fun <T> getPreference(key: String, defaultValue: T? = null): T {
|
||||
ensureInitialized()
|
||||
val value = preferenceState.value[key]
|
||||
|
||||
return when (defaultValue) {
|
||||
is String, null -> getString(key, defaultValue as? String) as T
|
||||
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
|
||||
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
|
||||
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
|
||||
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
|
||||
is String, null -> (value as? String ?: defaultValue as? String) as T
|
||||
is Int -> (value as? Int ?: defaultValue) as T
|
||||
is Boolean -> (value as? Boolean ?: defaultValue) as T
|
||||
is Float -> (value as? Float ?: defaultValue) as T
|
||||
is Long -> (value as? Long ?: defaultValue) as T
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeByName(preferences: MutablePreferences, key: String) {
|
||||
val targetKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return
|
||||
preferences.remove(targetKey)
|
||||
}
|
||||
|
||||
var token: String
|
||||
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_TOKEN] = value
|
||||
setPreference(Constants.PREF_TOKEN, value)
|
||||
}
|
||||
|
||||
var userId: Long
|
||||
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
|
||||
get() = getPreference(Constants.PREF_USER_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ID] = value
|
||||
setPreference(Constants.PREF_USER_ID, value)
|
||||
}
|
||||
|
||||
var nickname: String
|
||||
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
|
||||
get() = getPreference(Constants.PREF_NICKNAME, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NICKNAME] = value
|
||||
setPreference(Constants.PREF_NICKNAME, value)
|
||||
}
|
||||
|
||||
var email: String
|
||||
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
|
||||
get() = getPreference(Constants.PREF_EMAIL, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_EMAIL] = value
|
||||
setPreference(Constants.PREF_EMAIL, value)
|
||||
}
|
||||
|
||||
var profileImage: String
|
||||
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
|
||||
get() = getPreference(Constants.PREF_PROFILE_IMAGE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
|
||||
setPreference(Constants.PREF_PROFILE_IMAGE, value)
|
||||
}
|
||||
|
||||
var can: Int
|
||||
get() = sharedPreferences[Constants.PREF_CAN, 0]
|
||||
get() = getPreference(Constants.PREF_CAN, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CAN] = value
|
||||
setPreference(Constants.PREF_CAN, value)
|
||||
}
|
||||
|
||||
var point: Int
|
||||
get() = sharedPreferences[Constants.PREF_POINT, 0]
|
||||
get() = getPreference(Constants.PREF_POINT, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_POINT] = value
|
||||
setPreference(Constants.PREF_POINT, value)
|
||||
}
|
||||
|
||||
var role: String
|
||||
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
|
||||
get() = getPreference(Constants.PREF_USER_ROLE, MemberRole.USER.name)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ROLE] = value
|
||||
setPreference(Constants.PREF_USER_ROLE, value)
|
||||
}
|
||||
|
||||
var isAuth: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT] = value
|
||||
setPreference(Constants.PREF_IS_ADULT, value)
|
||||
}
|
||||
|
||||
var isAuditionNotification: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION, false]
|
||||
get() = getPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION] = value
|
||||
setPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, value)
|
||||
}
|
||||
|
||||
var isAdultContentVisible: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE] = value
|
||||
setPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, value)
|
||||
}
|
||||
|
||||
var contentPreference: Int
|
||||
get() = sharedPreferences[Constants.PREF_CONTENT_PREFERENCE, 0]
|
||||
get() = getPreference(Constants.PREF_CONTENT_PREFERENCE, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CONTENT_PREFERENCE] = value
|
||||
setPreference(Constants.PREF_CONTENT_PREFERENCE, value)
|
||||
}
|
||||
|
||||
var pushToken: String
|
||||
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_PUSH_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
|
||||
setPreference(Constants.PREF_PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
var isContentPlayLoop: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
|
||||
get() = getPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
|
||||
setPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, value)
|
||||
}
|
||||
|
||||
var notShowingEventPopupId: Long
|
||||
get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0]
|
||||
get() = getPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
|
||||
setPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, value)
|
||||
}
|
||||
|
||||
var isViewedOnboardingTutorial: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false]
|
||||
get() = getPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
|
||||
setPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, value)
|
||||
}
|
||||
|
||||
var noChatRoomList: List<Long>
|
||||
get() {
|
||||
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
|
||||
val list = getPreference(Constants.PREF_NO_CHAT_ROOM, "")
|
||||
val gson = Gson()
|
||||
val listType = object : TypeToken<List<Long>>() {}.type
|
||||
val myList = gson.fromJson<List<Long>>(list, listType)
|
||||
@@ -170,54 +273,60 @@ object SharedPreferenceManager {
|
||||
set(value) {
|
||||
val gson = Gson()
|
||||
val listJson = gson.toJson(value)
|
||||
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
|
||||
setPreference(Constants.PREF_NO_CHAT_ROOM, listJson)
|
||||
}
|
||||
|
||||
var isPlayerServiceRunning: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false]
|
||||
get() = getPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] = value
|
||||
setPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, value)
|
||||
}
|
||||
|
||||
var marketingPid: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_PID, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_PID, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_PID] = value
|
||||
setPreference(Constants.PREF_MARKETING_PID, value)
|
||||
}
|
||||
|
||||
var marketingUtmSource: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_SOURCE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_SOURCE, value)
|
||||
}
|
||||
|
||||
var marketingUtmMedium: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_MEDIUM, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_MEDIUM, value)
|
||||
}
|
||||
|
||||
var marketingUtmCampaign: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, value)
|
||||
}
|
||||
|
||||
var marketingLinkValue: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE, value)
|
||||
}
|
||||
|
||||
var marketingLinkValueId: Long
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID, 0L]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, value)
|
||||
}
|
||||
|
||||
var alreadyTrackingAppLaunch: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true]
|
||||
get() = getPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH] = value
|
||||
setPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, value)
|
||||
}
|
||||
|
||||
var appLanguageCode: String
|
||||
get() = getPreference(PREF_APP_LANGUAGE_CODE, "")
|
||||
set(value) {
|
||||
setPreference(PREF_APP_LANGUAGE_CODE, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -13,6 +12,9 @@ import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -68,6 +70,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
|
||||
@@ -95,34 +99,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_USER_ROLE) {
|
||||
if (
|
||||
sharedPreferences.getString(
|
||||
key,
|
||||
MemberRole.USER.name
|
||||
) == MemberRole.CREATOR.name
|
||||
) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.roleFlow.collect { role ->
|
||||
renderUploadContentByRole(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
setupView()
|
||||
bindData()
|
||||
|
||||
@@ -130,26 +117,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
}
|
||||
renderUploadContentByRole(SharedPreferenceManager.role)
|
||||
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
binding.llShortIcon.visibility = View.VISIBLE
|
||||
@@ -199,6 +173,23 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
||||
setupRecommendContent()
|
||||
}
|
||||
|
||||
private fun renderUploadContentByRole(role: String) {
|
||||
if (role == MemberRole.CREATOR.name) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setupLiveView() {
|
||||
liveAdapter = HomeLiveAdapter {
|
||||
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.live
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -15,6 +14,9 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -72,6 +74,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@UnstableApi
|
||||
@@ -90,27 +94,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
private var message = ""
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_USER_ROLE) {
|
||||
if (
|
||||
sharedPreferences.getString(
|
||||
key,
|
||||
MemberRole.USER.name
|
||||
) == MemberRole.CREATOR.name
|
||||
) {
|
||||
binding.llMakeLive.visibility = View.VISIBLE
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
} else {
|
||||
binding.llMakeLive.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -131,7 +114,14 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.roleFlow.collect { role ->
|
||||
renderMakeLiveByRole(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupView()
|
||||
|
||||
@@ -140,7 +130,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -159,17 +148,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
}
|
||||
}
|
||||
|
||||
binding.llMakeLive.visibility =
|
||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
renderMakeLiveByRole(SharedPreferenceManager.role)
|
||||
|
||||
setupToolbar()
|
||||
setupLiveNow()
|
||||
@@ -181,6 +160,19 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
||||
setupLiveReservation()
|
||||
}
|
||||
|
||||
private fun renderMakeLiveByRole(role: String) {
|
||||
if (role == MemberRole.CREATOR.name) {
|
||||
binding.llMakeLive.visibility = View.VISIBLE
|
||||
binding.llMakeLive.setOnClickListener {
|
||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
binding.llMakeLive.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||
binding.llShortIcon.visibility = View.VISIBLE
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -16,6 +15,9 @@ import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
@@ -24,6 +26,7 @@ import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.gun0912.tedpermission.PermissionListener
|
||||
import com.gun0912.tedpermission.normal.TedPermission
|
||||
@@ -52,6 +55,9 @@ import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@UnstableApi
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
|
||||
@@ -63,25 +69,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
|
||||
|
||||
private var mediaController: MediaController? = null
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||
private val audioContentReceiver = AudioContentReceiver()
|
||||
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
handler.postDelayed(
|
||||
{
|
||||
initAndVisibleMiniPlayer()
|
||||
},
|
||||
1500
|
||||
)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var playerStateJob: Job? = null
|
||||
|
||||
private fun initAndVisibleMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
@@ -97,32 +89,48 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
if (mediaController != null || mediaControllerFuture != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(applicationContext, componentName)
|
||||
val mediaControllerFuture =
|
||||
val controllerFuture =
|
||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
mediaControllerFuture = controllerFuture
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
try {
|
||||
if (mediaController != null) {
|
||||
controllerFuture.get().release()
|
||||
return@addListener
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
mediaController = controllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setImageResource(
|
||||
if (mediaController?.isPlaying == true) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
it.play()
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Logger.e(throwable, "Failed to connect player service")
|
||||
} finally {
|
||||
mediaControllerFuture = null
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(applicationContext)
|
||||
@@ -166,7 +174,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
}
|
||||
|
||||
private fun deInitMiniPlayer() {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
mediaControllerFuture?.cancel(true)
|
||||
mediaControllerFuture = null
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
}
|
||||
@@ -202,13 +213,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
updatePidAndGaid()
|
||||
getEventPopup()
|
||||
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(
|
||||
preferenceChangeListener
|
||||
)
|
||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
initAndVisibleMiniPlayer()
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
playerStateJob = lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||
if (isRunning) {
|
||||
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||
@@ -217,7 +232,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
playerStateJob?.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.settings.language
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
object LanguageManager {
|
||||
const val LANG_KO = "ko"
|
||||
const val LANG_EN = "en"
|
||||
const val LANG_JA = "ja"
|
||||
|
||||
private const val PREF_KEY_APP_LANGUAGE = "pref_app_language_code"
|
||||
|
||||
private fun prefs(context: Context): SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||
|
||||
fun isSupported(code: String): Boolean = when (code) {
|
||||
LANG_KO, LANG_EN, LANG_JA -> true
|
||||
else -> false
|
||||
@@ -25,8 +18,9 @@ object LanguageManager {
|
||||
* 사용자가 앱 내에서 명시적으로 선택한 언어 코드를 반환한다. 없으면 null.
|
||||
*/
|
||||
fun getUserSelectedLanguageOrNull(context: Context): String? {
|
||||
val code = prefs(context).getString(PREF_KEY_APP_LANGUAGE, null)
|
||||
return code?.takeIf { it.isNotBlank() }
|
||||
SharedPreferenceManager.init(context.applicationContext)
|
||||
val code = SharedPreferenceManager.appLanguageCode
|
||||
return code.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +58,7 @@ object LanguageManager {
|
||||
|
||||
fun setSelectedLanguage(context: Context, code: String) {
|
||||
val normalized = if (isSupported(code)) code else LANG_KO
|
||||
prefs(context).edit { putString(PREF_KEY_APP_LANGUAGE, normalized) }
|
||||
SharedPreferenceManager.init(context.applicationContext)
|
||||
SharedPreferenceManager.appLanguageCode = normalized
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user