refactor(preferences): DataStore 설정 저장 안정성을 높인다

This commit is contained in:
2026-03-11 12:32:34 +09:00
parent 8e1dabbb80
commit 418b734c3f
16 changed files with 847 additions and 285 deletions

View File

@@ -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"
}
}

View File

@@ -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
}
}