From cf3072124ce93d70ecaeb3e3d128d547c6790b18 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:52:24 +0530 Subject: [PATCH 1/9] Introduced custom ProtoDataStore to ensure type-safety --- .../repository/preferences/AppPreferences.kt | 16 +- .../preferences/AppPreferencesImpl.kt | 230 +++++------------- .../preferences/DataStorePreference.kt | 16 -- .../preferences/DataStoreSerializer.kt | 14 ++ .../repository/preferences/ProtoDataStore.kt | 66 +++++ .../android/util/DataStoreSerializers.kt | 37 +++ .../util/migrations/BlacklistMigration.kt | 36 +++ sharedTest/build.gradle | 1 + .../sharedtest/mocks/MockAppPreferences.kt | 121 +++++---- 9 files changed, 278 insertions(+), 259 deletions(-) delete mode 100644 app/src/main/java/org/listenbrainz/android/repository/preferences/DataStorePreference.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt create mode 100644 app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt create mode 100644 app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt index f1c4bf27..f7893738 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt @@ -3,11 +3,13 @@ package org.listenbrainz.android.repository.preferences import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode +import org.listenbrainz.android.repository.preferences.ProtoDataStore.DataStorePreference +import org.listenbrainz.android.repository.preferences.ProtoDataStore.PrimitiveDataStorePreference import org.listenbrainz.android.util.LinkedService interface AppPreferences { - val themePreference: DataStorePreference + val themePreference: DataStorePreference /** * @@ -21,10 +23,10 @@ interface AppPreferences { var permissionsPreference: String? /** Whitelist for ListenSubmissionService.*/ - val listeningWhitelist: DataStorePreference> + val listeningWhitelist: DataStorePreference, String> /** Music Apps in users device registered by listenService.*/ - val listeningApps: DataStorePreference> + val listeningApps: DataStorePreference,String> var onboardingCompleted: Boolean @@ -40,19 +42,19 @@ interface AppPreferences { suspend fun isUserLoggedIn() : Boolean /****ListenBrainz User Token:** User has to manually fill this token.*/ - val lbAccessToken: DataStorePreference + val lbAccessToken: PrimitiveDataStorePreference - val username: DataStorePreference + val username: PrimitiveDataStorePreference val refreshToken: String? var linkedServices: List /** Default is true. */ - val isListeningAllowed: DataStorePreference + val isListeningAllowed: PrimitiveDataStorePreference /** Default is true. */ - val shouldListenNewPlayers: DataStorePreference + val shouldListenNewPlayers: PrimitiveDataStorePreference val isNotificationServiceAllowed: Boolean diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt index 274b4877..f19772cf 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt @@ -3,12 +3,10 @@ package org.listenbrainz.android.repository.preferences import android.content.Context import android.content.pm.PackageManager import android.provider.Settings -import androidx.datastore.core.DataMigration 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.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.preference.PreferenceManager @@ -22,10 +20,8 @@ import kotlinx.coroutines.withContext import org.listenbrainz.android.model.PermissionStatus import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode -import org.listenbrainz.android.model.UiMode.Companion.asUiMode import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.IS_LISTENING_ALLOWED import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.LISTENING_APPS -import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.LISTENING_BLACKLIST import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.LISTENING_WHITELIST import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.SHOULD_LISTEN_NEW_PLAYERS import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys.THEME @@ -47,59 +43,35 @@ import org.listenbrainz.android.util.Constants.Strings.REFRESH_TOKEN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import org.listenbrainz.android.util.Constants.Strings.USERNAME +import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer +import org.listenbrainz.android.util.DataStoreSerializers.themeSerializer import org.listenbrainz.android.util.LinkedService import org.listenbrainz.android.util.TypeConverter +import org.listenbrainz.android.util.migrations.blacklistMigration -class AppPreferencesImpl(private val context: Context): AppPreferences { +private val Context.dataStore: DataStore by preferencesDataStore( + name = "settings", + produceMigrations = { context -> + // Since we're migrating from SharedPreferences, add a migration based on the + // SharedPreferences name + listOf(SharedPreferencesMigration( + context, + context.packageName + "_preferences", + setOf( + LB_ACCESS_TOKEN, + USERNAME, + PREFERENCE_SYSTEM_THEME, + PREFERENCE_LISTENING_APPS + ) + ), blacklistMigration) + } +) + +class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context.dataStore), AppPreferences { companion object { private val gson = Gson() - private val blacklistMigration: DataMigration = - object: DataMigration { - override suspend fun cleanUp() = Unit - - override suspend fun shouldMigrate(currentData: Preferences): Boolean { - // If blacklist is deleted, then we are sure that migration took place. - return currentData.contains(LISTENING_BLACKLIST) - } - - override suspend fun migrate(currentData: Preferences): Preferences { - val blacklist = currentData[LISTENING_BLACKLIST].asStringList() - val appList = currentData[LISTENING_APPS].asStringList() - - val whitelist = currentData[LISTENING_WHITELIST].asStringList().toMutableSet() - appList.forEach { pkg -> - if (!blacklist.contains(pkg)) { - whitelist.add(pkg) - } - } - - val mutablePreferences = currentData.toMutablePreferences() - mutablePreferences[LISTENING_WHITELIST] = Gson().toJson(whitelist.toList()) - mutablePreferences.remove(LISTENING_BLACKLIST) // Clear old stale data and key. - - return mutablePreferences.toPreferences() - } - } - - private val Context.dataStore: DataStore by preferencesDataStore( - name = "settings", - produceMigrations = { context -> - // Since we're migrating from SharedPreferences, add a migration based on the - // SharedPreferences name - listOf(SharedPreferencesMigration( - context, - context.packageName + "_preferences", - setOf( - LB_ACCESS_TOKEN, - USERNAME, - PREFERENCE_SYSTEM_THEME, - PREFERENCE_LISTENING_APPS - ) - ), blacklistMigration) - } - ) - private object PreferenceKeys { + object PreferenceKeys { val LB_ACCESS_TOKEN = stringPreferencesKey(Constants.Strings.LB_ACCESS_TOKEN) val USERNAME = stringPreferencesKey(Constants.Strings.USERNAME) val LISTENING_BLACKLIST = stringPreferencesKey(PREFERENCE_LISTENING_BLACKLIST) @@ -109,13 +81,6 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { val IS_LISTENING_ALLOWED = booleanPreferencesKey(PREFERENCE_SUBMIT_LISTENS) val SHOULD_LISTEN_NEW_PLAYERS = booleanPreferencesKey(PREFERENCE_LISTEN_NEW_PLAYERS) } - - fun String?.asStringList(): List { - return gson.fromJson( - this, - object: TypeToken>() {}.type - ) ?: emptyList() - } } private val preferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -142,47 +107,23 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { editor.apply() } - private val datastore: Flow - get() = context.dataStore.data - // Preferences Implementation - override val themePreference: DataStorePreference - get() = object : DataStorePreference { - override fun getFlow(): Flow = - datastore.map { it[THEME].asUiMode() } - - override suspend fun set(value: UiMode) { - context.dataStore.edit { it[THEME] = value.name } - } - } + override val themePreference: DataStorePreference + get() = object : DataStorePreference( + key = THEME, + serializer = themeSerializer + ) {} override var permissionsPreference: String? get() = preferences.getString(PREFERENCE_PERMS, PermissionStatus.NOT_REQUESTED.name) set(value) = setString(PREFERENCE_PERMS, value) - override val listeningWhitelist: DataStorePreference> - get() = object: DataStorePreference> { - override fun getFlow(): Flow> = - datastore.map { prefs -> - prefs[LISTENING_WHITELIST].asStringList() - } - - override suspend fun set(value: List) { - context.dataStore.edit { prefs -> - prefs[LISTENING_WHITELIST] = gson.toJson(value) - } - } - - override suspend fun getAndUpdate(update: (List) -> List) { - context.dataStore.updateData { - val updatedValue = update(it[LISTENING_WHITELIST].asStringList()) - val mutablePrefs = it.toMutablePreferences() - mutablePrefs[LISTENING_WHITELIST] = gson.toJson(updatedValue) - return@updateData mutablePrefs - } - } - } + override val listeningWhitelist: DataStorePreference, String> + get() = object: DataStorePreference, String>( + key = LISTENING_WHITELIST, + serializer = stringListSerializer + ) {} override val isNotificationServiceAllowed: Boolean get() { @@ -190,56 +131,23 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { return listeners != null && listeners.contains(context.packageName) } - override val isListeningAllowed: DataStorePreference - get() = object: DataStorePreference { - override fun getFlow(): Flow = - datastore.map { prefs -> - prefs[IS_LISTENING_ALLOWED] ?: true - } - - override suspend fun set(value: Boolean) { - context.dataStore.edit { prefs -> - prefs[IS_LISTENING_ALLOWED] = value - } - } - } - - override val shouldListenNewPlayers: DataStorePreference - get() = object : DataStorePreference { - override fun getFlow(): Flow = - datastore.map { prefs -> - prefs[SHOULD_LISTEN_NEW_PLAYERS] ?: true - } - - override suspend fun set(value: Boolean) { - context.dataStore.edit { prefs -> - prefs[SHOULD_LISTEN_NEW_PLAYERS] = value - } - } - } + override val isListeningAllowed: PrimitiveDataStorePreference + get() = object: PrimitiveDataStorePreference( + key = IS_LISTENING_ALLOWED, + defaultValue = true + ) {} - override val listeningApps: DataStorePreference> - get() = object: DataStorePreference> { - override fun getFlow(): Flow> = - datastore.map { prefs -> - prefs[LISTENING_APPS].asStringList() - } - - override suspend fun set(value: List) { - context.dataStore.edit { prefs -> - prefs[LISTENING_APPS] = gson.toJson(value) - } - } + override val shouldListenNewPlayers: PrimitiveDataStorePreference + get() = object: PrimitiveDataStorePreference( + key = SHOULD_LISTEN_NEW_PLAYERS, + defaultValue = true + ) {} - override suspend fun getAndUpdate(update: (List) -> List) { - context.dataStore.updateData { - val updatedValue = update(it[LISTENING_APPS].asStringList()) - val mutablePrefs = it.toMutablePreferences() - mutablePrefs[LISTENING_APPS] = gson.toJson(updatedValue) - return@updateData mutablePrefs - } - } - } + override val listeningApps: DataStorePreference, String> + get() = object: DataStorePreference, String>( + key = LISTENING_APPS, + serializer = stringListSerializer + ) {} override val version: String get() = try { @@ -253,7 +161,7 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { get() = preferences.getBoolean(ONBOARDING, false) set(value) = setBoolean(ONBOARDING, value) - override suspend fun logoutUser() = withContext(Dispatchers.IO) { + override suspend fun logoutUser(): Unit = withContext(Dispatchers.IO) { val editor = preferences.edit() editor.remove(REFRESH_TOKEN) editor.remove(USERNAME) @@ -285,35 +193,17 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { override suspend fun isUserLoggedIn() : Boolean = lbAccessToken.get().isNotEmpty() - override val lbAccessToken: DataStorePreference - get() = object : DataStorePreference { - override fun getFlow(): Flow = - datastore.map { prefs -> - prefs[PreferenceKeys.LB_ACCESS_TOKEN] ?: "" - } - - override suspend fun set(value: String) { - context.dataStore.edit { prefs -> - prefs[PreferenceKeys.LB_ACCESS_TOKEN] = value - } - } - - } - - override val username: DataStorePreference - get() = object: DataStorePreference { - override fun getFlow(): Flow = - datastore.map { prefs -> - prefs[PreferenceKeys.USERNAME] ?: "" - } - - override suspend fun set(value: String) { - context.dataStore.edit { prefs -> - prefs[PreferenceKeys.USERNAME] = value ?: "" - } - } - - } + override val lbAccessToken: PrimitiveDataStorePreference + get() = object : PrimitiveDataStorePreference( + key = PreferenceKeys.LB_ACCESS_TOKEN, + defaultValue = "" + ) {} + + override val username: PrimitiveDataStorePreference + get() = object : PrimitiveDataStorePreference( + key = PreferenceKeys.USERNAME, + defaultValue = "" + ) {} override var linkedServices: List get() { diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStorePreference.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStorePreference.kt deleted file mode 100644 index 2b79e4e7..00000000 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStorePreference.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.listenbrainz.android.repository.preferences - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first - -interface DataStorePreference { - suspend fun get(): T = getFlow().first() - - fun getFlow(): Flow - - suspend fun set(value: T) - - /** Update the value of the preference in atomic read-modify-write manner.*/ - suspend fun getAndUpdate(update: (T) -> T): Unit = - throw NotImplementedError("getAndUpdate has not been implemented for this preference.") -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt new file mode 100644 index 00000000..7d8c7d83 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.repository.preferences + +/** @property T Object or higher type. + * @property R Primitive or serializable type.*/ +interface DataStoreSerializer { + /** Convert from Primitive [R] to Object [T].*/ + fun from(value: R): T + + /** Convert to Primitive [T] to Object [R].*/ + fun to(value: T): R + + /** Default value for errors and null values.*/ + fun default(): T +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt new file mode 100644 index 00000000..90c4a08b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt @@ -0,0 +1,66 @@ +package org.listenbrainz.android.repository.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.util.Resource +import java.io.IOException + +abstract class ProtoDataStore(private val dataStore: DataStore) { + private fun defaultSerializer(defaultValue: T): DataStoreSerializer = + object: DataStoreSerializer { + override fun from(value: T): T = value + override fun to(value: T): T = value + override fun default(): T = defaultValue + } + + /** [DataStorePreference]s which are primitive in nature can use this class.*/ + abstract inner class PrimitiveDataStorePreference( + key: Preferences.Key, + defaultValue: T + ): DataStorePreference(key, defaultSerializer(defaultValue)) + + + /** A [DataStore] preference can be declared type-safe by making an object of this class. + * + * Every function can be overridden.*/ + abstract inner class DataStorePreference( + private val key: Preferences.Key, + private val serializer: DataStoreSerializer + ) { + open suspend fun get(): T = getFlow().firstOrNull() ?: serializer.default() + + open fun getFlow(): Flow = + dataStore.data.map { prefs -> + prefs[key]?.let { serializer.from(it) } ?: serializer.default() + }.catch { + if (it is IOException) + emit(serializer.default()) + else + throw it + } + + /** @return [Resource] If the value was updated or not.*/ + open suspend fun set(value: T): Resource = + try { + dataStore.edit { it[key] = serializer.to(value) } + Resource.success(value) + } catch (e: IOException) { + Resource.failure(error = ResponseError.FILE_NOT_FOUND.apply { actualResponse = e.localizedMessage }) + } + + /** Update the value of the preference in an atomic read-modify-write manner.*/ + open suspend fun getAndUpdate(update: (T) -> T) = + dataStore.updateData { prefs -> + val mutablePrefs = prefs.toMutablePreferences() + val currentValue = prefs[key]?.let { serializer.from(it) } ?: serializer.default() + mutablePrefs[key] = serializer.to(update(currentValue)) + return@updateData mutablePrefs + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt b/app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt new file mode 100644 index 00000000..08dc76e6 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt @@ -0,0 +1,37 @@ +package org.listenbrainz.android.util + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.listenbrainz.android.model.UiMode +import org.listenbrainz.android.model.UiMode.Companion.asUiMode +import org.listenbrainz.android.repository.preferences.DataStoreSerializer + +object DataStoreSerializers { + private val gson = Gson() + /** Serializes to String primitive.*/ + private fun gsonSerializer(defaultValue: T): DataStoreSerializer = + object: DataStoreSerializer { + override fun from(value: String): T = + gson.fromJson( + value, + object: TypeToken() {}.type + ) ?: defaultValue + + override fun to(value: T): String = gson.toJson(value) + + override fun default(): T = defaultValue + } + + val themeSerializer: DataStoreSerializer + get() = object: DataStoreSerializer { + override fun from(value: String): UiMode = value.asUiMode() + + override fun to(value: UiMode): String = value.name + + override fun default(): UiMode = UiMode.FOLLOW_SYSTEM + } + + + val stringListSerializer: DataStoreSerializer, String> + get() = gsonSerializer(emptyList()) +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt new file mode 100644 index 00000000..29d84d69 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt @@ -0,0 +1,36 @@ +package org.listenbrainz.android.util.migrations + +import androidx.datastore.core.DataMigration +import androidx.datastore.preferences.core.Preferences +import com.google.gson.Gson +import org.listenbrainz.android.repository.preferences.AppPreferencesImpl +import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys +import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer + +val blacklistMigration: DataMigration = + object: DataMigration { + override suspend fun cleanUp() = Unit + + override suspend fun shouldMigrate(currentData: Preferences): Boolean { + // If blacklist is deleted, then we are sure that migration took place. + return currentData.contains(AppPreferencesImpl.Companion.PreferenceKeys.LISTENING_BLACKLIST) + } + + override suspend fun migrate(currentData: Preferences): Preferences { + val blacklist = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_BLACKLIST] ?: "") + val appList = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_APPS] ?: "") + + val whitelist = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_WHITELIST] ?: "").toMutableSet() + appList.forEach { pkg -> + if (!blacklist.contains(pkg)) { + whitelist.add(pkg) + } + } + + val mutablePreferences = currentData.toMutablePreferences() + mutablePreferences[PreferenceKeys.LISTENING_WHITELIST] = Gson().toJson(whitelist.toList()) + mutablePreferences.remove(PreferenceKeys.LISTENING_BLACKLIST) // Clear old stale data and key. + + return mutablePreferences.toPreferences() + } + } \ No newline at end of file diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index 2c0ef958..69fbfaa3 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -31,6 +31,7 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' + implementation "androidx.datastore:datastore-preferences:1.0.0" //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index d89af250..88089915 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -1,18 +1,34 @@ package org.listenbrainz.sharedtest.mocks +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import org.listenbrainz.android.model.PermissionStatus import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.repository.preferences.AppPreferences -import org.listenbrainz.android.repository.preferences.DataStorePreference +import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys +import org.listenbrainz.android.repository.preferences.ProtoDataStore import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN +import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer +import org.listenbrainz.android.util.DataStoreSerializers.themeSerializer import org.listenbrainz.android.util.LinkedService import org.listenbrainz.sharedtest.utils.EntityTestUtils.testAccessToken import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername -/* +private val mockDataStore: DataStore = object : DataStore { + override val data: Flow + get() = flow {} + + /** Should NOT be called. Always override [ProtoDataStore.DataStorePreference.getAndUpdate] in mock.*/ + override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { + return transform(data.first()) + } +} + +/** For every new preference, add default value of the concerned shared preference as default value here. */ @@ -26,74 +42,49 @@ class MockAppPreferences( override val version: String = "", override val isNotificationServiceAllowed: Boolean = true, override var linkedServices: List = listOf() -) : AppPreferences { +): ProtoDataStore(dataStore = mockDataStore), AppPreferences { - override val themePreference: DataStorePreference = - object : DataStorePreference { - override fun getFlow(): Flow = flow { emit(UiMode.FOLLOW_SYSTEM) } - - override suspend fun set(value: UiMode) { - TODO("Not yet implemented") - } - } + override val themePreference: DataStorePreference = + object: DataStorePreference( + key = PreferenceKeys.THEME, + serializer = themeSerializer + ) {} - override val listeningWhitelist: DataStorePreference> = - object : DataStorePreference> { - override fun getFlow(): Flow> = flow {} - - override suspend fun set(value: List) { - TODO("Not yet implemented") - } - } + override val listeningWhitelist: DataStorePreference, String> = + object : DataStorePreference, String>( + key = PreferenceKeys.LISTENING_WHITELIST, + serializer = stringListSerializer.apply { } + ) {} - override val listeningApps: DataStorePreference> = - object : DataStorePreference> { - override fun getFlow(): Flow> = flow {} - - override suspend fun set(value: List) { - TODO("Not yet implemented") - } - } + override val listeningApps: DataStorePreference, String> = + object : DataStorePreference, String>( + key = PreferenceKeys.LISTENING_APPS, + serializer = stringListSerializer + ) {} - override val lbAccessToken: DataStorePreference = - object : DataStorePreference { - override fun getFlow(): Flow = flow { - emit(testAccessToken) - } + override val lbAccessToken: PrimitiveDataStorePreference = + object : PrimitiveDataStorePreference( + key = PreferenceKeys.LB_ACCESS_TOKEN, + defaultValue = testAccessToken + ) {} - override suspend fun set(value: String) { - TODO("Not yet implemented") - } - } + override val username: PrimitiveDataStorePreference = + object : PrimitiveDataStorePreference( + key = PreferenceKeys.USERNAME, + defaultValue = testUsername + ) {} - override val username: DataStorePreference = - object : DataStorePreference { - override fun getFlow(): Flow = flow { - emit(testUsername) - } - - override suspend fun set(value: String) { - TODO("Not yet implemented") - } - } + override val isListeningAllowed: PrimitiveDataStorePreference = + object : PrimitiveDataStorePreference( + key = PreferenceKeys.IS_LISTENING_ALLOWED, + defaultValue = true + ) {} - override val isListeningAllowed: DataStorePreference = - object : DataStorePreference { - override fun getFlow(): Flow = flow {} - - override suspend fun set(value: Boolean) { - TODO("Not yet implemented") - } - } - - override val shouldListenNewPlayers: DataStorePreference = - object : DataStorePreference { - override fun getFlow(): Flow = flow {} - - override suspend fun set(value: Boolean) { - TODO("Not yet implemented") - } - } + override val shouldListenNewPlayers: PrimitiveDataStorePreference = + object : PrimitiveDataStorePreference ( + key = PreferenceKeys.SHOULD_LISTEN_NEW_PLAYERS, + defaultValue = true + ) {} override suspend fun logoutUser() { @@ -104,7 +95,5 @@ class MockAppPreferences( emit(STATUS_LOGGED_IN) } - override suspend fun isUserLoggedIn(): Boolean { - TODO("Not yet implemented") - } + override suspend fun isUserLoggedIn(): Boolean = true } \ No newline at end of file From dc168fd4b68bac0030a4a27d84df21596e50eb76 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:08:31 +0530 Subject: [PATCH 2/9] Moving files around to meaningful folders --- .../android/di/brainzplayer/ServiceModule.kt | 4 ++-- .../brainzplayer/AlbumRepositoryImpl.kt | 4 ++-- .../brainzplayer/ArtistRepositoryImpl.kt | 4 ++-- .../brainzplayer/SongRepositoryImpl.kt | 2 +- .../repository/preferences/AppPreferences.kt | 4 ++-- .../preferences/AppPreferencesImpl.kt | 22 +++++++++---------- .../BrainzPlayerNotificationListener.kt | 2 +- .../android/service/BrainzPlayerService.kt | 10 ++++----- .../service/BrainzPlayerServiceConnection.kt | 2 +- .../android/service/MusicPlaybackPreparer.kt | 2 +- .../android/ui/components/Buttons.kt | 2 +- .../ui/screens/brainzplayer/AlbumScreen.kt | 2 +- .../ui/screens/brainzplayer/ArtistScreen.kt | 2 +- .../BrainzPlayerBackDropScreen.kt | 6 ++--- .../ui/screens/brainzplayer/PlaylistScreen.kt | 2 +- .../ui/screens/brainzplayer/SongScreen.kt | 2 +- .../screens/brainzplayer}/SongViewPager.kt | 22 +++++++++++++++---- .../util/{ => brainzplayer}/AlbumsData.kt | 2 +- .../BrainzPlayerExtensions.kt | 2 +- .../BrainzPlayerNotificationManager.kt | 8 +++---- .../{ => brainzplayer}/BrainzPlayerUtils.kt | 2 +- .../{ => brainzplayer}/LocalMusicSource.kt | 2 +- .../util/{ => brainzplayer}/MusicSource.kt | 2 +- .../util/{ => brainzplayer}/SongsData.kt | 2 +- .../datastore}/DataStoreSerializer.kt | 2 +- .../{ => datastore}/DataStoreSerializers.kt | 7 ++++-- .../datastore}/ProtoDataStore.kt | 2 +- .../migrations/BlacklistMigration.kt | 4 ++-- .../viewmodel/BrainzPlayerViewModel.kt | 12 +++++----- .../sharedtest/mocks/MockAppPreferences.kt | 6 ++--- 30 files changed, 80 insertions(+), 67 deletions(-) rename app/src/main/java/org/listenbrainz/android/{util => ui/screens/brainzplayer}/SongViewPager.kt (89%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/AlbumsData.kt (97%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/BrainzPlayerExtensions.kt (99%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/BrainzPlayerNotificationManager.kt (92%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/BrainzPlayerUtils.kt (82%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/LocalMusicSource.kt (98%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/MusicSource.kt (89%) rename app/src/main/java/org/listenbrainz/android/util/{ => brainzplayer}/SongsData.kt (98%) rename app/src/main/java/org/listenbrainz/android/{repository/preferences => util/datastore}/DataStoreSerializer.kt (86%) rename app/src/main/java/org/listenbrainz/android/util/{ => datastore}/DataStoreSerializers.kt (84%) rename app/src/main/java/org/listenbrainz/android/{repository/preferences => util/datastore}/ProtoDataStore.kt (97%) rename app/src/main/java/org/listenbrainz/android/util/{ => datastore}/migrations/BlacklistMigration.kt (91%) diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt index 446d946c..decd467e 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt @@ -14,8 +14,8 @@ import dagger.hilt.android.scopes.ServiceScoped import org.listenbrainz.android.repository.brainzplayer.AlbumRepository import org.listenbrainz.android.repository.brainzplayer.PlaylistRepository import org.listenbrainz.android.repository.brainzplayer.SongRepository -import org.listenbrainz.android.util.LocalMusicSource -import org.listenbrainz.android.util.MusicSource +import org.listenbrainz.android.util.brainzplayer.LocalMusicSource +import org.listenbrainz.android.util.brainzplayer.MusicSource @Module @InstallIn(ServiceComponent::class) diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt index b9d48833..5283943d 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt @@ -12,8 +12,8 @@ import kotlinx.coroutines.withContext import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Song import org.listenbrainz.android.model.dao.AlbumDao -import org.listenbrainz.android.util.AlbumsData -import org.listenbrainz.android.util.SongsData +import org.listenbrainz.android.util.brainzplayer.AlbumsData +import org.listenbrainz.android.util.brainzplayer.SongsData import org.listenbrainz.android.util.Transformer.toAlbum import org.listenbrainz.android.util.Transformer.toAlbumEntity diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt index 98cb14a6..f07da455 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt @@ -10,8 +10,8 @@ import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.Song import org.listenbrainz.android.model.dao.ArtistDao -import org.listenbrainz.android.util.AlbumsData -import org.listenbrainz.android.util.SongsData +import org.listenbrainz.android.util.brainzplayer.AlbumsData +import org.listenbrainz.android.util.brainzplayer.SongsData import org.listenbrainz.android.util.Transformer.toAlbumEntity import org.listenbrainz.android.util.Transformer.toArtist import org.listenbrainz.android.util.Transformer.toArtistEntity diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt index b65813b0..6312ab5c 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.listenbrainz.android.model.Song import org.listenbrainz.android.model.dao.SongDao -import org.listenbrainz.android.util.SongsData +import org.listenbrainz.android.util.brainzplayer.SongsData import org.listenbrainz.android.util.Transformer.toSong import org.listenbrainz.android.util.Transformer.toSongEntity import javax.inject.Inject diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt index f7893738..2bb6a851 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt @@ -3,9 +3,9 @@ package org.listenbrainz.android.repository.preferences import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode -import org.listenbrainz.android.repository.preferences.ProtoDataStore.DataStorePreference -import org.listenbrainz.android.repository.preferences.ProtoDataStore.PrimitiveDataStorePreference import org.listenbrainz.android.util.LinkedService +import org.listenbrainz.android.util.datastore.ProtoDataStore.DataStorePreference +import org.listenbrainz.android.util.datastore.ProtoDataStore.PrimitiveDataStorePreference interface AppPreferences { diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt index f19772cf..4d053766 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt @@ -10,8 +10,6 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.preference.PreferenceManager -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -43,11 +41,13 @@ import org.listenbrainz.android.util.Constants.Strings.REFRESH_TOKEN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import org.listenbrainz.android.util.Constants.Strings.USERNAME -import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer -import org.listenbrainz.android.util.DataStoreSerializers.themeSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.linkedServicesListSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.themeSerializer import org.listenbrainz.android.util.LinkedService +import org.listenbrainz.android.util.datastore.ProtoDataStore import org.listenbrainz.android.util.TypeConverter -import org.listenbrainz.android.util.migrations.blacklistMigration +import org.listenbrainz.android.util.datastore.migrations.blacklistMigration private val Context.dataStore: DataStore by preferencesDataStore( name = "settings", @@ -63,14 +63,13 @@ private val Context.dataStore: DataStore by preferencesDataStore( PREFERENCE_SYSTEM_THEME, PREFERENCE_LISTENING_APPS ) - ), blacklistMigration) + ), blacklistMigration + ) } ) class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context.dataStore), AppPreferences { companion object { - private val gson = Gson() - object PreferenceKeys { val LB_ACCESS_TOKEN = stringPreferencesKey(Constants.Strings.LB_ACCESS_TOKEN) val USERNAME = stringPreferencesKey(Constants.Strings.USERNAME) @@ -207,12 +206,11 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. override var linkedServices: List get() { - val jsonString = preferences.getString(LINKED_SERVICES, "") - val type = object : TypeToken>() {}.type - return gson.fromJson(jsonString, type) ?: emptyList() + val jsonString = preferences.getString(LINKED_SERVICES, "") ?: "" + return linkedServicesListSerializer.from(jsonString) } set(value) { - val jsonString = gson.toJson(value) + val jsonString = linkedServicesListSerializer.to(value) setString(LINKED_SERVICES, jsonString) } diff --git a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerNotificationListener.kt b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerNotificationListener.kt index fac7a298..345b9355 100644 --- a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerNotificationListener.kt +++ b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerNotificationListener.kt @@ -6,7 +6,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK import android.os.Build import androidx.core.content.ContextCompat import com.google.android.exoplayer2.ui.PlayerNotificationManager -import org.listenbrainz.android.util.BrainzPlayerUtils.NOTIFICATION_ID +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.NOTIFICATION_ID class BrainzPlayerNotificationListener(private val brainzPlayerService: BrainzPlayerService) : PlayerNotificationManager.NotificationListener { diff --git a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt index 5364f971..925c72ba 100644 --- a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt @@ -21,11 +21,11 @@ import org.listenbrainz.android.repository.brainzplayer.AlbumRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.brainzplayer.PlaylistRepository import org.listenbrainz.android.repository.brainzplayer.SongRepository -import org.listenbrainz.android.util.BrainzPlayerExtensions.toMediaMetadataCompat -import org.listenbrainz.android.util.BrainzPlayerNotificationManager -import org.listenbrainz.android.util.BrainzPlayerUtils.MEDIA_ROOT_ID -import org.listenbrainz.android.util.BrainzPlayerUtils.SERVICE_TAG -import org.listenbrainz.android.util.LocalMusicSource +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toMediaMetadataCompat +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerNotificationManager +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.MEDIA_ROOT_ID +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.SERVICE_TAG +import org.listenbrainz.android.util.brainzplayer.LocalMusicSource import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerServiceConnection.kt b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerServiceConnection.kt index 1d50fc46..7d3c3d66 100644 --- a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerServiceConnection.kt +++ b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerServiceConnection.kt @@ -17,7 +17,7 @@ import org.listenbrainz.android.BuildConfig import org.listenbrainz.android.model.PlayingTrack.Companion.toPlayingTrack import org.listenbrainz.android.model.RepeatMode import org.listenbrainz.android.repository.preferences.AppPreferences -import org.listenbrainz.android.util.BrainzPlayerExtensions.isPlaying +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.isPlaying import org.listenbrainz.android.util.ListenSubmissionState import org.listenbrainz.android.util.Resource diff --git a/app/src/main/java/org/listenbrainz/android/service/MusicPlaybackPreparer.kt b/app/src/main/java/org/listenbrainz/android/service/MusicPlaybackPreparer.kt index 07dc4f10..96459d61 100644 --- a/app/src/main/java/org/listenbrainz/android/service/MusicPlaybackPreparer.kt +++ b/app/src/main/java/org/listenbrainz/android/service/MusicPlaybackPreparer.kt @@ -7,7 +7,7 @@ import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.PlaybackStateCompat import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import org.listenbrainz.android.util.LocalMusicSource +import org.listenbrainz.android.util.brainzplayer.LocalMusicSource class MusicPlaybackPreparer(private val localMusicSource: LocalMusicSource, private val playerPrepared: (MediaMetadataCompat?) -> Unit) : MediaSessionConnector.PlaybackPreparer { diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/Buttons.kt b/app/src/main/java/org/listenbrainz/android/ui/components/Buttons.kt index d6019841..929ff4f1 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/Buttons.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/Buttons.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @OptIn(ExperimentalAnimationApi::class) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt index 0cd08efa..af20cc26 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt @@ -44,7 +44,7 @@ import org.listenbrainz.android.ui.components.BPLibraryEmptyMessage import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.AlbumViewModel import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt index eac62aef..990267f8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt @@ -48,7 +48,7 @@ import org.listenbrainz.android.ui.components.BPLibraryEmptyMessage import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.AlbumViewModel import org.listenbrainz.android.viewmodel.ArtistViewModel import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt index 9212601e..3e276357 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt @@ -1,7 +1,6 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import androidx.compose.animation.* import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -48,11 +47,10 @@ import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.PlayPauseIcon import org.listenbrainz.android.ui.components.SeekBar import org.listenbrainz.android.ui.screens.brainzplayer.ui.components.basicMarquee -import org.listenbrainz.android.util.BrainzPlayerExtensions.duration -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.duration +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.util.CacheService import org.listenbrainz.android.util.Constants.RECENTLY_PLAYED_KEY -import org.listenbrainz.android.util.SongViewPager import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.PlaylistViewModel import kotlin.math.absoluteValue diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt index c877ecbd..f8840e0d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt @@ -40,7 +40,7 @@ import org.listenbrainz.android.model.Playlist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.PlaylistViewModel diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt index e3f33aef..ca16e5ac 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt @@ -39,7 +39,7 @@ import org.listenbrainz.android.R import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.ui.components.BPLibraryEmptyMessage import org.listenbrainz.android.ui.components.forwardingPainter -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.PlaylistViewModel import org.listenbrainz.android.viewmodel.SongViewModel diff --git a/app/src/main/java/org/listenbrainz/android/util/SongViewPager.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongViewPager.kt similarity index 89% rename from app/src/main/java/org/listenbrainz/android/util/SongViewPager.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongViewPager.kt index c94f1892..b634cc57 100644 --- a/app/src/main/java/org/listenbrainz/android/util/SongViewPager.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongViewPager.kt @@ -1,17 +1,31 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.ui.screens.brainzplayer import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.BackdropScaffoldState +import androidx.compose.material.BackdropValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipPrevious +import androidx.compose.material.rememberBackdropScaffoldState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -35,7 +49,7 @@ import org.listenbrainz.android.R import org.listenbrainz.android.ui.components.PlayPauseIcon import org.listenbrainz.android.ui.components.SeekBar import org.listenbrainz.android.ui.screens.brainzplayer.ui.components.basicMarquee -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) diff --git a/app/src/main/java/org/listenbrainz/android/util/AlbumsData.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/AlbumsData.kt similarity index 97% rename from app/src/main/java/org/listenbrainz/android/util/AlbumsData.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/AlbumsData.kt index cf9fe6e7..fb7a690b 100644 --- a/app/src/main/java/org/listenbrainz/android/util/AlbumsData.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/AlbumsData.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.os.Build import android.provider.MediaStore diff --git a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerExtensions.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerExtensions.kt similarity index 99% rename from app/src/main/java/org/listenbrainz/android/util/BrainzPlayerExtensions.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerExtensions.kt index 9a5b4bd3..3d5a132d 100644 --- a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerExtensions.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerExtensions.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.content.ContentResolver import android.content.Context diff --git a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerNotificationManager.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerNotificationManager.kt similarity index 92% rename from app/src/main/java/org/listenbrainz/android/util/BrainzPlayerNotificationManager.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerNotificationManager.kt index af2d2a43..58ebc716 100644 --- a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerNotificationManager.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerNotificationManager.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.app.PendingIntent import android.content.Context @@ -10,9 +10,9 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ui.PlayerNotificationManager import kotlinx.coroutines.* import org.listenbrainz.android.R -import org.listenbrainz.android.util.BrainzPlayerExtensions.bitmap -import org.listenbrainz.android.util.BrainzPlayerUtils.NOTIFICATION_CHANNEL_ID -import org.listenbrainz.android.util.BrainzPlayerUtils.NOTIFICATION_ID +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.bitmap +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.NOTIFICATION_CHANNEL_ID +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.NOTIFICATION_ID class BrainzPlayerNotificationManager( private val context : Context, diff --git a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerUtils.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerUtils.kt similarity index 82% rename from app/src/main/java/org/listenbrainz/android/util/BrainzPlayerUtils.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerUtils.kt index 92cf30aa..5433de78 100644 --- a/app/src/main/java/org/listenbrainz/android/util/BrainzPlayerUtils.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/BrainzPlayerUtils.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer object BrainzPlayerUtils { const val SONG_COLLECTION = "songs" diff --git a/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/LocalMusicSource.kt similarity index 98% rename from app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/LocalMusicSource.kt index bd9c28da..5230a32e 100644 --- a/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/LocalMusicSource.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat diff --git a/app/src/main/java/org/listenbrainz/android/util/MusicSource.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/MusicSource.kt similarity index 89% rename from app/src/main/java/org/listenbrainz/android/util/MusicSource.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/MusicSource.kt index f82ca37f..0a5ed521 100644 --- a/app/src/main/java/org/listenbrainz/android/util/MusicSource.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/MusicSource.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaMetadataCompat diff --git a/app/src/main/java/org/listenbrainz/android/util/SongsData.kt b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/SongsData.kt similarity index 98% rename from app/src/main/java/org/listenbrainz/android/util/SongsData.kt rename to app/src/main/java/org/listenbrainz/android/util/brainzplayer/SongsData.kt index 3b646706..2ac83bd2 100644 --- a/app/src/main/java/org/listenbrainz/android/util/SongsData.kt +++ b/app/src/main/java/org/listenbrainz/android/util/brainzplayer/SongsData.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.brainzplayer import android.content.ContentUris import android.os.Build diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt similarity index 86% rename from app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt rename to app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt index 7d8c7d83..44fa8314 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/DataStoreSerializer.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.repository.preferences +package org.listenbrainz.android.util.datastore /** @property T Object or higher type. * @property R Primitive or serializable type.*/ diff --git a/app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt similarity index 84% rename from app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt rename to app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt index 08dc76e6..c8c3fce5 100644 --- a/app/src/main/java/org/listenbrainz/android/util/DataStoreSerializers.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt @@ -1,10 +1,10 @@ -package org.listenbrainz.android.util +package org.listenbrainz.android.util.datastore import com.google.gson.Gson import com.google.gson.reflect.TypeToken import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.model.UiMode.Companion.asUiMode -import org.listenbrainz.android.repository.preferences.DataStoreSerializer +import org.listenbrainz.android.util.LinkedService object DataStoreSerializers { private val gson = Gson() @@ -34,4 +34,7 @@ object DataStoreSerializers { val stringListSerializer: DataStoreSerializer, String> get() = gsonSerializer(emptyList()) + + val linkedServicesListSerializer: DataStoreSerializer, String> + get() = gsonSerializer(emptyList()) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt similarity index 97% rename from app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt rename to app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt index 90c4a08b..cec0ce7d 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/ProtoDataStore.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.repository.preferences +package org.listenbrainz.android.util.datastore import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences diff --git a/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt similarity index 91% rename from app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt rename to app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt index 29d84d69..277cb326 100644 --- a/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt @@ -1,11 +1,11 @@ -package org.listenbrainz.android.util.migrations +package org.listenbrainz.android.util.datastore.migrations import androidx.datastore.core.DataMigration import androidx.datastore.preferences.core.Preferences import com.google.gson.Gson import org.listenbrainz.android.repository.preferences.AppPreferencesImpl import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys -import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer val blacklistMigration: DataMigration = object: DataMigration { diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index 585ef8a6..34765913 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -27,12 +27,12 @@ import org.listenbrainz.android.repository.brainzplayer.SongRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.service.BrainzPlayerService import org.listenbrainz.android.service.BrainzPlayerServiceConnection -import org.listenbrainz.android.util.BrainzPlayerExtensions.currentPlaybackPosition -import org.listenbrainz.android.util.BrainzPlayerExtensions.isPlayEnabled -import org.listenbrainz.android.util.BrainzPlayerExtensions.isPlaying -import org.listenbrainz.android.util.BrainzPlayerExtensions.isPrepared -import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong -import org.listenbrainz.android.util.BrainzPlayerUtils.MEDIA_ROOT_ID +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.currentPlaybackPosition +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.isPlayEnabled +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.isPlaying +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.isPrepared +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.util.brainzplayer.BrainzPlayerUtils.MEDIA_ROOT_ID import org.listenbrainz.android.util.Resource import javax.inject.Inject diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index 88089915..b90bb2ab 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -10,10 +10,10 @@ import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys -import org.listenbrainz.android.repository.preferences.ProtoDataStore +import org.listenbrainz.android.util.datastore.ProtoDataStore import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN -import org.listenbrainz.android.util.DataStoreSerializers.stringListSerializer -import org.listenbrainz.android.util.DataStoreSerializers.themeSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer +import org.listenbrainz.android.util.datastore.DataStoreSerializers.themeSerializer import org.listenbrainz.android.util.LinkedService import org.listenbrainz.sharedtest.utils.EntityTestUtils.testAccessToken import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername From c067ed53b701b211f2e4f3052fb89208f613a1f1 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:57:23 +0530 Subject: [PATCH 3/9] Wording and concurrency fix --- .../ListenServiceManagerImpl.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/repository/listenservicemanager/ListenServiceManagerImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/listenservicemanager/ListenServiceManagerImpl.kt index c4d22d56..5cb5c940 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/listenservicemanager/ListenServiceManagerImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/listenservicemanager/ListenServiceManagerImpl.kt @@ -17,6 +17,7 @@ import org.listenbrainz.android.model.PlayingTrack.Companion.toPlayingTrack import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.util.ListenSubmissionState import org.listenbrainz.android.util.ListenSubmissionState.Companion.extractTitle +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject /** The sole responsibility of this layer is to maintain mutual exclusion between [onMetadataChanged] and @@ -40,7 +41,7 @@ class ListenServiceManagerImpl @Inject constructor( /** Used to avoid repetitive submissions.*/ private var lastNotificationPostTs = System.currentTimeMillis() private lateinit var whitelist: List - private var isScrobblingAllowed: Boolean = true + private var isListeningAllowed: AtomicBoolean = AtomicBoolean(true) init { with(scope) { @@ -55,9 +56,9 @@ class ListenServiceManagerImpl @Inject constructor( } launch(Dispatchers.Default) { appPreferences.isListeningAllowed.getFlow().collect { - isScrobblingAllowed = it + isListeningAllowed.set(it) // Immediately discard current listen if "Send Listens" option has been turned off. - if (!isScrobblingAllowed) { + if (!it) { listenSubmissionState.discardCurrentListen() } } @@ -67,7 +68,7 @@ class ListenServiceManagerImpl @Inject constructor( override fun onMetadataChanged(metadata: MediaMetadata?, player: String) { handler.post { - if (!isScrobblingAllowed) return@post + if (!isListeningAllowed.get()) return@post if (metadata == null) return@post val newTimestamp = System.currentTimeMillis() @@ -103,7 +104,7 @@ class ListenServiceManagerImpl @Inject constructor( * means the track has been changed.*/ override fun onNotificationPosted(sbn: StatusBarNotification?) { handler.post { - if (!isScrobblingAllowed) return@post + if (!isListeningAllowed.get()) return@post // Only CATEGORY_TRANSPORT contain media player metadata. if (sbn?.notification?.category != Notification.CATEGORY_TRANSPORT) return@post @@ -138,7 +139,7 @@ class ListenServiceManagerImpl @Inject constructor( override fun onNotificationRemoved(sbn: StatusBarNotification?) { scope.launch { - if (!isScrobblingAllowed) return@launch + if (!isListeningAllowed.get()) return@launch if (sbn?.notification?.category == Notification.CATEGORY_TRANSPORT && sbn.packageName in appPreferences.listeningWhitelist.get() From 8126f33400fa3ed11f2061a35c5c828eb5ca7ce3 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:00:09 +0530 Subject: [PATCH 4/9] AppPreferences are testable now --- .../repository/preferences/AppPreferences.kt | 18 +++--- .../preferences/AppPreferencesImpl.kt | 26 ++++---- .../android/util/datastore/Preference.kt | 26 ++++++++ .../android/util/datastore/ProtoDataStore.kt | 24 ++++++-- .../sharedtest/mocks/MockAppPreferences.kt | 59 ++++++------------- .../sharedtest/mocks/MockPreferences.kt | 43 ++++++++++++++ 6 files changed, 129 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt index 2bb6a851..80136bf7 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt @@ -4,12 +4,12 @@ import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.util.LinkedService -import org.listenbrainz.android.util.datastore.ProtoDataStore.DataStorePreference -import org.listenbrainz.android.util.datastore.ProtoDataStore.PrimitiveDataStorePreference +import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference +import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference interface AppPreferences { - val themePreference: DataStorePreference + val themePreference: ComplexPreference /** * @@ -23,10 +23,10 @@ interface AppPreferences { var permissionsPreference: String? /** Whitelist for ListenSubmissionService.*/ - val listeningWhitelist: DataStorePreference, String> + val listeningWhitelist: ComplexPreference> /** Music Apps in users device registered by listenService.*/ - val listeningApps: DataStorePreference,String> + val listeningApps: ComplexPreference> var onboardingCompleted: Boolean @@ -42,19 +42,19 @@ interface AppPreferences { suspend fun isUserLoggedIn() : Boolean /****ListenBrainz User Token:** User has to manually fill this token.*/ - val lbAccessToken: PrimitiveDataStorePreference + val lbAccessToken: PrimitivePreference - val username: PrimitiveDataStorePreference + val username: PrimitivePreference val refreshToken: String? var linkedServices: List /** Default is true. */ - val isListeningAllowed: PrimitiveDataStorePreference + val isListeningAllowed: PrimitivePreference /** Default is true. */ - val shouldListenNewPlayers: PrimitiveDataStorePreference + val shouldListenNewPlayers: PrimitivePreference val isNotificationServiceAllowed: Boolean diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt index 4d053766..3bce4015 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt @@ -41,12 +41,14 @@ import org.listenbrainz.android.util.Constants.Strings.REFRESH_TOKEN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import org.listenbrainz.android.util.Constants.Strings.USERNAME +import org.listenbrainz.android.util.LinkedService +import org.listenbrainz.android.util.TypeConverter +import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference import org.listenbrainz.android.util.datastore.DataStoreSerializers.linkedServicesListSerializer import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer import org.listenbrainz.android.util.datastore.DataStoreSerializers.themeSerializer -import org.listenbrainz.android.util.LinkedService +import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference import org.listenbrainz.android.util.datastore.ProtoDataStore -import org.listenbrainz.android.util.TypeConverter import org.listenbrainz.android.util.datastore.migrations.blacklistMigration private val Context.dataStore: DataStore by preferencesDataStore( @@ -108,8 +110,8 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. // Preferences Implementation - override val themePreference: DataStorePreference - get() = object : DataStorePreference( + override val themePreference: ComplexPreference + get() = object : ComplexDataStorePreference( key = THEME, serializer = themeSerializer ) {} @@ -118,8 +120,8 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. get() = preferences.getString(PREFERENCE_PERMS, PermissionStatus.NOT_REQUESTED.name) set(value) = setString(PREFERENCE_PERMS, value) - override val listeningWhitelist: DataStorePreference, String> - get() = object: DataStorePreference, String>( + override val listeningWhitelist: ComplexPreference> + get() = object: ComplexDataStorePreference>( key = LISTENING_WHITELIST, serializer = stringListSerializer ) {} @@ -130,20 +132,20 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. return listeners != null && listeners.contains(context.packageName) } - override val isListeningAllowed: PrimitiveDataStorePreference + override val isListeningAllowed: PrimitivePreference get() = object: PrimitiveDataStorePreference( key = IS_LISTENING_ALLOWED, defaultValue = true ) {} - override val shouldListenNewPlayers: PrimitiveDataStorePreference + override val shouldListenNewPlayers: PrimitivePreference get() = object: PrimitiveDataStorePreference( key = SHOULD_LISTEN_NEW_PLAYERS, defaultValue = true ) {} - override val listeningApps: DataStorePreference, String> - get() = object: DataStorePreference, String>( + override val listeningApps: ComplexPreference> + get() = object: ComplexDataStorePreference>( key = LISTENING_APPS, serializer = stringListSerializer ) {} @@ -192,13 +194,13 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. override suspend fun isUserLoggedIn() : Boolean = lbAccessToken.get().isNotEmpty() - override val lbAccessToken: PrimitiveDataStorePreference + override val lbAccessToken: PrimitivePreference get() = object : PrimitiveDataStorePreference( key = PreferenceKeys.LB_ACCESS_TOKEN, defaultValue = "" ) {} - override val username: PrimitiveDataStorePreference + override val username: PrimitivePreference get() = object : PrimitiveDataStorePreference( key = PreferenceKeys.USERNAME, defaultValue = "" diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt new file mode 100644 index 00000000..b330f7ed --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt @@ -0,0 +1,26 @@ +package org.listenbrainz.android.util.datastore + +import kotlinx.coroutines.flow.Flow +import org.listenbrainz.android.util.Resource + +/** A Type-safe preference.*/ +interface Preference { + suspend fun get(): T + + fun getFlow(): Flow + + /** @return [Resource] If the value was updated or not.*/ + suspend fun set(value: T): Resource + + /** Update the value of the preference in an atomic read-modify-write manner.*/ + suspend fun getAndUpdate(update: (T) -> T) + + companion object { + /** [Preference]s which are primitive in nature can implement this interface.*/ + interface PrimitivePreference: Preference + + /** [Preference]s which have to be converted to [String] when saved can implement this interface.*/ + interface ComplexPreference: Preference + } +} + diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt index cec0ce7d..033969ea 100644 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt @@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference +import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference import java.io.IOException abstract class ProtoDataStore(private val dataStore: DataStore) { @@ -19,11 +21,19 @@ abstract class ProtoDataStore(private val dataStore: DataStore) { override fun default(): T = defaultValue } + /** [DataStorePreference]s which are primitive in nature can use this class.*/ abstract inner class PrimitiveDataStorePreference( key: Preferences.Key, defaultValue: T - ): DataStorePreference(key, defaultSerializer(defaultValue)) + ): PrimitivePreference, DataStorePreference(key, defaultSerializer(defaultValue)) + + + /** [DataStorePreference]s which are complex in nature can use this class.*/ + abstract inner class ComplexDataStorePreference( + key: Preferences.Key, + serializer: DataStoreSerializer + ): ComplexPreference, DataStorePreference(key, serializer) /** A [DataStore] preference can be declared type-safe by making an object of this class. @@ -32,10 +42,10 @@ abstract class ProtoDataStore(private val dataStore: DataStore) { abstract inner class DataStorePreference( private val key: Preferences.Key, private val serializer: DataStoreSerializer - ) { - open suspend fun get(): T = getFlow().firstOrNull() ?: serializer.default() + ): Preference { + override suspend fun get(): T = getFlow().firstOrNull() ?: serializer.default() - open fun getFlow(): Flow = + override fun getFlow(): Flow = dataStore.data.map { prefs -> prefs[key]?.let { serializer.from(it) } ?: serializer.default() }.catch { @@ -46,7 +56,7 @@ abstract class ProtoDataStore(private val dataStore: DataStore) { } /** @return [Resource] If the value was updated or not.*/ - open suspend fun set(value: T): Resource = + override suspend fun set(value: T): Resource = try { dataStore.edit { it[key] = serializer.to(value) } Resource.success(value) @@ -55,12 +65,14 @@ abstract class ProtoDataStore(private val dataStore: DataStore) { } /** Update the value of the preference in an atomic read-modify-write manner.*/ - open suspend fun getAndUpdate(update: (T) -> T) = + override suspend fun getAndUpdate(update: (T) -> T) { dataStore.updateData { prefs -> val mutablePrefs = prefs.toMutablePreferences() val currentValue = prefs[key]?.let { serializer.from(it) } ?: serializer.default() mutablePrefs[key] = serializer.to(update(currentValue)) return@updateData mutablePrefs } + } + } } \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index b90bb2ab..321e20fd 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -9,12 +9,13 @@ import org.listenbrainz.android.model.PermissionStatus import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.repository.preferences.AppPreferences -import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys -import org.listenbrainz.android.util.datastore.ProtoDataStore import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN -import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer -import org.listenbrainz.android.util.datastore.DataStoreSerializers.themeSerializer import org.listenbrainz.android.util.LinkedService +import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference +import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference +import org.listenbrainz.android.util.datastore.ProtoDataStore +import org.listenbrainz.sharedtest.mocks.MockPreferences.mockComplexPreference +import org.listenbrainz.sharedtest.mocks.MockPreferences.mockPrimitivePreference import org.listenbrainz.sharedtest.utils.EntityTestUtils.testAccessToken import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername @@ -44,48 +45,26 @@ class MockAppPreferences( override var linkedServices: List = listOf() ): ProtoDataStore(dataStore = mockDataStore), AppPreferences { - override val themePreference: DataStorePreference = - object: DataStorePreference( - key = PreferenceKeys.THEME, - serializer = themeSerializer - ) {} - - override val listeningWhitelist: DataStorePreference, String> = - object : DataStorePreference, String>( - key = PreferenceKeys.LISTENING_WHITELIST, - serializer = stringListSerializer.apply { } - ) {} + override val themePreference: ComplexPreference = + mockComplexPreference(UiMode.FOLLOW_SYSTEM) - override val listeningApps: DataStorePreference, String> = - object : DataStorePreference, String>( - key = PreferenceKeys.LISTENING_APPS, - serializer = stringListSerializer - ) {} + override val listeningWhitelist: ComplexPreference> = + mockComplexPreference(emptyList()) - override val lbAccessToken: PrimitiveDataStorePreference = - object : PrimitiveDataStorePreference( - key = PreferenceKeys.LB_ACCESS_TOKEN, - defaultValue = testAccessToken - ) {} + override val listeningApps: ComplexPreference> = + mockComplexPreference(emptyList()) - override val username: PrimitiveDataStorePreference = - object : PrimitiveDataStorePreference( - key = PreferenceKeys.USERNAME, - defaultValue = testUsername - ) {} + override val lbAccessToken: PrimitivePreference = + mockPrimitivePreference(testAccessToken) - override val isListeningAllowed: PrimitiveDataStorePreference = - object : PrimitiveDataStorePreference( - key = PreferenceKeys.IS_LISTENING_ALLOWED, - defaultValue = true - ) {} + override val username: PrimitivePreference = + mockPrimitivePreference(testUsername) - override val shouldListenNewPlayers: PrimitiveDataStorePreference = - object : PrimitiveDataStorePreference ( - key = PreferenceKeys.SHOULD_LISTEN_NEW_PLAYERS, - defaultValue = true - ) {} + override val isListeningAllowed: PrimitivePreference = + mockPrimitivePreference(true) + override val shouldListenNewPlayers: PrimitivePreference = + mockPrimitivePreference(true) override suspend fun logoutUser() { TODO("Not yet implemented") diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt new file mode 100644 index 00000000..27918994 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt @@ -0,0 +1,43 @@ +package org.listenbrainz.sharedtest.mocks + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.datastore.Preference +import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference +import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference + +object MockPreferences { + fun mockPrimitivePreference(mockValue: T): PrimitivePreference = + object : PrimitivePreference { + override suspend fun get(): T = mockValue + + override fun getFlow(): Flow = flow { emit(mockValue) } + + override suspend fun getAndUpdate(update: (T) -> T) {} + + override suspend fun set(value: T): Resource = Resource.success(mockValue) + } + + fun mockComplexPreference(mockValue: T) = + object : ComplexPreference { + override suspend fun get(): T = mockValue + + override fun getFlow(): Flow = flow { emit(mockValue) } + + override suspend fun getAndUpdate(update: (T) -> T) {} + + override suspend fun set(value: T): Resource = Resource.success(mockValue) + } + + fun mockPreference(mockValue: T): Preference = + object: Preference { + override suspend fun get(): T = mockValue + + override fun getFlow(): Flow = flow { emit(mockValue) } + + override suspend fun getAndUpdate(update: (T) -> T) {} + + override suspend fun set(value: T): Resource = Resource.success(mockValue) + } +} \ No newline at end of file From 20755996ed1f04a1505a91815548121ed9911c15 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:20:26 +0530 Subject: [PATCH 5/9] Typo fix --- .../listenbrainz/android/util/datastore/DataStoreSerializer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt index 44fa8314..81349a55 100644 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt @@ -6,7 +6,7 @@ interface DataStoreSerializer { /** Convert from Primitive [R] to Object [T].*/ fun from(value: R): T - /** Convert to Primitive [T] to Object [R].*/ + /** Convert to Primitive [T] from Object [R].*/ fun to(value: T): R /** Default value for errors and null values.*/ From ae4cac6dd12ac31bd07a6d8ace41fa0c78a3037a Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:23:22 +0530 Subject: [PATCH 6/9] Update Readme.md --- Readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 7f5c1519..8fb5b8c0 100644 --- a/Readme.md +++ b/Readme.md @@ -54,7 +54,9 @@ Got **something interesting** you'd like to **ask or share**? Start a discussion ## Roadmap -Proposed future plans for the app are discussed [here](https://docs.google.com/document/d/1hY5oloIiANeXg1R9oSBIr2ZUHSlm7LU8qy6tW1VJMQg/edit?usp=sharing). +- Proposed future plans for the app are discussed [here](https://docs.google.com/document/d/1hY5oloIiANeXg1R9oSBIr2ZUHSlm7LU8qy6tW1VJMQg/edit?usp=sharing). +- Official Release Plan is [here](https://docs.google.com/document/d/1LSDXWciL4LMS_LJJ0Y47BZtU6jEI-8hiliWbzEOYKsA/edit?usp=sharing) +- Future features are discussed [here](https://docs.google.com/document/d/1fmsgFVvxAN39nUyaMsEdyePBaDyDwxelJXeLhaDylhs/edit?usp=sharing) ## Issues From 4ed7d39891d664a0627db8e4885167f369b0621a Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Fri, 8 Mar 2024 22:30:00 +0530 Subject: [PATCH 7/9] Used TypeSafeDataStore to simplify preferences. --- app/build.gradle | 4 + .../repository/preferences/AppPreferences.kt | 4 +- .../preferences/AppPreferencesImpl.kt | 51 ++++-------- .../android/ui/screens/main/MainActivity.kt | 9 ++- .../listenbrainz/android/ui/theme/Theme.kt | 2 +- .../util/datastore/DataStoreSerializer.kt | 14 ---- .../util/datastore/DataStoreSerializers.kt | 26 +------ .../android/util/datastore/Preference.kt | 26 ------- .../android/util/datastore/ProtoDataStore.kt | 78 ------------------- .../migrations/BlacklistMigration.kt | 14 ++-- .../android/viewmodel/AboutViewModel.kt | 11 +-- .../android/viewmodel/DashBoardViewModel.kt | 2 +- build.gradle | 1 + .../sharedtest/mocks/MockAppPreferences.kt | 3 - .../sharedtest/mocks/MockPreferences.kt | 3 - 15 files changed, 41 insertions(+), 207 deletions(-) delete mode 100644 app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt rename app/src/main/java/org/listenbrainz/android/util/{datastore => }/migrations/BlacklistMigration.kt (64%) diff --git a/app/build.gradle b/app/build.gradle index 23556267..032d1906 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,10 @@ dependencies { implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation "androidx.paging:paging-compose:3.2.1" + // Typesafe-datastore + implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore:$typesafe_datastore_version" + implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-gson:$typesafe_datastore_version" + implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:$typesafe_datastore_version" //Image downloading and Caching library implementation 'com.github.bumptech.glide:glide:4.16.0' diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt index 80136bf7..98b535e9 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt @@ -1,11 +1,11 @@ package org.listenbrainz.android.repository.preferences +import com.jasjeet.typesafe_datastore.preferences.ComplexPreference +import com.jasjeet.typesafe_datastore.preferences.PrimitivePreference import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.util.LinkedService -import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference -import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference interface AppPreferences { diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt index 3bce4015..a1011422 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt @@ -10,6 +10,9 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.preference.PreferenceManager +import com.jasjeet.typesafe_datastore.preferences.ComplexPreference +import com.jasjeet.typesafe_datastore.preferences.PrimitivePreference +import com.jasjeet.typesafe_datastore_gson.AutoTypedDataStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -43,13 +46,8 @@ import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import org.listenbrainz.android.util.Constants.Strings.USERNAME import org.listenbrainz.android.util.LinkedService import org.listenbrainz.android.util.TypeConverter -import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference -import org.listenbrainz.android.util.datastore.DataStoreSerializers.linkedServicesListSerializer -import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer import org.listenbrainz.android.util.datastore.DataStoreSerializers.themeSerializer -import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference -import org.listenbrainz.android.util.datastore.ProtoDataStore -import org.listenbrainz.android.util.datastore.migrations.blacklistMigration +import org.listenbrainz.android.util.migrations.blacklistMigration private val Context.dataStore: DataStore by preferencesDataStore( name = "settings", @@ -70,7 +68,7 @@ private val Context.dataStore: DataStore by preferencesDataStore( } ) -class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context.dataStore), AppPreferences { +class AppPreferencesImpl(private val context: Context): AutoTypedDataStore(context.dataStore), AppPreferences { companion object { object PreferenceKeys { val LB_ACCESS_TOKEN = stringPreferencesKey(Constants.Strings.LB_ACCESS_TOKEN) @@ -111,20 +109,14 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. // Preferences Implementation override val themePreference: ComplexPreference - get() = object : ComplexDataStorePreference( - key = THEME, - serializer = themeSerializer - ) {} + get() = createComplexPreference(THEME, themeSerializer) override var permissionsPreference: String? get() = preferences.getString(PREFERENCE_PERMS, PermissionStatus.NOT_REQUESTED.name) set(value) = setString(PREFERENCE_PERMS, value) override val listeningWhitelist: ComplexPreference> - get() = object: ComplexDataStorePreference>( - key = LISTENING_WHITELIST, - serializer = stringListSerializer - ) {} + get() = listPreference(LISTENING_WHITELIST) override val isNotificationServiceAllowed: Boolean get() { @@ -133,22 +125,13 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. } override val isListeningAllowed: PrimitivePreference - get() = object: PrimitiveDataStorePreference( - key = IS_LISTENING_ALLOWED, - defaultValue = true - ) {} + get() = booleanPreference(IS_LISTENING_ALLOWED, true) override val shouldListenNewPlayers: PrimitivePreference - get() = object: PrimitiveDataStorePreference( - key = SHOULD_LISTEN_NEW_PLAYERS, - defaultValue = true - ) {} + get() = booleanPreference(SHOULD_LISTEN_NEW_PLAYERS, true) override val listeningApps: ComplexPreference> - get() = object: ComplexDataStorePreference>( - key = LISTENING_APPS, - serializer = stringListSerializer - ) {} + get() = listPreference(LISTENING_APPS) override val version: String get() = try { @@ -195,24 +178,18 @@ class AppPreferencesImpl(private val context: Context) : ProtoDataStore(context. lbAccessToken.get().isNotEmpty() override val lbAccessToken: PrimitivePreference - get() = object : PrimitiveDataStorePreference( - key = PreferenceKeys.LB_ACCESS_TOKEN, - defaultValue = "" - ) {} + get() = stringPreference(PreferenceKeys.LB_ACCESS_TOKEN) override val username: PrimitivePreference - get() = object : PrimitiveDataStorePreference( - key = PreferenceKeys.USERNAME, - defaultValue = "" - ) {} + get() = stringPreference(PreferenceKeys.USERNAME) override var linkedServices: List get() { val jsonString = preferences.getString(LINKED_SERVICES, "") ?: "" - return linkedServicesListSerializer.from(jsonString) + return listSerializer().from(jsonString) } set(value) { - val jsonString = linkedServicesListSerializer.to(value) + val jsonString = listSerializer().to(value) setString(LINKED_SERVICES, jsonString) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 48890922..6081ac50 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -15,7 +15,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.LifecycleStartEffect @@ -51,7 +56,7 @@ class MainActivity : ComponentActivity() { dashBoardViewModel = ViewModelProvider(this)[DashBoardViewModel::class.java] setContent { - ListenBrainzTheme { + ListenBrainzTheme(appPreferences = remember { dashBoardViewModel.appPreferences }) { // TODO: Since this view-model will remain throughout the lifecycle of the app, // we can have tasks which require such lifecycle access or longevity. We can get this view-model's // instance anywhere when we initialize it as a hilt view-model. diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt index 6b47dd33..5272f27d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt @@ -201,7 +201,7 @@ fun ListenBrainzTheme( systemTheme: Boolean = isSystemInDarkTheme(), systemUiController: SystemUiController = rememberSystemUiController(), context: Context = LocalContext.current, - appPreferences: AppPreferences = AppPreferencesImpl(context), + appPreferences: AppPreferences = remember { AppPreferencesImpl(context) }, // Dynamic color is available on Android 12+ //dynamicColor: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, // dynamicColor: Boolean = false,//Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt deleted file mode 100644 index 81349a55..00000000 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.listenbrainz.android.util.datastore - -/** @property T Object or higher type. - * @property R Primitive or serializable type.*/ -interface DataStoreSerializer { - /** Convert from Primitive [R] to Object [T].*/ - fun from(value: R): T - - /** Convert to Primitive [T] from Object [R].*/ - fun to(value: T): R - - /** Default value for errors and null values.*/ - fun default(): T -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt index c8c3fce5..93674956 100644 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt +++ b/app/src/main/java/org/listenbrainz/android/util/datastore/DataStoreSerializers.kt @@ -1,27 +1,10 @@ package org.listenbrainz.android.util.datastore -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.jasjeet.typesafe_datastore.DataStoreSerializer import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.model.UiMode.Companion.asUiMode -import org.listenbrainz.android.util.LinkedService object DataStoreSerializers { - private val gson = Gson() - /** Serializes to String primitive.*/ - private fun gsonSerializer(defaultValue: T): DataStoreSerializer = - object: DataStoreSerializer { - override fun from(value: String): T = - gson.fromJson( - value, - object: TypeToken() {}.type - ) ?: defaultValue - - override fun to(value: T): String = gson.toJson(value) - - override fun default(): T = defaultValue - } - val themeSerializer: DataStoreSerializer get() = object: DataStoreSerializer { override fun from(value: String): UiMode = value.asUiMode() @@ -30,11 +13,4 @@ object DataStoreSerializers { override fun default(): UiMode = UiMode.FOLLOW_SYSTEM } - - - val stringListSerializer: DataStoreSerializer, String> - get() = gsonSerializer(emptyList()) - - val linkedServicesListSerializer: DataStoreSerializer, String> - get() = gsonSerializer(emptyList()) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt deleted file mode 100644 index b330f7ed..00000000 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/Preference.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.listenbrainz.android.util.datastore - -import kotlinx.coroutines.flow.Flow -import org.listenbrainz.android.util.Resource - -/** A Type-safe preference.*/ -interface Preference { - suspend fun get(): T - - fun getFlow(): Flow - - /** @return [Resource] If the value was updated or not.*/ - suspend fun set(value: T): Resource - - /** Update the value of the preference in an atomic read-modify-write manner.*/ - suspend fun getAndUpdate(update: (T) -> T) - - companion object { - /** [Preference]s which are primitive in nature can implement this interface.*/ - interface PrimitivePreference: Preference - - /** [Preference]s which have to be converted to [String] when saved can implement this interface.*/ - interface ComplexPreference: Preference - } -} - diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt b/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt deleted file mode 100644 index 033969ea..00000000 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/ProtoDataStore.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.listenbrainz.android.util.datastore - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import org.listenbrainz.android.model.ResponseError -import org.listenbrainz.android.util.Resource -import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference -import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference -import java.io.IOException - -abstract class ProtoDataStore(private val dataStore: DataStore) { - private fun defaultSerializer(defaultValue: T): DataStoreSerializer = - object: DataStoreSerializer { - override fun from(value: T): T = value - override fun to(value: T): T = value - override fun default(): T = defaultValue - } - - - /** [DataStorePreference]s which are primitive in nature can use this class.*/ - abstract inner class PrimitiveDataStorePreference( - key: Preferences.Key, - defaultValue: T - ): PrimitivePreference, DataStorePreference(key, defaultSerializer(defaultValue)) - - - /** [DataStorePreference]s which are complex in nature can use this class.*/ - abstract inner class ComplexDataStorePreference( - key: Preferences.Key, - serializer: DataStoreSerializer - ): ComplexPreference, DataStorePreference(key, serializer) - - - /** A [DataStore] preference can be declared type-safe by making an object of this class. - * - * Every function can be overridden.*/ - abstract inner class DataStorePreference( - private val key: Preferences.Key, - private val serializer: DataStoreSerializer - ): Preference { - override suspend fun get(): T = getFlow().firstOrNull() ?: serializer.default() - - override fun getFlow(): Flow = - dataStore.data.map { prefs -> - prefs[key]?.let { serializer.from(it) } ?: serializer.default() - }.catch { - if (it is IOException) - emit(serializer.default()) - else - throw it - } - - /** @return [Resource] If the value was updated or not.*/ - override suspend fun set(value: T): Resource = - try { - dataStore.edit { it[key] = serializer.to(value) } - Resource.success(value) - } catch (e: IOException) { - Resource.failure(error = ResponseError.FILE_NOT_FOUND.apply { actualResponse = e.localizedMessage }) - } - - /** Update the value of the preference in an atomic read-modify-write manner.*/ - override suspend fun getAndUpdate(update: (T) -> T) { - dataStore.updateData { prefs -> - val mutablePrefs = prefs.toMutablePreferences() - val currentValue = prefs[key]?.let { serializer.from(it) } ?: serializer.default() - mutablePrefs[key] = serializer.to(update(currentValue)) - return@updateData mutablePrefs - } - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt similarity index 64% rename from app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt rename to app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt index 277cb326..cb65499c 100644 --- a/app/src/main/java/org/listenbrainz/android/util/datastore/migrations/BlacklistMigration.kt +++ b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt @@ -1,11 +1,10 @@ -package org.listenbrainz.android.util.datastore.migrations +package org.listenbrainz.android.util.migrations import androidx.datastore.core.DataMigration import androidx.datastore.preferences.core.Preferences import com.google.gson.Gson -import org.listenbrainz.android.repository.preferences.AppPreferencesImpl +import com.jasjeet.typesafe_datastore_gson.AutoTypedDataStore.Companion.listSerializer import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys -import org.listenbrainz.android.util.datastore.DataStoreSerializers.stringListSerializer val blacklistMigration: DataMigration = object: DataMigration { @@ -13,14 +12,15 @@ val blacklistMigration: DataMigration = override suspend fun shouldMigrate(currentData: Preferences): Boolean { // If blacklist is deleted, then we are sure that migration took place. - return currentData.contains(AppPreferencesImpl.Companion.PreferenceKeys.LISTENING_BLACKLIST) + return currentData.contains(PreferenceKeys.LISTENING_BLACKLIST) } override suspend fun migrate(currentData: Preferences): Preferences { - val blacklist = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_BLACKLIST] ?: "") - val appList = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_APPS] ?: "") + val serializer = listSerializer() + val blacklist = serializer.from(currentData[PreferenceKeys.LISTENING_BLACKLIST] ?: "") + val appList = serializer.from(currentData[PreferenceKeys.LISTENING_APPS] ?: "") - val whitelist = stringListSerializer.from(currentData[PreferenceKeys.LISTENING_WHITELIST] ?: "").toMutableSet() + val whitelist = serializer.from(currentData[PreferenceKeys.LISTENING_WHITELIST] ?: "").toMutableSet() appList.forEach { pkg -> if (!blacklist.contains(pkg)) { whitelist.add(pkg) diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/AboutViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/AboutViewModel.kt index 92861eb7..556d2157 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/AboutViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/AboutViewModel.kt @@ -1,19 +1,14 @@ package org.listenbrainz.android.viewmodel -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences import javax.inject.Inject @HiltViewModel class AboutViewModel @Inject constructor( - val appPreferences: AppPreferences, - application: Application, -) : AndroidViewModel(application) { + val appPreferences: AppPreferences +) : ViewModel() { fun version(): String { return appPreferences.version diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt index ad259b05..8fc369e3 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt @@ -25,7 +25,7 @@ import javax.inject.Inject @HiltViewModel class DashBoardViewModel @Inject constructor( - private val appPreferences: AppPreferences, + val appPreferences: AppPreferences, private val application: Application, private val remotePlaybackHandler: RemotePlaybackHandler, @IoDispatcher private val ioDispatcher: CoroutineDispatcher diff --git a/build.gradle b/build.gradle index 84bc209d..026a4412 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { work_version = '2.9.0' exoplayer_version = '2.19.1' paging_version = "3.2.1" + typesafe_datastore_version = "1.0.2" } repositories { google() diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index 321e20fd..b0dcc3bc 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -11,9 +11,6 @@ import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.util.LinkedService -import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference -import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference -import org.listenbrainz.android.util.datastore.ProtoDataStore import org.listenbrainz.sharedtest.mocks.MockPreferences.mockComplexPreference import org.listenbrainz.sharedtest.mocks.MockPreferences.mockPrimitivePreference import org.listenbrainz.sharedtest.utils.EntityTestUtils.testAccessToken diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt index 27918994..c5a6afaa 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt @@ -3,9 +3,6 @@ package org.listenbrainz.sharedtest.mocks import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import org.listenbrainz.android.util.Resource -import org.listenbrainz.android.util.datastore.Preference -import org.listenbrainz.android.util.datastore.Preference.Companion.ComplexPreference -import org.listenbrainz.android.util.datastore.Preference.Companion.PrimitivePreference object MockPreferences { fun mockPrimitivePreference(mockValue: T): PrimitivePreference = From 0255880ee91829cbcf9eadd0991bc4ae4e88636f Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Wed, 13 Mar 2024 12:51:52 +0530 Subject: [PATCH 8/9] Updated migration --- .../util/migrations/BlacklistMigration.kt | 41 ++++++++----------- build.gradle | 2 +- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt index cb65499c..a9215c31 100644 --- a/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt +++ b/app/src/main/java/org/listenbrainz/android/util/migrations/BlacklistMigration.kt @@ -3,34 +3,29 @@ package org.listenbrainz.android.util.migrations import androidx.datastore.core.DataMigration import androidx.datastore.preferences.core.Preferences import com.google.gson.Gson +import com.jasjeet.typesafe_datastore.migrations.CustomMigration import com.jasjeet.typesafe_datastore_gson.AutoTypedDataStore.Companion.listSerializer import org.listenbrainz.android.repository.preferences.AppPreferencesImpl.Companion.PreferenceKeys val blacklistMigration: DataMigration = - object: DataMigration { - override suspend fun cleanUp() = Unit + CustomMigration( + currentKey = PreferenceKeys.LISTENING_BLACKLIST, + newKey = PreferenceKeys.LISTENING_WHITELIST, + ) { currentData -> + val serializer = listSerializer() + val blacklist = serializer.from(currentData[PreferenceKeys.LISTENING_BLACKLIST] ?: "") + val appList = serializer.from(currentData[PreferenceKeys.LISTENING_APPS] ?: "") - override suspend fun shouldMigrate(currentData: Preferences): Boolean { - // If blacklist is deleted, then we are sure that migration took place. - return currentData.contains(PreferenceKeys.LISTENING_BLACKLIST) - } - - override suspend fun migrate(currentData: Preferences): Preferences { - val serializer = listSerializer() - val blacklist = serializer.from(currentData[PreferenceKeys.LISTENING_BLACKLIST] ?: "") - val appList = serializer.from(currentData[PreferenceKeys.LISTENING_APPS] ?: "") - - val whitelist = serializer.from(currentData[PreferenceKeys.LISTENING_WHITELIST] ?: "").toMutableSet() - appList.forEach { pkg -> - if (!blacklist.contains(pkg)) { - whitelist.add(pkg) - } + val whitelist = serializer.from(currentData[PreferenceKeys.LISTENING_WHITELIST] ?: "").toMutableSet() + appList.forEach { pkg -> + if (!blacklist.contains(pkg)) { + whitelist.add(pkg) } - - val mutablePreferences = currentData.toMutablePreferences() - mutablePreferences[PreferenceKeys.LISTENING_WHITELIST] = Gson().toJson(whitelist.toList()) - mutablePreferences.remove(PreferenceKeys.LISTENING_BLACKLIST) // Clear old stale data and key. - - return mutablePreferences.toPreferences() } + + val mutablePreferences = currentData.toMutablePreferences() + mutablePreferences[PreferenceKeys.LISTENING_WHITELIST] = Gson().toJson(whitelist.toList()) + mutablePreferences.remove(PreferenceKeys.LISTENING_BLACKLIST) // Clear old stale data and key. + + return@CustomMigration mutablePreferences.toPreferences() } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 026a4412..a5770334 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { work_version = '2.9.0' exoplayer_version = '2.19.1' paging_version = "3.2.1" - typesafe_datastore_version = "1.0.2" + typesafe_datastore_version = "1.0.3" } repositories { google() From f35ae1312e51ea24ddefb223e7b3c8fee49859de Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:56:23 +0530 Subject: [PATCH 9/9] Save for laptop repair --- app/build.gradle | 3 +- .../org/listenbrainz/android/Extensions.kt | 27 ++++ .../android/YearInMusicActivityTest.kt | 23 ++- .../listenbrainz/android/di/TestAppModule.kt | 6 +- .../listenbrainz/android/feed/FeedFlowTest.kt | 54 +++++++ .../android/FeedPagingSourceTest.kt | 28 ++++ .../android/FeedRepositoryTest.kt | 125 ++++++++++++++++ .../listenbrainz/android/FeedViewModelTest.kt | 139 ++++++++++++++++++ .../android/yim/YimViewModelTest.kt | 21 ++- sharedTest/build.gradle | 1 + .../sharedtest/mocks/MockAppPreferences.kt | 31 +--- .../sharedtest/mocks/MockPreferences.kt | 40 ----- 12 files changed, 414 insertions(+), 84 deletions(-) create mode 100644 app/src/androidTest/java/org/listenbrainz/android/Extensions.kt create mode 100644 app/src/androidTest/java/org/listenbrainz/android/feed/FeedFlowTest.kt create mode 100644 app/src/test/java/org/listenbrainz/android/FeedPagingSourceTest.kt create mode 100644 app/src/test/java/org/listenbrainz/android/FeedRepositoryTest.kt create mode 100644 app/src/test/java/org/listenbrainz/android/FeedViewModelTest.kt delete mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt diff --git a/app/build.gradle b/app/build.gradle index 032d1906..d0cdc158 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,7 +152,7 @@ dependencies { // Typesafe-datastore implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore:$typesafe_datastore_version" implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-gson:$typesafe_datastore_version" - implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:$typesafe_datastore_version" + testImplementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:$typesafe_datastore_version" //Image downloading and Caching library implementation 'com.github.bumptech.glide:glide:4.16.0' @@ -221,6 +221,7 @@ dependencies { // Mockito framework testImplementation 'org.mockito:mockito-core:5.10.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1' + androidTestImplementation "org.mockito:mockito-android:5.2.1" debugImplementation "androidx.test:monitor:1.6.1" // Solves "class PlatformTestStorageRegistery not found" error for ui tests. debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.1' diff --git a/app/src/androidTest/java/org/listenbrainz/android/Extensions.kt b/app/src/androidTest/java/org/listenbrainz/android/Extensions.kt new file mode 100644 index 00000000..d8478f7b --- /dev/null +++ b/app/src/androidTest/java/org/listenbrainz/android/Extensions.kt @@ -0,0 +1,27 @@ +package org.listenbrainz.android + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.listenbrainz.android.model.UiMode +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.isUiModeIsDark + +fun AndroidComposeTestRule, ComponentActivity>.setExplicitContent( + uiMode: UiMode = UiMode.DARK, + content: @Composable () -> Unit +) { + isUiModeIsDark = when(uiMode) { + UiMode.LIGHT -> mutableStateOf(false) + UiMode.DARK -> mutableStateOf(true) + UiMode.FOLLOW_SYSTEM -> mutableStateOf(null) + } + + this.setContent { + ListenBrainzTheme { + content() + } + } +} diff --git a/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt b/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt index 1c1baa7a..b6bcc57a 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt @@ -2,23 +2,28 @@ package org.listenbrainz.android import androidx.activity.ComponentActivity import androidx.annotation.StringRes -import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTouchInput import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.ui.screens.yim.navigation.YimNavigation import org.listenbrainz.android.util.connectivityobserver.ConnectivityObserver import org.listenbrainz.android.viewmodel.YimViewModel -import org.listenbrainz.sharedtest.mocks.MockAppPreferences import org.listenbrainz.sharedtest.mocks.MockNetworkConnectivityViewModel import org.listenbrainz.sharedtest.mocks.MockYimRepository +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner -@RunWith(AndroidJUnit4::class) +@RunWith(MockitoJUnitRunner::class) //@LargeTest @HiltAndroidTest class YearInMusicActivityTest { @@ -29,14 +34,16 @@ class YearInMusicActivityTest { @get:Rule(order = 1) val rule = createAndroidComposeRule() + @Mock + private lateinit var mockAppPreferences: AppPreferences + private lateinit var activity : ComponentActivity @Before fun setup(){ activity = rule.activity val yimViewModel = YimViewModel( - MockYimRepository(), - MockAppPreferences() + MockYimRepository(), mockAppPreferences ) val networkViewModel = MockNetworkConnectivityViewModel(ConnectivityObserver.NetworkStatus.AVAILABLE) @@ -86,4 +93,8 @@ class YearInMusicActivityTest { performClick() } } + + private fun initMocks() { + + } } diff --git a/app/src/androidTest/java/org/listenbrainz/android/di/TestAppModule.kt b/app/src/androidTest/java/org/listenbrainz/android/di/TestAppModule.kt index 1a784f70..8b089dda 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/di/TestAppModule.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/di/TestAppModule.kt @@ -1,7 +1,6 @@ package org.listenbrainz.android.di import android.content.Context - import android.util.Log import androidx.work.Configuration import androidx.work.WorkManager @@ -14,7 +13,6 @@ import dagger.hilt.components.SingletonComponent import dagger.hilt.testing.TestInstallIn import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.service.BrainzPlayerServiceConnection -import org.listenbrainz.sharedtest.mocks.MockAppPreferences import javax.inject.Singleton @Module @@ -47,8 +45,8 @@ class TestAppModule { @Provides fun providesContext(@ApplicationContext context: Context): Context = context - @Singleton + /*@Singleton @Provides - fun providesAppPreferences() : AppPreferences = MockAppPreferences() + fun providesAppPreferences() : AppPreferences = MockAppPreferences()*/ } diff --git a/app/src/androidTest/java/org/listenbrainz/android/feed/FeedFlowTest.kt b/app/src/androidTest/java/org/listenbrainz/android/feed/FeedFlowTest.kt new file mode 100644 index 00000000..893f17c8 --- /dev/null +++ b/app/src/androidTest/java/org/listenbrainz/android/feed/FeedFlowTest.kt @@ -0,0 +1,54 @@ +package org.listenbrainz.android.feed + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.model.feed.FeedEvent +import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.ReviewEntityType +import org.listenbrainz.android.setExplicitContent +import org.listenbrainz.android.ui.screens.feed.FeedScreen +import org.listenbrainz.android.ui.screens.feed.FeedUiState + +@RunWith(AndroidJUnit4::class) +@LargeTest +@HiltAndroidTest +class FeedFlowTest { + + @get:Rule(order = 0) + val rule = createAndroidComposeRule() + + @Before + fun setup(){ + + rule.setExplicitContent { + + FeedScreen( + uiState = FeedUiState(), + scrollToTopState = false, + onScrollToTop = {}, + onDeleteOrHide = { feedEvent: FeedEvent, feedEventType: FeedEventType, s: String -> }, + onErrorShown = {}, + recommendTrack = {}, + personallyRecommendTrack = { feedEvent: FeedEvent, strings: List, s: String -> }, + review = { feedEvent: FeedEvent, reviewEntityType: ReviewEntityType, s: String, i: Int?, s1: String -> }, + pin = { feedEvent: FeedEvent, s: String? -> }, + searchFollower = {}, + isCritiqueBrainzLinked = { true }, + onPlay = {} + ) + } + } + + @Test + fun feedScreenTest() = runTest { + + } +} \ No newline at end of file diff --git a/app/src/test/java/org/listenbrainz/android/FeedPagingSourceTest.kt b/app/src/test/java/org/listenbrainz/android/FeedPagingSourceTest.kt new file mode 100644 index 00000000..a0be5ca3 --- /dev/null +++ b/app/src/test/java/org/listenbrainz/android/FeedPagingSourceTest.kt @@ -0,0 +1,28 @@ +package org.listenbrainz.android + +import androidx.paging.PagingSource +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.model.feed.FeedData +import org.listenbrainz.android.ui.screens.feed.FollowListensPagingSource +import org.listenbrainz.sharedtest.utils.ResourceString +import org.listenbrainz.sharedtest.utils.ResourceString.toClass +import org.mockito.junit.MockitoJUnitRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class FeedPagingSourceTest: BaseUnitTest() { + @Test + fun test() { + val expectedFeedData = + FollowListensPagingSource.processFeedEvents( + ResourceString.my_feed_page_1.toClass() + ) + val expectedResult = PagingSource.LoadResult.Page( + data = expectedFeedData, + prevKey = null, + nextKey = expectedFeedData.last().event.created + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/listenbrainz/android/FeedRepositoryTest.kt b/app/src/test/java/org/listenbrainz/android/FeedRepositoryTest.kt new file mode 100644 index 00000000..e32aa849 --- /dev/null +++ b/app/src/test/java/org/listenbrainz/android/FeedRepositoryTest.kt @@ -0,0 +1,125 @@ +package org.listenbrainz.android + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.model.SocialResponse +import org.listenbrainz.android.model.feed.FeedData +import org.listenbrainz.android.model.feed.FeedEventDeletionData +import org.listenbrainz.android.model.feed.FeedEventVisibilityData +import org.listenbrainz.android.repository.feed.FeedRepository +import org.listenbrainz.android.repository.feed.FeedRepositoryImpl +import org.listenbrainz.android.service.FeedService +import org.listenbrainz.android.util.Resource +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername +import org.listenbrainz.sharedtest.utils.ResourceString.follow_listens_page_1 +import org.listenbrainz.sharedtest.utils.ResourceString.my_feed_page_1 +import org.listenbrainz.sharedtest.utils.ResourceString.similar_listens_page_1 +import org.listenbrainz.sharedtest.utils.ResourceString.status_ok +import org.listenbrainz.sharedtest.utils.RetrofitUtils +import org.mockito.junit.MockitoJUnitRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class FeedRepositoryTest : BaseUnitTest() { + + private lateinit var webServer: MockWebServer + private lateinit var repository: FeedRepository + @Before + fun setup() { + webServer = MockWebServer() + webServer.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return when (request.path) { + + "/user/$testUsername/feed/events?count=${FeedRepository.FeedEventCount}" -> MockResponse().setResponseCode(200).setBody( + my_feed_page_1 + ) + + "/user/$testUsername/feed/events/listens/following?count=${FeedRepository.FeedListensCount}" -> MockResponse().setResponseCode(200).setBody( + follow_listens_page_1 + ) + + "/user/$testUsername/feed/events/listens/similar?count=${FeedRepository.FeedListensCount}" -> MockResponse().setResponseCode(200).setBody( + similar_listens_page_1 + ) + + "/user/$testUsername/feed/events/delete" -> MockResponse().setResponseCode(200).setBody( + status_ok + ) + + "/user/$testUsername/feed/events/hide" -> MockResponse().setResponseCode(200).setBody( + status_ok + ) + + "/user/$testUsername/feed/events/unhide" -> MockResponse().setResponseCode(200).setBody( + status_ok + ) + + else -> MockResponse().setResponseCode(400) + } + + } + } + webServer.start() + val service = RetrofitUtils.createTestService(FeedService::class.java, webServer.url("/")) + repository = FeedRepositoryImpl(service) + } + + @After + fun teardown() { + webServer.close() + } + + @Test + fun `test getFeedEvents`() = test { + val result = repository.getFeedEvents(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(my_feed_page_1, TypeToken.get(FeedData::class.java)), result.data) + } + + @Test + fun `test getFeedFollowListens`() = test { + val result = repository.getFeedFollowListens(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(follow_listens_page_1, TypeToken.get(FeedData::class.java)), result.data) + } + + @Test + fun `getFeedSimilarListens - success`() = test { + val result = repository.getFeedSimilarListens(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(similar_listens_page_1, TypeToken.get(FeedData::class.java)), result.data) + } + + @Test + fun `deleteEvent - success`() = test { + val result = repository.deleteEvent(testUsername, FeedEventDeletionData()) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(status_ok, TypeToken.get(SocialResponse::class.java)), result.data) + } + + @Test + fun `hideEvent - success`() = test { + val result = repository.hideEvent(testUsername, FeedEventVisibilityData()) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(status_ok, TypeToken.get(SocialResponse::class.java)), result.data) + } + + @Test + fun `unhideEvent - success`() = test { + val result = repository.hideEvent(testUsername, FeedEventVisibilityData()) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(Gson().fromJson(status_ok, TypeToken.get(SocialResponse::class.java)), result.data) + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/listenbrainz/android/FeedViewModelTest.kt b/app/src/test/java/org/listenbrainz/android/FeedViewModelTest.kt new file mode 100644 index 00000000..aff28d75 --- /dev/null +++ b/app/src/test/java/org/listenbrainz/android/FeedViewModelTest.kt @@ -0,0 +1,139 @@ +package org.listenbrainz.android + +import com.jasjeet.typesafe_datastore_test.MockPrimitivePreference +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.SocialResponse +import org.listenbrainz.android.model.feed.FeedEvent +import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedEventVisibilityData +import org.listenbrainz.android.repository.feed.FeedRepository +import org.listenbrainz.android.repository.listens.ListensRepository +import org.listenbrainz.android.repository.preferences.AppPreferences +import org.listenbrainz.android.repository.remoteplayer.RemotePlaybackHandler +import org.listenbrainz.android.repository.social.SocialRepository +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.viewmodel.FeedViewModel +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testFamiliarUser +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.wheneverBlocking + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class FeedViewModelTest: BaseUnitTest() +{ + private lateinit var viewModel : FeedViewModel + + @Mock + private lateinit var socialRepository: SocialRepository + + @Mock + private lateinit var feedRepository: FeedRepository + + @Mock + private lateinit var appPreferences: AppPreferences + + @Mock + private lateinit var listensRepository: ListensRepository + + @Mock + private lateinit var remotePlaybackHandler: RemotePlaybackHandler + + @Before + fun setup() { + /*wheneverBlocking { + feedRepository.getFeedEvents(username = testUsername, maxTs = null, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.my_feed_page_1.toClass()) + ) + + wheneverBlocking { + feedRepository.getFeedEvents(username = testUsername, maxTs = 1692700512, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.my_feed_page_2.toClass()) + ) + + wheneverBlocking { + feedRepository.getFeedSimilarListens(username = testUsername, maxTs = null, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.similar_listens_page_1.toClass()) + ) + + wheneverBlocking { + feedRepository.getFeedSimilarListens(username = testUsername, maxTs = 1694881863, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.similar_listens_page_2.toClass()) + ) + + wheneverBlocking { + feedRepository.getFeedFollowListens(username = testUsername, maxTs = null, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.follow_listens_page_1.toClass()) + ) + + wheneverBlocking { + feedRepository.getFeedFollowListens(username = testUsername, maxTs = 1694878358, count = FeedEventCount) + }.doReturn( + Resource.success(ResourceString.follow_listens_page_2.toClass()) + )*/ + + viewModel = FeedViewModel( + feedRepository, + socialRepository, + listensRepository, + appPreferences, + remotePlaybackHandler, + testDispatcher(), + testDispatcher() + ) + } + + @Test + fun `test init`() = test { + val uiState = viewModel.uiState.value + assertEquals(true, uiState.myFeedState.isDeletedMap.isEmpty()) + assertEquals(true, uiState.myFeedState.isHiddenMap.isEmpty()) + } + + @Test + fun `test hide`() = test { + /** Condition for hide is that the event is not user's. + * Meanwhile for delete, user should be the one who created the event.*/ + val mockEventType = FeedEventType.RECORDING_RECOMMENDATION + val mockEvent = FeedEvent( + 0, + 0, + mockEventType.type, + metadata = Metadata(username = testFamiliarUser), + hidden = false, + username = testFamiliarUser + ) + wheneverBlocking { + appPreferences.username + }.doReturn(MockPrimitivePreference(testUsername)) + + wheneverBlocking { + feedRepository.hideEvent(testUsername, FeedEventVisibilityData(mockEventType.type, mockEvent.id.toString())) + }.doReturn(Resource.success(SocialResponse())) + + // Calling + viewModel.hideOrDeleteEvent( + mockEvent, + mockEventType, + testUsername + ) + + advanceUntilIdle() + + val uiState = viewModel.uiState.value + assertEquals(true, uiState.myFeedState.isHiddenMap[mockEvent.id]) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/listenbrainz/android/yim/YimViewModelTest.kt b/app/src/test/java/org/listenbrainz/android/yim/YimViewModelTest.kt index 72cf9cf3..71e3c801 100644 --- a/app/src/test/java/org/listenbrainz/android/yim/YimViewModelTest.kt +++ b/app/src/test/java/org/listenbrainz/android/yim/YimViewModelTest.kt @@ -5,32 +5,43 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.BaseUnitTest +import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.util.Resource import org.listenbrainz.android.viewmodel.YimViewModel -import org.listenbrainz.sharedtest.mocks.MockAppPreferences import org.listenbrainz.sharedtest.mocks.MockYimRepository import org.listenbrainz.sharedtest.testdata.YimRepositoryTestData.testYimData import org.listenbrainz.sharedtest.utils.AssertionUtils.checkYimAssertions +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner -class YimViewModelTest{ +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class YimViewModelTest: BaseUnitTest() { private lateinit var viewModel : YimViewModel + @Mock + private lateinit var mockAppPreferences: AppPreferences + + @Mock + private lateinit var mockYimRepository: MockYimRepository + @OptIn(ExperimentalCoroutinesApi::class) @Before fun setup(){ Dispatchers.setMain(StandardTestDispatcher()) - viewModel = YimViewModel(MockYimRepository(), MockAppPreferences()) + viewModel = YimViewModel(mockYimRepository, mockAppPreferences) } @Test - fun getDataTest() = runTest { + fun getDataTest() = test { val expected = testYimData launch(Dispatchers.Main) { val resultResource = viewModel.yimData diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index e3404e4d..f6cf4841 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' implementation 'androidx.room:room-testing:2.6.1' debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + implementation "com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:$typesafe_datastore_version" implementation 'androidx.test:runner:1.5.2' implementation 'androidx.test.ext:junit:1.1.5' diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index b0dcc3bc..8bca62c1 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -1,35 +1,10 @@ package org.listenbrainz.sharedtest.mocks -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import org.listenbrainz.android.model.PermissionStatus -import org.listenbrainz.android.model.Playable -import org.listenbrainz.android.model.UiMode -import org.listenbrainz.android.repository.preferences.AppPreferences -import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN -import org.listenbrainz.android.util.LinkedService -import org.listenbrainz.sharedtest.mocks.MockPreferences.mockComplexPreference -import org.listenbrainz.sharedtest.mocks.MockPreferences.mockPrimitivePreference -import org.listenbrainz.sharedtest.utils.EntityTestUtils.testAccessToken -import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername - -private val mockDataStore: DataStore = object : DataStore { - override val data: Flow - get() = flow {} - - /** Should NOT be called. Always override [ProtoDataStore.DataStorePreference.getAndUpdate] in mock.*/ - override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { - return transform(data.first()) - } -} - /** For every new preference, add default value of the concerned shared preference as default value here. */ +/* class MockAppPreferences( override var permissionsPreference: String? = PermissionStatus.NOT_REQUESTED.name, override var onboardingCompleted: Boolean = false, @@ -40,7 +15,7 @@ class MockAppPreferences( override val version: String = "", override val isNotificationServiceAllowed: Boolean = true, override var linkedServices: List = listOf() -): ProtoDataStore(dataStore = mockDataStore), AppPreferences { +): AppPreferences { override val themePreference: ComplexPreference = mockComplexPreference(UiMode.FOLLOW_SYSTEM) @@ -72,4 +47,4 @@ class MockAppPreferences( } override suspend fun isUserLoggedIn(): Boolean = true -} \ No newline at end of file +}*/ diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt deleted file mode 100644 index c5a6afaa..00000000 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockPreferences.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.listenbrainz.sharedtest.mocks - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import org.listenbrainz.android.util.Resource - -object MockPreferences { - fun mockPrimitivePreference(mockValue: T): PrimitivePreference = - object : PrimitivePreference { - override suspend fun get(): T = mockValue - - override fun getFlow(): Flow = flow { emit(mockValue) } - - override suspend fun getAndUpdate(update: (T) -> T) {} - - override suspend fun set(value: T): Resource = Resource.success(mockValue) - } - - fun mockComplexPreference(mockValue: T) = - object : ComplexPreference { - override suspend fun get(): T = mockValue - - override fun getFlow(): Flow = flow { emit(mockValue) } - - override suspend fun getAndUpdate(update: (T) -> T) {} - - override suspend fun set(value: T): Resource = Resource.success(mockValue) - } - - fun mockPreference(mockValue: T): Preference = - object: Preference { - override suspend fun get(): T = mockValue - - override fun getFlow(): Flow = flow { emit(mockValue) } - - override suspend fun getAndUpdate(update: (T) -> T) {} - - override suspend fun set(value: T): Resource = Resource.success(mockValue) - } -} \ No newline at end of file