refactor(preferences): DataStore 설정 저장 안정성을 높인다
This commit is contained in:
@@ -65,6 +65,7 @@ android {
|
|||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 224
|
versionCode 224
|
||||||
versionName "1.51.1"
|
versionName "1.51.1"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -165,6 +166,7 @@ dependencies {
|
|||||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
||||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||||
}
|
}
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.2.0"
|
||||||
|
|
||||||
// Gson
|
// Gson
|
||||||
implementation "com.google.code.gson:gson:2.13.2"
|
implementation "com.google.code.gson:gson:2.13.2"
|
||||||
@@ -253,6 +255,10 @@ dependencies {
|
|||||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
|
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
|
||||||
testImplementation 'io.mockk:mockk:1.14.6'
|
testImplementation 'io.mockk:mockk:1.14.6'
|
||||||
|
|
||||||
|
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 com.orhanobut.logger.Logger
|
||||||
import kr.co.vividnext.sodalive.BuildConfig
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
import kr.co.vividnext.sodalive.R
|
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.ImageLoaderProvider
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||||
@@ -40,6 +41,7 @@ class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
|||||||
|
|
||||||
SodaLiveApplicationHolder.init(this)
|
SodaLiveApplicationHolder.init(this)
|
||||||
SharedPreferenceManager.init(applicationContext)
|
SharedPreferenceManager.init(applicationContext)
|
||||||
|
ChatRoomPreferenceManager.init(applicationContext)
|
||||||
|
|
||||||
ImageLoaderProvider.init(applicationContext)
|
ImageLoaderProvider.init(applicationContext)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -13,6 +12,9 @@ import android.widget.ImageView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
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.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
@@ -23,6 +25,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.RoundedCornersTransformation
|
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.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
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.databinding.ActivityAudioContentPlaylistDetailBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -60,25 +67,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||||||
|
|
||||||
private val contentList = mutableListOf<AudioContentPlaylistContent>()
|
private val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||||
private var mediaController: MediaController? = null
|
private var mediaController: 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() }
|
||||||
@SuppressLint("SetTextI18n")
|
private var playerStateJob: Job? = null
|
||||||
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 fun initAndVisibleMiniPlayer() {
|
private fun initAndVisibleMiniPlayer() {
|
||||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||||
@@ -94,18 +86,29 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun connectPlayerService() {
|
private fun connectPlayerService() {
|
||||||
|
if (mediaController != null || mediaControllerFuture != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||||
val sessionToken = SessionToken(applicationContext, componentName)
|
val sessionToken = SessionToken(applicationContext, componentName)
|
||||||
val mediaControllerFuture =
|
val controllerFuture =
|
||||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||||
mediaControllerFuture.addListener(
|
mediaControllerFuture = controllerFuture
|
||||||
|
controllerFuture.addListener(
|
||||||
{
|
{
|
||||||
mediaController = mediaControllerFuture.get()
|
try {
|
||||||
|
if (mediaController != null) {
|
||||||
|
controllerFuture.get().release()
|
||||||
|
return@addListener
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController = controllerFuture.get()
|
||||||
setupMediaController()
|
setupMediaController()
|
||||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||||
|
|
||||||
binding.ivPlayOrPause.setImageResource(
|
binding.ivPlayOrPause.setImageResource(
|
||||||
if (mediaController!!.isPlaying) {
|
if (mediaController?.isPlaying == true) {
|
||||||
R.drawable.ic_player_pause
|
R.drawable.ic_player_pause
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_player_play
|
R.drawable.ic_player_play
|
||||||
@@ -121,6 +124,11 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Logger.e(throwable, "Failed to connect player service")
|
||||||
|
} finally {
|
||||||
|
mediaControllerFuture = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ContextCompat.getMainExecutor(applicationContext)
|
ContextCompat.getMainExecutor(applicationContext)
|
||||||
)
|
)
|
||||||
@@ -163,7 +171,10 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deInitMiniPlayer() {
|
private fun deInitMiniPlayer() {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
binding.clMiniPlayer.visibility = View.GONE
|
binding.clMiniPlayer.visibility = View.GONE
|
||||||
|
mediaControllerFuture?.cancel(true)
|
||||||
|
mediaControllerFuture = null
|
||||||
mediaController?.release()
|
mediaController?.release()
|
||||||
mediaController = null
|
mediaController = null
|
||||||
}
|
}
|
||||||
@@ -180,18 +191,24 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
|
|||||||
|
|
||||||
bindData()
|
bindData()
|
||||||
viewModel.getPlaylistDetail(playlistId)
|
viewModel.getPlaylistDetail(playlistId)
|
||||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
|
||||||
|
|
||||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
playerStateJob = lifecycleScope.launch {
|
||||||
initAndVisibleMiniPlayer()
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||||
|
if (isRunning) {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||||
} else {
|
} else {
|
||||||
deInitMiniPlayer()
|
deInitMiniPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
deInitMiniPlayer()
|
deInitMiniPlayer()
|
||||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
playerStateJob?.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.talk.room
|
package kr.co.vividnext.sodalive.chat.talk.room
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -41,7 +39,6 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
|||||||
private var characterId: Long = 0L
|
private var characterId: Long = 0L
|
||||||
private var profileUrl: String = ""
|
private var profileUrl: String = ""
|
||||||
|
|
||||||
private val prefsName = "chat_room_prefs"
|
|
||||||
private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId"
|
private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId"
|
||||||
|
|
||||||
private lateinit var adapter: BgAdapter
|
private lateinit var adapter: BgAdapter
|
||||||
@@ -87,8 +84,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
// 초기 선택: 저장된 이미지 ID 사용
|
// 초기 선택: 저장된 이미지 ID 사용
|
||||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
val savedId = ChatRoomPreferenceManager.getLong(bgImageIdKey(roomId), -1L)
|
||||||
val savedId = prefs.getLong(bgImageIdKey(roomId), -1L)
|
|
||||||
selectedId = if (savedId > 0) savedId else null
|
selectedId = if (savedId > 0) savedId else null
|
||||||
items.clear()
|
items.clear()
|
||||||
|
|
||||||
@@ -124,12 +120,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveAndApply(item: BgItem) {
|
private fun saveAndApply(item: BgItem) {
|
||||||
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
if (item.id > 0) {
|
||||||
prefs.edit {
|
ChatRoomPreferenceManager.putLong(bgImageIdKey(roomId), item.id)
|
||||||
if (item.id > 0) putLong(
|
} else {
|
||||||
bgImageIdKey(roomId),
|
ChatRoomPreferenceManager.remove(bgImageIdKey(roomId))
|
||||||
item.id
|
|
||||||
) else remove(bgImageIdKey(roomId))
|
|
||||||
}
|
}
|
||||||
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
|
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import android.text.TextWatcher
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -55,12 +54,10 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
||||||
private var characterInfo: CharacterInfo? = null
|
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 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) {
|
private fun setNoticeHidden(hidden: Boolean) {
|
||||||
prefs.edit { putBoolean(noticePrefKey(roomId), hidden) }
|
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupView() {
|
override fun setupView() {
|
||||||
@@ -866,7 +863,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun applyBackgroundVisibility() {
|
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.ivBackgroundProfile.isVisible = visible
|
||||||
binding.viewCharacterDim.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 bgImageIdPrefKey(): String = "chat_bg_image_id_room_$roomId"
|
||||||
|
|
||||||
private fun getSavedBackgroundImageId(): Long? {
|
private fun getSavedBackgroundImageId(): Long? {
|
||||||
val id = prefs.getLong(bgImageIdPrefKey(), -1L)
|
val id = ChatRoomPreferenceManager.getLong(bgImageIdPrefKey(), -1L)
|
||||||
return if (id > 0) id else null
|
return if (id > 0) id else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,9 +909,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
"chat_bg_image_id_room_$roomId",
|
"chat_bg_image_id_room_$roomId",
|
||||||
noticePrefKey(roomId)
|
noticePrefKey(roomId)
|
||||||
)
|
)
|
||||||
prefs.edit {
|
ChatRoomPreferenceManager.removeAll(keys)
|
||||||
keys.forEach { remove(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResetChatRequested() {
|
fun onResetChatRequested() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.talk.room
|
package kr.co.vividnext.sodalive.chat.talk.room
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -11,7 +10,6 @@ import android.widget.RelativeLayout
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
import kr.co.vividnext.sodalive.R
|
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() }
|
view.findViewById<ImageView>(R.id.iv_close)?.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
val prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
val bgKey = bgPrefKey(roomId)
|
val bgKey = bgPrefKey(roomId)
|
||||||
|
|
||||||
val switch = view.findViewById<SwitchMaterial>(R.id.sw_background)
|
val switch = view.findViewById<SwitchMaterial>(R.id.sw_background)
|
||||||
switch?.isChecked = prefs.getBoolean(bgKey, true)
|
switch?.isChecked = ChatRoomPreferenceManager.getBoolean(bgKey, true)
|
||||||
switch?.setOnCheckedChangeListener { _, isChecked ->
|
switch?.setOnCheckedChangeListener { _, isChecked ->
|
||||||
prefs.edit { putBoolean(bgKey, isChecked) }
|
ChatRoomPreferenceManager.putBoolean(bgKey, isChecked)
|
||||||
(activity as? ChatRoomActivity)?.applyBackgroundVisibility()
|
(activity as? ChatRoomActivity)?.applyBackgroundVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +73,6 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_ROOM_ID = "arg_room_id"
|
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"
|
private fun bgPrefKey(roomId: Long) = "chat_bg_visible_room_$roomId"
|
||||||
|
|
||||||
fun newInstance(roomId: Long): ChatRoomMoreDialogFragment {
|
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
|
package kr.co.vividnext.sodalive.common
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.preference.PreferenceManager
|
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.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
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 {
|
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) {
|
fun init(context: Context) {
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
if (initialized) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerOnSharedPreferenceChangeListener(
|
synchronized(initLock) {
|
||||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
if (initialized) {
|
||||||
) {
|
return
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterOnSharedPreferenceChangeListener(
|
dataStore = AppPreferencesDataStoreProvider.get(context.applicationContext)
|
||||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
val initialPreferences = runBlocking {
|
||||||
) {
|
dataStore.data.first()
|
||||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
}
|
||||||
|
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 clear() {
|
fun clear() {
|
||||||
sharedPreferences.edit { editor ->
|
ensureInitialized()
|
||||||
sharedPreferences.all.keys
|
|
||||||
.filterNot { it == Constants.PREF_PUSH_TOKEN }
|
preferenceState.update { state ->
|
||||||
.forEach { editor.remove(it) }
|
val pushToken = state[Constants.PREF_PUSH_TOKEN]
|
||||||
|
if (pushToken != null) {
|
||||||
|
mapOf(Constants.PREF_PUSH_TOKEN to pushToken)
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
|
appScope.launch {
|
||||||
val editor = this.edit()
|
dataStore.edit { preferences ->
|
||||||
operation(editor)
|
val keysToRemove = preferences.asMap().keys.filter { it.name != Constants.PREF_PUSH_TOKEN }
|
||||||
editor.apply()
|
keysToRemove.forEach { preferenceKey ->
|
||||||
|
preferences.remove(preferenceKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private operator fun SharedPreferences.set(key: String, value: Any?) {
|
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) {
|
when (value) {
|
||||||
is String? -> edit { it.putString(key, value) }
|
null -> removeByName(preferences, key)
|
||||||
is Int -> edit { it.putInt(key, value) }
|
is String -> preferences[stringPreferencesKey(key)] = value
|
||||||
is Boolean -> edit { it.putBoolean(key, value) }
|
is Int -> preferences[intPreferencesKey(key)] = value
|
||||||
is Float -> edit { it.putFloat(key, value) }
|
is Boolean -> preferences[booleanPreferencesKey(key)] = value
|
||||||
is Long -> edit { it.putLong(key, value) }
|
is Float -> preferences[floatPreferencesKey(key)] = value
|
||||||
|
is Long -> preferences[longPreferencesKey(key)] = value
|
||||||
else -> throw UnsupportedOperationException("Error")
|
else -> throw UnsupportedOperationException("Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@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) {
|
return when (defaultValue) {
|
||||||
is String, null -> getString(key, defaultValue as? String) as T
|
is String, null -> (value as? String ?: defaultValue as? String) as T
|
||||||
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
|
is Int -> (value as? Int ?: defaultValue) as T
|
||||||
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
|
is Boolean -> (value as? Boolean ?: defaultValue) as T
|
||||||
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
|
is Float -> (value as? Float ?: defaultValue) as T
|
||||||
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
|
is Long -> (value as? Long ?: defaultValue) as T
|
||||||
else -> throw UnsupportedOperationException("Error")
|
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
|
var token: String
|
||||||
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
|
get() = getPreference(Constants.PREF_TOKEN, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_TOKEN] = value
|
setPreference(Constants.PREF_TOKEN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId: Long
|
var userId: Long
|
||||||
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
|
get() = getPreference(Constants.PREF_USER_ID, 0L)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_USER_ID] = value
|
setPreference(Constants.PREF_USER_ID, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var nickname: String
|
var nickname: String
|
||||||
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
|
get() = getPreference(Constants.PREF_NICKNAME, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_NICKNAME] = value
|
setPreference(Constants.PREF_NICKNAME, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var email: String
|
var email: String
|
||||||
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
|
get() = getPreference(Constants.PREF_EMAIL, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_EMAIL] = value
|
setPreference(Constants.PREF_EMAIL, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileImage: String
|
var profileImage: String
|
||||||
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
|
get() = getPreference(Constants.PREF_PROFILE_IMAGE, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
|
setPreference(Constants.PREF_PROFILE_IMAGE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var can: Int
|
var can: Int
|
||||||
get() = sharedPreferences[Constants.PREF_CAN, 0]
|
get() = getPreference(Constants.PREF_CAN, 0)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_CAN] = value
|
setPreference(Constants.PREF_CAN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var point: Int
|
var point: Int
|
||||||
get() = sharedPreferences[Constants.PREF_POINT, 0]
|
get() = getPreference(Constants.PREF_POINT, 0)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_POINT] = value
|
setPreference(Constants.PREF_POINT, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var role: String
|
var role: String
|
||||||
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
|
get() = getPreference(Constants.PREF_USER_ROLE, MemberRole.USER.name)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_USER_ROLE] = value
|
setPreference(Constants.PREF_USER_ROLE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAuth: Boolean
|
var isAuth: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
|
get() = getPreference(Constants.PREF_IS_ADULT, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_ADULT] = value
|
setPreference(Constants.PREF_IS_ADULT, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAuditionNotification: Boolean
|
var isAuditionNotification: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION, false]
|
get() = getPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION] = value
|
setPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAdultContentVisible: Boolean
|
var isAdultContentVisible: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true]
|
get() = getPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE] = value
|
setPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentPreference: Int
|
var contentPreference: Int
|
||||||
get() = sharedPreferences[Constants.PREF_CONTENT_PREFERENCE, 0]
|
get() = getPreference(Constants.PREF_CONTENT_PREFERENCE, 0)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_CONTENT_PREFERENCE] = value
|
setPreference(Constants.PREF_CONTENT_PREFERENCE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushToken: String
|
var pushToken: String
|
||||||
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
|
get() = getPreference(Constants.PREF_PUSH_TOKEN, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
|
setPreference(Constants.PREF_PUSH_TOKEN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isContentPlayLoop: Boolean
|
var isContentPlayLoop: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
|
get() = getPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
|
setPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var notShowingEventPopupId: Long
|
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) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
|
setPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isViewedOnboardingTutorial: Boolean
|
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) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
|
setPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var noChatRoomList: List<Long>
|
var noChatRoomList: List<Long>
|
||||||
get() {
|
get() {
|
||||||
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
|
val list = getPreference(Constants.PREF_NO_CHAT_ROOM, "")
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val listType = object : TypeToken<List<Long>>() {}.type
|
val listType = object : TypeToken<List<Long>>() {}.type
|
||||||
val myList = gson.fromJson<List<Long>>(list, listType)
|
val myList = gson.fromJson<List<Long>>(list, listType)
|
||||||
@@ -170,54 +273,60 @@ object SharedPreferenceManager {
|
|||||||
set(value) {
|
set(value) {
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val listJson = gson.toJson(value)
|
val listJson = gson.toJson(value)
|
||||||
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
|
setPreference(Constants.PREF_NO_CHAT_ROOM, listJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isPlayerServiceRunning: Boolean
|
var isPlayerServiceRunning: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false]
|
get() = getPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] = value
|
setPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingPid: String
|
var marketingPid: String
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_PID, ""]
|
get() = getPreference(Constants.PREF_MARKETING_PID, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_PID] = value
|
setPreference(Constants.PREF_MARKETING_PID, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingUtmSource: String
|
var marketingUtmSource: String
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE, ""]
|
get() = getPreference(Constants.PREF_MARKETING_UTM_SOURCE, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE] = value
|
setPreference(Constants.PREF_MARKETING_UTM_SOURCE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingUtmMedium: String
|
var marketingUtmMedium: String
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM, ""]
|
get() = getPreference(Constants.PREF_MARKETING_UTM_MEDIUM, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM] = value
|
setPreference(Constants.PREF_MARKETING_UTM_MEDIUM, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingUtmCampaign: String
|
var marketingUtmCampaign: String
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN, ""]
|
get() = getPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN] = value
|
setPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingLinkValue: String
|
var marketingLinkValue: String
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE, ""]
|
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE, "")
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE] = value
|
setPreference(Constants.PREF_MARKETING_LINK_VALUE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var marketingLinkValueId: Long
|
var marketingLinkValueId: Long
|
||||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID, 0L]
|
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, 0L)
|
||||||
set(value) {
|
set(value) {
|
||||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID] = value
|
setPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var alreadyTrackingAppLaunch: Boolean
|
var alreadyTrackingAppLaunch: Boolean
|
||||||
get() = sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true]
|
get() = getPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true)
|
||||||
set(value) {
|
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
|
package kr.co.vividnext.sodalive.home
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -13,6 +12,9 @@ import android.widget.Toast
|
|||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
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.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -68,6 +70,8 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
|
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
|
||||||
@@ -95,34 +99,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
private val preferenceChangeListener =
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
super.onViewCreated(view, savedInstanceState)
|
||||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
|
||||||
if (key == Constants.PREF_USER_ROLE) {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
sharedPreferences.getString(
|
SharedPreferenceManager.roleFlow.collect { role ->
|
||||||
key,
|
renderUploadContentByRole(role)
|
||||||
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)
|
|
||||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
|
||||||
setupView()
|
setupView()
|
||||||
bindData()
|
bindData()
|
||||||
|
|
||||||
@@ -130,26 +117,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||||
|
|
||||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
renderUploadContentByRole(SharedPreferenceManager.role)
|
||||||
binding.llUploadContent.visibility = View.VISIBLE
|
|
||||||
binding.llUploadContent.setOnClickListener {
|
|
||||||
startActivity(
|
|
||||||
Intent(
|
|
||||||
requireActivity(),
|
|
||||||
AudioContentUploadActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.llUploadContent.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||||
binding.llShortIcon.visibility = View.VISIBLE
|
binding.llShortIcon.visibility = View.VISIBLE
|
||||||
@@ -199,6 +173,23 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
|
|||||||
setupRecommendContent()
|
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() {
|
private fun setupLiveView() {
|
||||||
liveAdapter = HomeLiveAdapter {
|
liveAdapter = HomeLiveAdapter {
|
||||||
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
ensureLoginAndAdultAuth(isAdult = it.isAdult) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.live
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -15,6 +14,9 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
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.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -72,6 +74,8 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -90,27 +94,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
private var message = ""
|
private var message = ""
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -131,7 +114,14 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
SharedPreferenceManager.roleFlow.collect { role ->
|
||||||
|
renderMakeLiveByRole(role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupView()
|
setupView()
|
||||||
|
|
||||||
@@ -140,7 +130,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,17 +148,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.llMakeLive.visibility =
|
renderMakeLiveByRole(SharedPreferenceManager.role)
|
||||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.llMakeLive.setOnClickListener {
|
|
||||||
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
|
|
||||||
activityResultLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupLiveNow()
|
setupLiveNow()
|
||||||
@@ -181,6 +160,19 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
|
|||||||
setupLiveReservation()
|
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() {
|
private fun setupToolbar() {
|
||||||
if (SharedPreferenceManager.token.isNotBlank()) {
|
if (SharedPreferenceManager.token.isNotBlank()) {
|
||||||
binding.llShortIcon.visibility = View.VISIBLE
|
binding.llShortIcon.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -16,6 +15,9 @@ import android.os.Looper
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
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.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
@@ -24,6 +26,7 @@ import androidx.media3.session.MediaController
|
|||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import com.gun0912.tedpermission.PermissionListener
|
import com.gun0912.tedpermission.PermissionListener
|
||||||
import com.gun0912.tedpermission.normal.TedPermission
|
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 kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
|
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
|
||||||
@@ -63,25 +69,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
|
private lateinit var notificationSettingsDialog: NotificationSettingsDialog
|
||||||
|
|
||||||
private var mediaController: MediaController? = null
|
private var mediaController: 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 audioContentReceiver = AudioContentReceiver()
|
private val audioContentReceiver = AudioContentReceiver()
|
||||||
|
private var playerStateJob: Job? = null
|
||||||
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 fun initAndVisibleMiniPlayer() {
|
private fun initAndVisibleMiniPlayer() {
|
||||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||||
@@ -97,18 +89,29 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun connectPlayerService() {
|
private fun connectPlayerService() {
|
||||||
|
if (mediaController != null || mediaControllerFuture != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||||
val sessionToken = SessionToken(applicationContext, componentName)
|
val sessionToken = SessionToken(applicationContext, componentName)
|
||||||
val mediaControllerFuture =
|
val controllerFuture =
|
||||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||||
mediaControllerFuture.addListener(
|
mediaControllerFuture = controllerFuture
|
||||||
|
controllerFuture.addListener(
|
||||||
{
|
{
|
||||||
mediaController = mediaControllerFuture.get()
|
try {
|
||||||
|
if (mediaController != null) {
|
||||||
|
controllerFuture.get().release()
|
||||||
|
return@addListener
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaController = controllerFuture.get()
|
||||||
setupMediaController()
|
setupMediaController()
|
||||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||||
|
|
||||||
binding.ivPlayerPlayOrPause.setImageResource(
|
binding.ivPlayerPlayOrPause.setImageResource(
|
||||||
if (mediaController!!.isPlaying) {
|
if (mediaController?.isPlaying == true) {
|
||||||
R.drawable.ic_player_pause
|
R.drawable.ic_player_pause
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_player_play
|
R.drawable.ic_player_play
|
||||||
@@ -124,6 +127,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Logger.e(throwable, "Failed to connect player service")
|
||||||
|
} finally {
|
||||||
|
mediaControllerFuture = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ContextCompat.getMainExecutor(applicationContext)
|
ContextCompat.getMainExecutor(applicationContext)
|
||||||
)
|
)
|
||||||
@@ -166,7 +174,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deInitMiniPlayer() {
|
private fun deInitMiniPlayer() {
|
||||||
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
binding.clMiniPlayer.visibility = View.GONE
|
binding.clMiniPlayer.visibility = View.GONE
|
||||||
|
mediaControllerFuture?.cancel(true)
|
||||||
|
mediaControllerFuture = null
|
||||||
mediaController?.release()
|
mediaController?.release()
|
||||||
mediaController = null
|
mediaController = null
|
||||||
}
|
}
|
||||||
@@ -202,14 +213,18 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
updatePidAndGaid()
|
updatePidAndGaid()
|
||||||
getEventPopup()
|
getEventPopup()
|
||||||
|
|
||||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(
|
playerStateJob = lifecycleScope.launch {
|
||||||
preferenceChangeListener
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
)
|
SharedPreferenceManager.isPlayerServiceRunningFlow.collect { isRunning ->
|
||||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
if (isRunning) {
|
||||||
initAndVisibleMiniPlayer()
|
handler.removeCallbacks(showMiniPlayerRunnable)
|
||||||
|
handler.postDelayed(showMiniPlayerRunnable, 1500)
|
||||||
} else {
|
} else {
|
||||||
deInitMiniPlayer()
|
deInitMiniPlayer()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||||
}
|
}
|
||||||
@@ -217,7 +232,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
deInitMiniPlayer()
|
deInitMiniPlayer()
|
||||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
playerStateJob?.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
package kr.co.vividnext.sodalive.settings.language
|
package kr.co.vividnext.sodalive.settings.language
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.edit
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
|
|
||||||
object LanguageManager {
|
object LanguageManager {
|
||||||
const val LANG_KO = "ko"
|
const val LANG_KO = "ko"
|
||||||
const val LANG_EN = "en"
|
const val LANG_EN = "en"
|
||||||
const val LANG_JA = "ja"
|
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) {
|
fun isSupported(code: String): Boolean = when (code) {
|
||||||
LANG_KO, LANG_EN, LANG_JA -> true
|
LANG_KO, LANG_EN, LANG_JA -> true
|
||||||
else -> false
|
else -> false
|
||||||
@@ -25,8 +18,9 @@ object LanguageManager {
|
|||||||
* 사용자가 앱 내에서 명시적으로 선택한 언어 코드를 반환한다. 없으면 null.
|
* 사용자가 앱 내에서 명시적으로 선택한 언어 코드를 반환한다. 없으면 null.
|
||||||
*/
|
*/
|
||||||
fun getUserSelectedLanguageOrNull(context: Context): String? {
|
fun getUserSelectedLanguageOrNull(context: Context): String? {
|
||||||
val code = prefs(context).getString(PREF_KEY_APP_LANGUAGE, null)
|
SharedPreferenceManager.init(context.applicationContext)
|
||||||
return code?.takeIf { it.isNotBlank() }
|
val code = SharedPreferenceManager.appLanguageCode
|
||||||
|
return code.takeIf { it.isNotBlank() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +58,7 @@ object LanguageManager {
|
|||||||
|
|
||||||
fun setSelectedLanguage(context: Context, code: String) {
|
fun setSelectedLanguage(context: Context, code: String) {
|
||||||
val normalized = if (isSupported(code)) code else LANG_KO
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
docs/20260309_DataStore전환및SharedPreferences마이그레이션.md
Normal file
57
docs/20260309_DataStore전환및SharedPreferences마이그레이션.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# DataStore 전환 및 SharedPreferences 마이그레이션
|
||||||
|
|
||||||
|
- [x] 공식 문서의 DataStore 권고 문구 확인 및 근거 정리
|
||||||
|
- [x] 프로젝트 내 SharedPreferences 사용 지점 식별
|
||||||
|
- [x] `SharedPreferenceManager`를 DataStore 기반으로 전환
|
||||||
|
- [x] `LanguageManager`를 DataStore 기반 읽기/쓰기로 전환
|
||||||
|
- [x] 채팅방 `chat_room_prefs`를 DataStore + 마이그레이션으로 전환
|
||||||
|
- [x] 앱 초기화 경로에 DataStore 초기화 반영
|
||||||
|
- [x] 진단/테스트/빌드 검증 결과 기록
|
||||||
|
- [x] 핵심 런타임 회귀 자동 테스트(androidTest) 추가
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 2026-03-09
|
||||||
|
- 무엇/왜/어떻게: Android 공식 문구를 확인한 뒤, 기본 설정 저장소(`SharedPreferenceManager`)와 채팅방 전용 저장소(`chat_room_prefs`)를 각각 DataStore로 전환하고 `SharedPreferencesMigration`으로 기존 설치 기기의 데이터가 손실 없이 이관되도록 구현했다. 기존 호출부 대량 수정을 피하기 위해 기존 매니저 API를 유지하고 내부 저장소만 교체했다.
|
||||||
|
- 공식 문서 근거: "If you're using `SharedPreferences` to store data, consider migrating to DataStore instead." (`App Architecture: Data Layer - DataStore - Android Developers`, https://developer.android.com/topic/libraries/architecture/datastore)
|
||||||
|
- 실행 명령: `lsp_diagnostics` (변경 Kotlin 파일 8개)
|
||||||
|
- 결과: 현재 실행 환경에 Kotlin LSP 서버가 없어 진단 불가(`No LSP server configured for extension: .kt`).
|
||||||
|
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||||
|
- 결과: `BUILD SUCCESSFUL` (단위 테스트/디버그 빌드 성공).
|
||||||
|
- 실행 명령: Oracle 리뷰(`bg_0bc7de61`)
|
||||||
|
- 결과: 리스너 콜백 쓰레드 호환성 리스크(백그라운드 스레드에서 UI 리스너 호출 가능)와 읽기 전용 `SharedPreferences` 뷰의 no-op `edit()` 리스크를 확인했고, `SharedPreferenceManager`에서 메인 스레드 디스패치 및 fail-fast `edit()` 예외 처리로 반영했다.
|
||||||
|
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` (Oracle 피드백 반영 후 재검증)
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 실행 명령: `./gradlew :app:lintDebug`
|
||||||
|
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml`의 `MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 중단되었고, 재실행에서도 동일 결과다.
|
||||||
|
- 실행 명령: `./gradlew :app:ktlintCheck`
|
||||||
|
- 결과: 실패. 기존 코드베이스 전반의 스타일 이슈(다수 파일)로 `:app:ktlintMainSourceSetCheck` 실패.
|
||||||
|
- 무엇/왜/어떻게: 요청에 따라 "첫 접근 시 이관" 지점 주석을 추가했다. `AppPreferencesDataStoreProvider`의 `SharedPreferencesMigration` 등록부와 `SharedPreferenceManager`/`ChatRoomPreferenceManager`의 `dataStore.data.first()` 트리거 지점에 주석을 배치해, 언제 이관이 실행되는지 코드를 읽는 즉시 파악할 수 있게 했다.
|
||||||
|
- 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest`
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 무엇/왜/어떻게: 사용자 요청에 맞춰 "전체 일괄 치환" 대신 값 변화 반응 지점과 즉시 사용 안정성이 중요한 지점을 우선 리팩터링했다. `SharedPreferenceManager`/`ChatRoomPreferenceManager` 내부 `runBlocking`을 제거하고 비동기 수집 + 메모리 캐시 기반으로 변경했으며, 기존 `OnSharedPreferenceChangeListener` 의존 화면 4개를 `Flow` 수집으로 전환했다.
|
||||||
|
- 반응형 전환 파일: `MainActivity`, `HomeFragment`, `LiveFragment`, `AudioContentPlaylistDetailActivity`에서 `isPlayerServiceRunningFlow`/`roleFlow`를 `repeatOnLifecycle`로 수집해 UI를 갱신하도록 변경.
|
||||||
|
- 실행 명령: `./gradlew :app:assembleDebug :app:testDebugUnitTest` (비동기 리팩터링 반영 후)
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 실행 명령: `./gradlew :app:lintDebug` (최종)
|
||||||
|
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml`의 `MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.
|
||||||
|
|
||||||
|
### 2026-03-11
|
||||||
|
- 무엇/왜/어떻게: 핵심 런타임 회귀 2건을 수정했다. (1) `SharedPreferenceManager`/`ChatRoomPreferenceManager`가 초기 캐시 반영 전에 기본값을 반환하던 문제를 막기 위해 `init()`에서 `dataStore.data.first()`로 초기 스냅샷을 먼저 로딩한 뒤 `initialized`를 설정하도록 조정했다. (2) `repeatOnLifecycle(STARTED)` 재수집 시 미니플레이어가 중복 연결되던 문제를 막기 위해 `MainActivity`/`AudioContentPlaylistDetailActivity`에 `mediaControllerFuture` 가드와 지연 실행 runnable 정리(`removeCallbacks`)를 추가했다.
|
||||||
|
- 반영 파일: `SharedPreferenceManager.kt`, `ChatRoomPreferenceManager.kt`, `MainActivity.kt`, `AudioContentPlaylistDetailActivity.kt`.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (변경 Kotlin 파일 4개)
|
||||||
|
- 결과: 현재 실행 환경에 Kotlin LSP 서버가 없어 진단 불가(`No LSP server configured for extension: .kt`).
|
||||||
|
- 실행 명령: `./gradlew --stop && ./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 실행 명령: `./gradlew :app:lintDebug`
|
||||||
|
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml`의 `MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.
|
||||||
|
- 무엇/왜/어떻게: 수동 재현에 의존하지 않도록 런타임 회귀 자동 검증을 위한 계측 테스트를 추가했다. `DataStoreRuntimeRegressionTest`에서 초기 스냅샷 재초기화 경로를 검증하고, `MiniPlayerConnectionGuardTest`에서 `mediaControllerFuture`가 이미 존재할 때 `connectPlayerService()`가 조기 반환되는 가드를 검증한다. 이를 위해 `androidTest` 실행 환경(`testInstrumentationRunner`, `androidTestImplementation`)을 설정했고, 테스트 재초기화를 위해 `SharedPreferenceManager`/`ChatRoomPreferenceManager`/`AppPreferencesDataStoreProvider`에 `resetForTest()` 훅을 추가했다.
|
||||||
|
- 반영 파일: `app/build.gradle`, `AppPreferencesDataStoreProvider.kt`, `SharedPreferenceManager.kt`, `ChatRoomPreferenceManager.kt`, `app/src/androidTest/java/kr/co/vividnext/sodalive/runtime/DataStoreRuntimeRegressionTest.kt`, `app/src/androidTest/java/kr/co/vividnext/sodalive/runtime/MiniPlayerConnectionGuardTest.kt`.
|
||||||
|
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug :app:assembleDebugAndroidTest`
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 실행 명령: `./gradlew :app:connectedDebugAndroidTest`
|
||||||
|
- 결과: `BUILD SUCCESSFUL` (SM-G960N - 10, 4 tests passed).
|
||||||
|
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||||
|
- 결과: `BUILD SUCCESSFUL`.
|
||||||
|
- 실행 명령: `./gradlew :app:lintDebug`
|
||||||
|
- 결과: 실패. 기존 프로젝트 이슈(`AndroidManifest.xml`의 `MissingClass(com.facebook.FacebookActivity)` 포함 18 errors, 577 warnings)로 동일하게 중단.
|
||||||
Reference in New Issue
Block a user