mirror of
https://github.com/GrakovNe/lissen-android.git
synced 2025-12-23 22:18:09 -05:00
wip
This commit is contained in:
@@ -21,6 +21,7 @@ import org.grakovne.lissen.common.moshi
|
||||
import org.grakovne.lissen.lib.domain.DetailedItem
|
||||
import org.grakovne.lissen.lib.domain.DownloadOption
|
||||
import org.grakovne.lissen.lib.domain.Library
|
||||
import org.grakovne.lissen.lib.domain.LibraryOffset
|
||||
import org.grakovne.lissen.lib.domain.LibraryType
|
||||
import org.grakovne.lissen.lib.domain.SeekTime
|
||||
import org.grakovne.lissen.lib.domain.connection.LocalUrl
|
||||
@@ -38,481 +39,511 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LissenSharedPreferences
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) {
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
fun hasCredentials(): Boolean {
|
||||
val host = getHost()
|
||||
val username = getUsername()
|
||||
val hasToken = getToken() != null || getAccessToken() != null
|
||||
|
||||
return try {
|
||||
host != null && username != null && hasToken
|
||||
} catch (ex: Exception) {
|
||||
false
|
||||
}
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) {
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
fun hasCredentials(): Boolean {
|
||||
val host = getHost()
|
||||
val username = getUsername()
|
||||
val hasToken = getToken() != null || getAccessToken() != null
|
||||
|
||||
return try {
|
||||
host != null && username != null && hasToken
|
||||
} catch (ex: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
fun clearCredentials() {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_TOKEN)
|
||||
remove(KEY_ACCESS_TOKEN)
|
||||
remove(KEY_REFRESH_TOKEN)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCredentials() {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_TOKEN)
|
||||
remove(KEY_ACCESS_TOKEN)
|
||||
remove(KEY_REFRESH_TOKEN)
|
||||
}
|
||||
|
||||
fun clearPreferences() {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_HOST)
|
||||
remove(KEY_USERNAME)
|
||||
remove(KEY_TOKEN)
|
||||
remove(KEY_ACCESS_TOKEN)
|
||||
remove(KEY_REFRESH_TOKEN)
|
||||
|
||||
remove(KEY_SERVER_VERSION)
|
||||
|
||||
remove(CACHE_FORCE_ENABLED)
|
||||
|
||||
remove(KEY_PREFERRED_LIBRARY_ID)
|
||||
remove(KEY_PREFERRED_LIBRARY_NAME)
|
||||
remove(KEY_PREFERRED_LIBRARY_TYPE)
|
||||
|
||||
remove(KEY_CUSTOM_HEADERS)
|
||||
remove(KEY_BYPASS_SSL)
|
||||
remove(KEY_LOCAL_URLS)
|
||||
|
||||
remove(KEY_PLAYING_BOOK)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPreferences() {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_HOST)
|
||||
remove(KEY_USERNAME)
|
||||
remove(KEY_TOKEN)
|
||||
remove(KEY_ACCESS_TOKEN)
|
||||
remove(KEY_REFRESH_TOKEN)
|
||||
|
||||
remove(KEY_SERVER_VERSION)
|
||||
|
||||
remove(CACHE_FORCE_ENABLED)
|
||||
|
||||
remove(KEY_PREFERRED_LIBRARY_ID)
|
||||
remove(KEY_PREFERRED_LIBRARY_NAME)
|
||||
remove(KEY_PREFERRED_LIBRARY_TYPE)
|
||||
|
||||
remove(KEY_CUSTOM_HEADERS)
|
||||
remove(KEY_BYPASS_SSL)
|
||||
remove(KEY_LOCAL_URLS)
|
||||
|
||||
remove(KEY_PLAYING_BOOK)
|
||||
}
|
||||
|
||||
fun getAutoDownloadDelayed() = sharedPreferences.getBoolean(KEY_AUTO_DOWNLOAD_DELAYED, false)
|
||||
|
||||
fun saveAutoDownloadDelayed(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(KEY_AUTO_DOWNLOAD_DELAYED, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAutoDownloadDelayed() = sharedPreferences.getBoolean(KEY_AUTO_DOWNLOAD_DELAYED, false)
|
||||
|
||||
fun saveAutoDownloadDelayed(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(KEY_AUTO_DOWNLOAD_DELAYED, enabled)
|
||||
}
|
||||
|
||||
fun getAcraEnabled() = sharedPreferences.getBoolean(org.acra.ACRA.PREF_ENABLE_ACRA, true)
|
||||
|
||||
fun saveAcraEnabled(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(org.acra.ACRA.PREF_ENABLE_ACRA, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAcraEnabled() = sharedPreferences.getBoolean(org.acra.ACRA.PREF_ENABLE_ACRA, true)
|
||||
|
||||
fun saveAcraEnabled(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(org.acra.ACRA.PREF_ENABLE_ACRA, enabled)
|
||||
}
|
||||
|
||||
fun getSslBypass() = sharedPreferences.getBoolean(KEY_BYPASS_SSL, false)
|
||||
|
||||
fun saveSslBypass(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(KEY_BYPASS_SSL, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSslBypass() = sharedPreferences.getBoolean(KEY_BYPASS_SSL, false)
|
||||
|
||||
fun saveSslBypass(enabled: Boolean) {
|
||||
sharedPreferences.edit {
|
||||
putBoolean(KEY_BYPASS_SSL, enabled)
|
||||
}
|
||||
|
||||
fun saveHost(host: String) = sharedPreferences.edit { putString(KEY_HOST, host) }
|
||||
|
||||
fun getHost(): String? = sharedPreferences.getString(KEY_HOST, null)
|
||||
|
||||
fun getDeviceId(): String {
|
||||
val existingDeviceId = sharedPreferences.getString(KEY_DEVICE_ID, null)
|
||||
|
||||
if (existingDeviceId != null) {
|
||||
return existingDeviceId
|
||||
}
|
||||
|
||||
return UUID
|
||||
.randomUUID()
|
||||
.toString()
|
||||
.also { sharedPreferences.edit { putString(KEY_DEVICE_ID, it) } }
|
||||
}
|
||||
|
||||
fun saveHost(host: String) = sharedPreferences.edit { putString(KEY_HOST, host) }
|
||||
|
||||
fun getHost(): String? = sharedPreferences.getString(KEY_HOST, null)
|
||||
|
||||
fun getDeviceId(): String {
|
||||
val existingDeviceId = sharedPreferences.getString(KEY_DEVICE_ID, null)
|
||||
|
||||
if (existingDeviceId != null) {
|
||||
return existingDeviceId
|
||||
}
|
||||
|
||||
// Once the different channel will supported, this shall be extended
|
||||
fun getChannel() = ChannelCode.AUDIOBOOKSHELF
|
||||
|
||||
fun getPreferredLibrary(): Library? {
|
||||
val id = getPreferredLibraryId() ?: return null
|
||||
val name = getPreferredLibraryName() ?: return null
|
||||
|
||||
val type = getPreferredLibraryType()
|
||||
|
||||
return Library(
|
||||
id = id,
|
||||
title = name,
|
||||
type = type,
|
||||
)
|
||||
|
||||
return UUID
|
||||
.randomUUID()
|
||||
.toString()
|
||||
.also { sharedPreferences.edit { putString(KEY_DEVICE_ID, it) } }
|
||||
}
|
||||
|
||||
// Once the different channel will supported, this shall be extended
|
||||
fun getChannel() = ChannelCode.AUDIOBOOKSHELF
|
||||
|
||||
fun getPreferredLibrary(): Library? {
|
||||
val id = getPreferredLibraryId() ?: return null
|
||||
val name = getPreferredLibraryName() ?: return null
|
||||
|
||||
val type = getPreferredLibraryType()
|
||||
|
||||
return Library(
|
||||
id = id,
|
||||
title = name,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
|
||||
fun savePreferredLibrary(library: Library) {
|
||||
saveActiveLibraryId(library.id)
|
||||
saveActiveLibraryName(library.title)
|
||||
saveActiveLibraryType(library.type)
|
||||
}
|
||||
|
||||
fun saveLibraryOrdering(configuration: LibraryOrderingConfiguration) {
|
||||
val adapter = moshi.adapter(LibraryOrderingConfiguration::class.java)
|
||||
|
||||
val json = adapter.toJson(configuration)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_LIBRARY_ORDERING, json)
|
||||
}
|
||||
|
||||
fun savePreferredLibrary(library: Library) {
|
||||
saveActiveLibraryId(library.id)
|
||||
saveActiveLibraryName(library.title)
|
||||
saveActiveLibraryType(library.type)
|
||||
}
|
||||
|
||||
fun saveLibraryOrdering(configuration: LibraryOrderingConfiguration) {
|
||||
val adapter = moshi.adapter(LibraryOrderingConfiguration::class.java)
|
||||
|
||||
val json = adapter.toJson(configuration)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_LIBRARY_ORDERING, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryOrdering(): LibraryOrderingConfiguration {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_ORDERING, null)
|
||||
return when (json) {
|
||||
null -> LibraryOrderingConfiguration.default
|
||||
else -> {
|
||||
val adapter = moshi.adapter(LibraryOrderingConfiguration::class.java)
|
||||
adapter.fromJson(json) ?: LibraryOrderingConfiguration.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun savePlaybackVolumeBoost(playbackVolumeBoost: PlaybackVolumeBoost) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_VOLUME_BOOST, playbackVolumeBoost.name)
|
||||
}
|
||||
|
||||
fun getPlaybackVolumeBoost(): PlaybackVolumeBoost =
|
||||
sharedPreferences
|
||||
.getString(KEY_VOLUME_BOOST, PlaybackVolumeBoost.DISABLED.name)
|
||||
?.let { PlaybackVolumeBoost.valueOf(it) }
|
||||
?: PlaybackVolumeBoost.DISABLED
|
||||
|
||||
fun saveAutoDownloadNetworkType(networkTypeAutoCache: NetworkTypeAutoCache) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE, networkTypeAutoCache.name)
|
||||
}
|
||||
|
||||
fun getAutoDownloadNetworkType(): NetworkTypeAutoCache =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE, NetworkTypeAutoCache.WIFI_ONLY.name)
|
||||
?.let { NetworkTypeAutoCache.valueOf(it) }
|
||||
?: NetworkTypeAutoCache.WIFI_ONLY
|
||||
|
||||
fun saveAutoDownloadLibraryTypes(types: List<LibraryType>) {
|
||||
val type = Types.newParameterizedType(List::class.java, LibraryType::class.java)
|
||||
val adapter = moshi.adapter<List<LibraryType>>(type)
|
||||
val json = adapter.toJson(types)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAutoDownloadLibraryTypes(): List<LibraryType> {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE, null)
|
||||
|
||||
return when (json) {
|
||||
null -> LibraryType.meaningfulTypes
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, LibraryType::class.java)
|
||||
val adapter = moshi.adapter<List<LibraryType>>(type)
|
||||
adapter.fromJson(json) ?: LibraryType.meaningfulTypes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveColorScheme(colorScheme: ColorScheme) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_COLOR_SCHEME, colorScheme.name)
|
||||
}
|
||||
|
||||
fun getColorScheme(): ColorScheme =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_COLOR_SCHEME, ColorScheme.FOLLOW_SYSTEM.name)
|
||||
?.let { ColorScheme.valueOf(it) }
|
||||
?: ColorScheme.FOLLOW_SYSTEM
|
||||
|
||||
fun saveAutoDownloadOption(option: DownloadOption?) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD, option?.makeId())
|
||||
}
|
||||
|
||||
fun getAutoDownloadOption(): DownloadOption? =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_AUTO_DOWNLOAD, null)
|
||||
?.makeDownloadOption()
|
||||
|
||||
fun savePlaybackSpeed(factor: Float) = sharedPreferences.edit { putFloat(KEY_PREFERRED_PLAYBACK_SPEED, factor) }
|
||||
|
||||
fun getPlaybackSpeed(): Float = sharedPreferences.getFloat(KEY_PREFERRED_PLAYBACK_SPEED, 1f)
|
||||
|
||||
val playingBookFlow: Flow<DetailedItem?> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_PLAYING_BOOK) {
|
||||
trySend(getPlayingBook())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getPlayingBook())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val playbackVolumeBoostFlow: Flow<PlaybackVolumeBoost> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_VOLUME_BOOST) {
|
||||
trySend(getPlaybackVolumeBoost())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getPlaybackVolumeBoost())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val colorSchemeFlow: Flow<ColorScheme> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_PREFERRED_COLOR_SCHEME) {
|
||||
trySend(getColorScheme())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getColorScheme())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun saveActiveLibraryId(host: String) = sharedPreferences.edit { putString(KEY_PREFERRED_LIBRARY_ID, host) }
|
||||
|
||||
private fun getPreferredLibraryId(): String? = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_ID, null)
|
||||
|
||||
private fun saveActiveLibraryName(host: String) = sharedPreferences.edit { putString(KEY_PREFERRED_LIBRARY_NAME, host) }
|
||||
|
||||
private fun getPreferredLibraryType(): LibraryType =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_LIBRARY_TYPE, null)
|
||||
?.let { LibraryType.valueOf(it) }
|
||||
?: LibraryType.LIBRARY
|
||||
|
||||
private fun saveActiveLibraryType(type: LibraryType) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_LIBRARY_TYPE, type.name)
|
||||
}
|
||||
|
||||
private fun getPreferredLibraryName(): String? = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_NAME, null)
|
||||
|
||||
fun enableForceCache() = sharedPreferences.edit { putBoolean(CACHE_FORCE_ENABLED, true) }
|
||||
|
||||
fun disableForceCache() = sharedPreferences.edit { putBoolean(CACHE_FORCE_ENABLED, false) }
|
||||
|
||||
fun isForceCache(): Boolean = sharedPreferences.getBoolean(CACHE_FORCE_ENABLED, false)
|
||||
|
||||
fun saveUsername(username: String) = sharedPreferences.edit { putString(KEY_USERNAME, username) }
|
||||
|
||||
fun getUsername(): String? = sharedPreferences.getString(KEY_USERNAME, null)
|
||||
|
||||
fun saveServerVersion(version: String) = sharedPreferences.edit { putString(KEY_SERVER_VERSION, version) }
|
||||
|
||||
fun getServerVersion(): String? = sharedPreferences.getString(KEY_SERVER_VERSION, null)
|
||||
|
||||
fun saveToken(token: String) {
|
||||
val encrypted = encrypt(token)
|
||||
sharedPreferences.edit { putString(KEY_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun saveAccessToken(accessToken: String) {
|
||||
val encrypted = encrypt(accessToken)
|
||||
sharedPreferences.edit { putString(KEY_ACCESS_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun saveRefreshToken(refreshToken: String) {
|
||||
val encrypted = encrypt(refreshToken)
|
||||
sharedPreferences.edit { putString(KEY_REFRESH_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun getAccessToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun getRefreshToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_REFRESH_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun getToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun savePlayingBook(book: DetailedItem?) {
|
||||
if (book == null) {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_PLAYING_BOOK)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = moshi.adapter(DetailedItem::class.java)
|
||||
val json = adapter.toJson(book)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PLAYING_BOOK, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlayingBook(): DetailedItem? {
|
||||
val json = sharedPreferences.getString(KEY_PLAYING_BOOK, null)
|
||||
|
||||
return when (json) {
|
||||
null -> null
|
||||
else -> {
|
||||
val adapter = moshi.adapter(DetailedItem::class.java)
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSeekTime(seekTime: SeekTime) {
|
||||
val adapter = moshi.adapter(SeekTime::class.java)
|
||||
val json = adapter.toJson(seekTime)
|
||||
|
||||
sharedPreferences.edit(commit = true) { putString(KEY_PREFERRED_SEEK_TIME, json) }
|
||||
}
|
||||
|
||||
fun getSeekTime(): SeekTime {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_SEEK_TIME, null)
|
||||
return when (json) {
|
||||
null -> SeekTime.Default
|
||||
else -> {
|
||||
val adapter = moshi.adapter(SeekTime::class.java)
|
||||
adapter.fromJson(json) ?: SeekTime.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCustomHeaders(headers: List<ServerRequestHeader>) {
|
||||
val type = Types.newParameterizedType(List::class.java, ServerRequestHeader::class.java)
|
||||
val adapter = moshi.adapter<List<ServerRequestHeader>>(type)
|
||||
val json = adapter.toJson(headers)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_CUSTOM_HEADERS, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCustomHeaders(): List<ServerRequestHeader> {
|
||||
val json = sharedPreferences.getString(KEY_CUSTOM_HEADERS, null)
|
||||
return when (json) {
|
||||
null -> emptyList()
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, ServerRequestHeader::class.java)
|
||||
val adapter = moshi.adapter<List<ServerRequestHeader>>(type)
|
||||
adapter.fromJson(json) ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalUrls(urls: List<LocalUrl>) {
|
||||
val type = Types.newParameterizedType(List::class.java, LocalUrl::class.java)
|
||||
val adapter = moshi.adapter<List<LocalUrl>>(type)
|
||||
val json = adapter.toJson(urls)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_LOCAL_URLS, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocalUrls(): List<LocalUrl> {
|
||||
val json = sharedPreferences.getString(KEY_LOCAL_URLS, null)
|
||||
return when (json) {
|
||||
null -> emptyList()
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, LocalUrl::class.java)
|
||||
val adapter = moshi.adapter<List<LocalUrl>>(type)
|
||||
adapter.fromJson(json) ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_ALIAS = "secure_key_alias"
|
||||
private const val KEY_HOST = "host"
|
||||
private const val KEY_USERNAME = "username"
|
||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||
private const val KEY_TOKEN = "token"
|
||||
private const val CACHE_FORCE_ENABLED = "cache_force_enabled"
|
||||
|
||||
private const val KEY_SERVER_VERSION = "server_version"
|
||||
|
||||
private const val KEY_DEVICE_ID = "device_id"
|
||||
|
||||
private const val KEY_PREFERRED_LIBRARY_ID = "preferred_library_id"
|
||||
private const val KEY_PREFERRED_LIBRARY_NAME = "preferred_library_name"
|
||||
private const val KEY_PREFERRED_LIBRARY_TYPE = "preferred_library_type"
|
||||
|
||||
private const val KEY_PREFERRED_PLAYBACK_SPEED = "preferred_playback_speed"
|
||||
private const val KEY_PREFERRED_SEEK_TIME = "preferred_seek_time"
|
||||
|
||||
private const val KEY_PREFERRED_COLOR_SCHEME = "preferred_color_scheme"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD = "preferred_auto_download"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE = "preferred_auto_download_network_type"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE = "preferred_auto_download_library_type"
|
||||
private const val KEY_AUTO_DOWNLOAD_DELAYED = "auto_download_delayed"
|
||||
private const val KEY_PREFERRED_LIBRARY_ORDERING = "preferred_library_ordering"
|
||||
|
||||
private const val KEY_CUSTOM_HEADERS = "custom_headers"
|
||||
private const val KEY_BYPASS_SSL = "bypass_ssl"
|
||||
private const val KEY_LOCAL_URLS = "local_urls"
|
||||
|
||||
private const val KEY_PLAYING_BOOK = "playing_book"
|
||||
private const val KEY_VOLUME_BOOST = "volume_boost"
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
|
||||
private fun getSecretKey(): SecretKey {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||
keyStore.load(null)
|
||||
|
||||
keyStore.getKey(KEY_ALIAS, null)?.let {
|
||||
return it as SecretKey
|
||||
}
|
||||
|
||||
val keyGenerator =
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
|
||||
val keyGenParameterSpec =
|
||||
KeyGenParameterSpec
|
||||
.Builder(
|
||||
KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build()
|
||||
keyGenerator.init(keyGenParameterSpec)
|
||||
return keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
private fun encrypt(data: String): String {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
|
||||
|
||||
val cipherText = cipher.doFinal(data.toByteArray())
|
||||
val ivAndCipherText = cipher.iv + cipherText
|
||||
|
||||
return Base64.encodeToString(ivAndCipherText, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
private fun decrypt(data: String): String? {
|
||||
val decodedData = Base64.decode(data, Base64.DEFAULT)
|
||||
val iv = decodedData.sliceArray(0 until 12)
|
||||
val cipherText = decodedData.sliceArray(12 until decodedData.size)
|
||||
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
|
||||
|
||||
return try {
|
||||
String(cipher.doFinal(cipherText))
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryOrdering(): LibraryOrderingConfiguration {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_ORDERING, null)
|
||||
return when (json) {
|
||||
null -> LibraryOrderingConfiguration.default
|
||||
else -> {
|
||||
val adapter = moshi.adapter(LibraryOrderingConfiguration::class.java)
|
||||
adapter.fromJson(json) ?: LibraryOrderingConfiguration.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun savePlaybackVolumeBoost(playbackVolumeBoost: PlaybackVolumeBoost) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_VOLUME_BOOST, playbackVolumeBoost.name)
|
||||
}
|
||||
|
||||
fun getPlaybackVolumeBoost(): PlaybackVolumeBoost =
|
||||
sharedPreferences
|
||||
.getString(KEY_VOLUME_BOOST, PlaybackVolumeBoost.DISABLED.name)
|
||||
?.let { PlaybackVolumeBoost.valueOf(it) }
|
||||
?: PlaybackVolumeBoost.DISABLED
|
||||
|
||||
fun saveAutoDownloadNetworkType(networkTypeAutoCache: NetworkTypeAutoCache) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE, networkTypeAutoCache.name)
|
||||
}
|
||||
|
||||
fun getAutoDownloadNetworkType(): NetworkTypeAutoCache =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE, NetworkTypeAutoCache.WIFI_ONLY.name)
|
||||
?.let { NetworkTypeAutoCache.valueOf(it) }
|
||||
?: NetworkTypeAutoCache.WIFI_ONLY
|
||||
|
||||
fun saveAutoDownloadLibraryTypes(types: List<LibraryType>) {
|
||||
val type = Types.newParameterizedType(List::class.java, LibraryType::class.java)
|
||||
val adapter = moshi.adapter<List<LibraryType>>(type)
|
||||
val json = adapter.toJson(types)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAutoDownloadLibraryTypes(): List<LibraryType> {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE, null)
|
||||
|
||||
return when (json) {
|
||||
null -> LibraryType.meaningfulTypes
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, LibraryType::class.java)
|
||||
val adapter = moshi.adapter<List<LibraryType>>(type)
|
||||
adapter.fromJson(json) ?: LibraryType.meaningfulTypes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveColorScheme(colorScheme: ColorScheme) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_COLOR_SCHEME, colorScheme.name)
|
||||
}
|
||||
|
||||
fun getColorScheme(): ColorScheme =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_COLOR_SCHEME, ColorScheme.FOLLOW_SYSTEM.name)
|
||||
?.let { ColorScheme.valueOf(it) }
|
||||
?: ColorScheme.FOLLOW_SYSTEM
|
||||
|
||||
fun saveAutoDownloadOption(option: DownloadOption?) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_AUTO_DOWNLOAD, option?.makeId())
|
||||
}
|
||||
|
||||
fun getAutoDownloadOption(): DownloadOption? =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_AUTO_DOWNLOAD, null)
|
||||
?.makeDownloadOption()
|
||||
|
||||
fun savePlaybackSpeed(factor: Float) = sharedPreferences.edit { putFloat(KEY_PREFERRED_PLAYBACK_SPEED, factor) }
|
||||
|
||||
fun getPlaybackSpeed(): Float = sharedPreferences.getFloat(KEY_PREFERRED_PLAYBACK_SPEED, 1f)
|
||||
|
||||
val playingBookFlow: Flow<DetailedItem?> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_PLAYING_BOOK) {
|
||||
trySend(getPlayingBook())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getPlayingBook())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val playbackVolumeBoostFlow: Flow<PlaybackVolumeBoost> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_VOLUME_BOOST) {
|
||||
trySend(getPlaybackVolumeBoost())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getPlaybackVolumeBoost())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val colorSchemeFlow: Flow<ColorScheme> =
|
||||
callbackFlow {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == KEY_PREFERRED_COLOR_SCHEME) {
|
||||
trySend(getColorScheme())
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
trySend(getColorScheme())
|
||||
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun saveActiveLibraryId(host: String) = sharedPreferences.edit { putString(KEY_PREFERRED_LIBRARY_ID, host) }
|
||||
|
||||
private fun getPreferredLibraryId(): String? = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_ID, null)
|
||||
|
||||
private fun saveActiveLibraryName(host: String) = sharedPreferences.edit { putString(KEY_PREFERRED_LIBRARY_NAME, host) }
|
||||
|
||||
private fun getPreferredLibraryType(): LibraryType =
|
||||
sharedPreferences
|
||||
.getString(KEY_PREFERRED_LIBRARY_TYPE, null)
|
||||
?.let { LibraryType.valueOf(it) }
|
||||
?: LibraryType.LIBRARY
|
||||
|
||||
private fun saveActiveLibraryType(type: LibraryType) =
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PREFERRED_LIBRARY_TYPE, type.name)
|
||||
}
|
||||
|
||||
private fun getPreferredLibraryName(): String? = sharedPreferences.getString(KEY_PREFERRED_LIBRARY_NAME, null)
|
||||
|
||||
fun enableForceCache() = sharedPreferences.edit { putBoolean(CACHE_FORCE_ENABLED, true) }
|
||||
|
||||
fun disableForceCache() = sharedPreferences.edit { putBoolean(CACHE_FORCE_ENABLED, false) }
|
||||
|
||||
fun isForceCache(): Boolean = sharedPreferences.getBoolean(CACHE_FORCE_ENABLED, false)
|
||||
|
||||
fun saveUsername(username: String) = sharedPreferences.edit { putString(KEY_USERNAME, username) }
|
||||
|
||||
fun getUsername(): String? = sharedPreferences.getString(KEY_USERNAME, null)
|
||||
|
||||
fun saveServerVersion(version: String) = sharedPreferences.edit { putString(KEY_SERVER_VERSION, version) }
|
||||
|
||||
fun getServerVersion(): String? = sharedPreferences.getString(KEY_SERVER_VERSION, null)
|
||||
|
||||
fun saveToken(token: String) {
|
||||
val encrypted = encrypt(token)
|
||||
sharedPreferences.edit { putString(KEY_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun saveAccessToken(accessToken: String) {
|
||||
val encrypted = encrypt(accessToken)
|
||||
sharedPreferences.edit { putString(KEY_ACCESS_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun saveRefreshToken(refreshToken: String) {
|
||||
val encrypted = encrypt(refreshToken)
|
||||
sharedPreferences.edit { putString(KEY_REFRESH_TOKEN, encrypted) }
|
||||
}
|
||||
|
||||
fun getAccessToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun getRefreshToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_REFRESH_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun getToken(): String? {
|
||||
val encrypted = sharedPreferences.getString(KEY_TOKEN, null) ?: return null
|
||||
return decrypt(encrypted)
|
||||
}
|
||||
|
||||
fun savePlayingBook(book: DetailedItem?) {
|
||||
if (book == null) {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_PLAYING_BOOK)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = moshi.adapter(DetailedItem::class.java)
|
||||
val json = adapter.toJson(book)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PLAYING_BOOK, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlayingBook(): DetailedItem? {
|
||||
val json = sharedPreferences.getString(KEY_PLAYING_BOOK, null)
|
||||
|
||||
return when (json) {
|
||||
null -> null
|
||||
else -> {
|
||||
val adapter = moshi.adapter(DetailedItem::class.java)
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSeekTime(seekTime: SeekTime) {
|
||||
val adapter = moshi.adapter(SeekTime::class.java)
|
||||
val json = adapter.toJson(seekTime)
|
||||
|
||||
sharedPreferences.edit(commit = true) { putString(KEY_PREFERRED_SEEK_TIME, json) }
|
||||
}
|
||||
|
||||
fun getSeekTime(): SeekTime {
|
||||
val json = sharedPreferences.getString(KEY_PREFERRED_SEEK_TIME, null)
|
||||
return when (json) {
|
||||
null -> SeekTime.Default
|
||||
else -> {
|
||||
val adapter = moshi.adapter(SeekTime::class.java)
|
||||
adapter.fromJson(json) ?: SeekTime.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun saveLibraryOffset(offset: LibraryOffset?) {
|
||||
if (offset == null) {
|
||||
sharedPreferences.edit {
|
||||
remove(KEY_LIBRARY_OFFSET)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = moshi.adapter(LibraryOffset::class.java)
|
||||
val json = adapter.toJson(offset)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_LIBRARY_OFFSET, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryOffset(): LibraryOffset? {
|
||||
val json = sharedPreferences.getString(KEY_LIBRARY_OFFSET, null)
|
||||
|
||||
return when (json) {
|
||||
null -> null
|
||||
else -> {
|
||||
val adapter = moshi.adapter(LibraryOffset::class.java)
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCustomHeaders(headers: List<ServerRequestHeader>) {
|
||||
val type = Types.newParameterizedType(List::class.java, ServerRequestHeader::class.java)
|
||||
val adapter = moshi.adapter<List<ServerRequestHeader>>(type)
|
||||
val json = adapter.toJson(headers)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_CUSTOM_HEADERS, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCustomHeaders(): List<ServerRequestHeader> {
|
||||
val json = sharedPreferences.getString(KEY_CUSTOM_HEADERS, null)
|
||||
return when (json) {
|
||||
null -> emptyList()
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, ServerRequestHeader::class.java)
|
||||
val adapter = moshi.adapter<List<ServerRequestHeader>>(type)
|
||||
adapter.fromJson(json) ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalUrls(urls: List<LocalUrl>) {
|
||||
val type = Types.newParameterizedType(List::class.java, LocalUrl::class.java)
|
||||
val adapter = moshi.adapter<List<LocalUrl>>(type)
|
||||
val json = adapter.toJson(urls)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_LOCAL_URLS, json)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocalUrls(): List<LocalUrl> {
|
||||
val json = sharedPreferences.getString(KEY_LOCAL_URLS, null)
|
||||
return when (json) {
|
||||
null -> emptyList()
|
||||
else -> {
|
||||
val type = Types.newParameterizedType(List::class.java, LocalUrl::class.java)
|
||||
val adapter = moshi.adapter<List<LocalUrl>>(type)
|
||||
adapter.fromJson(json) ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_ALIAS = "secure_key_alias"
|
||||
private const val KEY_HOST = "host"
|
||||
private const val KEY_USERNAME = "username"
|
||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||
private const val KEY_TOKEN = "token"
|
||||
private const val CACHE_FORCE_ENABLED = "cache_force_enabled"
|
||||
|
||||
private const val KEY_SERVER_VERSION = "server_version"
|
||||
|
||||
private const val KEY_DEVICE_ID = "device_id"
|
||||
|
||||
private const val KEY_PREFERRED_LIBRARY_ID = "preferred_library_id"
|
||||
private const val KEY_PREFERRED_LIBRARY_NAME = "preferred_library_name"
|
||||
private const val KEY_PREFERRED_LIBRARY_TYPE = "preferred_library_type"
|
||||
|
||||
private const val KEY_PREFERRED_PLAYBACK_SPEED = "preferred_playback_speed"
|
||||
private const val KEY_PREFERRED_SEEK_TIME = "preferred_seek_time"
|
||||
|
||||
private const val KEY_PREFERRED_COLOR_SCHEME = "preferred_color_scheme"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD = "preferred_auto_download"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD_NETWORK_TYPE = "preferred_auto_download_network_type"
|
||||
private const val KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE = "preferred_auto_download_library_type"
|
||||
private const val KEY_AUTO_DOWNLOAD_DELAYED = "auto_download_delayed"
|
||||
private const val KEY_PREFERRED_LIBRARY_ORDERING = "preferred_library_ordering"
|
||||
|
||||
private const val KEY_CUSTOM_HEADERS = "custom_headers"
|
||||
private const val KEY_BYPASS_SSL = "bypass_ssl"
|
||||
private const val KEY_LOCAL_URLS = "local_urls"
|
||||
|
||||
private const val KEY_PLAYING_BOOK = "playing_book"
|
||||
private const val KEY_VOLUME_BOOST = "volume_boost"
|
||||
|
||||
private const val KEY_LIBRARY_OFFSET = "library_offset"
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
|
||||
private fun getSecretKey(): SecretKey {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
|
||||
keyStore.load(null)
|
||||
|
||||
keyStore.getKey(KEY_ALIAS, null)?.let {
|
||||
return it as SecretKey
|
||||
}
|
||||
|
||||
val keyGenerator =
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
|
||||
val keyGenParameterSpec =
|
||||
KeyGenParameterSpec
|
||||
.Builder(
|
||||
KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build()
|
||||
keyGenerator.init(keyGenParameterSpec)
|
||||
return keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
private fun encrypt(data: String): String {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
|
||||
|
||||
val cipherText = cipher.doFinal(data.toByteArray())
|
||||
val ivAndCipherText = cipher.iv + cipherText
|
||||
|
||||
return Base64.encodeToString(ivAndCipherText, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
private fun decrypt(data: String): String? {
|
||||
val decodedData = Base64.decode(data, Base64.DEFAULT)
|
||||
val iv = decodedData.sliceArray(0 until 12)
|
||||
val cipherText = decodedData.sliceArray(12 until decodedData.size)
|
||||
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
|
||||
|
||||
return try {
|
||||
String(cipher.doFinal(cipherText))
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.grakovne.lissen.content.LissenMediaProvider
|
||||
import org.grakovne.lissen.lib.domain.Book
|
||||
import org.grakovne.lissen.lib.domain.LibraryOffset
|
||||
import org.grakovne.lissen.lib.domain.LibraryType
|
||||
import org.grakovne.lissen.lib.domain.RecentBook
|
||||
import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences
|
||||
@@ -32,147 +33,181 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class LibraryViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaChannel: LissenMediaProvider,
|
||||
private val preferences: LissenSharedPreferences,
|
||||
) : ViewModel() {
|
||||
private val _recentBooks = MutableLiveData<List<RecentBook>>(emptyList())
|
||||
val recentBooks: LiveData<List<RecentBook>> = _recentBooks
|
||||
|
||||
private val _recentBookUpdating = MutableLiveData(false)
|
||||
val recentBookUpdating: LiveData<Boolean> = _recentBookUpdating
|
||||
|
||||
private val _searchRequested = MutableLiveData(false)
|
||||
val searchRequested: LiveData<Boolean> = _searchRequested
|
||||
|
||||
private val _searchToken = MutableStateFlow(EMPTY_SEARCH)
|
||||
|
||||
private var defaultPagingSource: PagingSource<Int, Book>? = null
|
||||
private var searchPagingSource: PagingSource<Int, Book>? = null
|
||||
|
||||
private val _totalCount = MutableLiveData<Int>()
|
||||
val totalCount: LiveData<Int> = _totalCount
|
||||
|
||||
private val pageConfig =
|
||||
PagingConfig(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialLoadSize = PAGE_SIZE,
|
||||
prefetchDistance = PAGE_SIZE,
|
||||
)
|
||||
|
||||
fun getPager(isSearchRequested: Boolean) =
|
||||
when (isSearchRequested) {
|
||||
true -> searchPager
|
||||
false -> libraryPager
|
||||
}
|
||||
|
||||
private val searchPager: Flow<PagingData<Book>> =
|
||||
combine(
|
||||
_searchToken,
|
||||
searchRequested.asFlow(),
|
||||
) { token, requested ->
|
||||
Pair(token, requested)
|
||||
}.flatMapLatest { (token, _) ->
|
||||
Pager(
|
||||
config = pageConfig,
|
||||
pagingSourceFactory = {
|
||||
val source =
|
||||
LibrarySearchPagingSource(
|
||||
preferences = preferences,
|
||||
mediaChannel = mediaChannel,
|
||||
searchToken = token,
|
||||
limit = PAGE_SEARCH_SIZE,
|
||||
) { _totalCount.postValue(it) }
|
||||
|
||||
searchPagingSource = source
|
||||
source
|
||||
},
|
||||
).flow
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
private val libraryPager: Flow<PagingData<Book>> by lazy {
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaChannel: LissenMediaProvider,
|
||||
private val preferences: LissenSharedPreferences,
|
||||
) : ViewModel() {
|
||||
private val _recentBooks = MutableLiveData<List<RecentBook>>(emptyList())
|
||||
val recentBooks: LiveData<List<RecentBook>> = _recentBooks
|
||||
|
||||
private val _recentBookUpdating = MutableLiveData(false)
|
||||
val recentBookUpdating: LiveData<Boolean> = _recentBookUpdating
|
||||
|
||||
private val _searchRequested = MutableLiveData(false)
|
||||
val searchRequested: LiveData<Boolean> = _searchRequested
|
||||
|
||||
private val _searchToken = MutableStateFlow(EMPTY_SEARCH)
|
||||
|
||||
private var defaultPagingSource: PagingSource<Int, Book>? = null
|
||||
private var searchPagingSource: PagingSource<Int, Book>? = null
|
||||
|
||||
private val _totalCount = MutableLiveData<Int>()
|
||||
val totalCount: LiveData<Int> = _totalCount
|
||||
|
||||
private val _libraryOffset = MutableLiveData<LibraryOffset?>(preferences.getLibraryOffset())
|
||||
|
||||
private val pageConfig =
|
||||
PagingConfig(
|
||||
pageSize = PAGE_SIZE,
|
||||
initialLoadSize = PAGE_SIZE,
|
||||
prefetchDistance = PAGE_SIZE,
|
||||
)
|
||||
|
||||
fun getPager(isSearchRequested: Boolean) =
|
||||
when (isSearchRequested) {
|
||||
true -> searchPager
|
||||
false -> libraryPager
|
||||
}
|
||||
|
||||
private val searchPager: Flow<PagingData<Book>> =
|
||||
combine(
|
||||
_searchToken,
|
||||
searchRequested.asFlow(),
|
||||
) { token, requested ->
|
||||
Pair(token, requested)
|
||||
}.flatMapLatest { (token, _) ->
|
||||
Pager(
|
||||
config = pageConfig,
|
||||
pagingSourceFactory = {
|
||||
val source = LibraryDefaultPagingSource(preferences, mediaChannel) { _totalCount.postValue(it) }
|
||||
defaultPagingSource = source
|
||||
|
||||
val source =
|
||||
LibrarySearchPagingSource(
|
||||
preferences = preferences,
|
||||
mediaChannel = mediaChannel,
|
||||
searchToken = token,
|
||||
limit = PAGE_SEARCH_SIZE,
|
||||
) { _totalCount.postValue(it) }
|
||||
|
||||
searchPagingSource = source
|
||||
source
|
||||
},
|
||||
).flow.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun requestSearch() {
|
||||
_searchRequested.postValue(true)
|
||||
}
|
||||
|
||||
fun dismissSearch() {
|
||||
_searchRequested.postValue(false)
|
||||
_searchToken.value = EMPTY_SEARCH
|
||||
}
|
||||
|
||||
fun updateSearch(token: String) {
|
||||
viewModelScope.launch { _searchToken.emit(token) }
|
||||
}
|
||||
|
||||
fun fetchPreferredLibraryTitle(): String? =
|
||||
preferences
|
||||
.getPreferredLibrary()
|
||||
?.title
|
||||
|
||||
fun fetchPreferredLibraryType() =
|
||||
preferences
|
||||
.getPreferredLibrary()
|
||||
?.type
|
||||
?: LibraryType.UNKNOWN
|
||||
|
||||
fun refreshRecentListening() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
fetchRecentListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshLibrary() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
when (searchRequested.value) {
|
||||
true -> searchPagingSource?.invalidate()
|
||||
else -> defaultPagingSource?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchRecentListening() {
|
||||
_recentBookUpdating.postValue(true)
|
||||
|
||||
val preferredLibrary =
|
||||
preferences.getPreferredLibrary()?.id ?: run {
|
||||
_recentBookUpdating.postValue(false)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
mediaChannel
|
||||
.fetchRecentListenedBooks(preferredLibrary)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
_recentBooks.postValue(it)
|
||||
_recentBookUpdating.postValue(false)
|
||||
},
|
||||
onFailure = {
|
||||
_recentBookUpdating.postValue(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMPTY_SEARCH = ""
|
||||
private const val PAGE_SIZE = 20
|
||||
private const val PAGE_SEARCH_SIZE = 50
|
||||
).flow
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
private val libraryPager: Flow<PagingData<Book>> by lazy {
|
||||
Pager(
|
||||
config = pageConfig,
|
||||
pagingSourceFactory = {
|
||||
val source = LibraryDefaultPagingSource(preferences, mediaChannel) { _totalCount.postValue(it) }
|
||||
defaultPagingSource = source
|
||||
|
||||
source
|
||||
},
|
||||
).flow.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun requestSearch() {
|
||||
_searchRequested.postValue(true)
|
||||
}
|
||||
|
||||
fun dismissSearch() {
|
||||
_searchRequested.postValue(false)
|
||||
_searchToken.value = EMPTY_SEARCH
|
||||
}
|
||||
|
||||
fun updateSearch(token: String) {
|
||||
viewModelScope.launch { _searchToken.emit(token) }
|
||||
}
|
||||
|
||||
fun clearLibraryOffset() {
|
||||
_libraryOffset.postValue(null)
|
||||
preferences.saveLibraryOffset(null)
|
||||
}
|
||||
|
||||
fun fetchLibraryOffset(): Int {
|
||||
val offset = preferences.getLibraryOffset() ?: return 0
|
||||
val currentLibraryId = preferences.getPreferredLibrary()?.id ?: return 0
|
||||
|
||||
return when(currentLibraryId == offset.libraryId) {
|
||||
true -> offset.offset
|
||||
false -> 0.also { clearLibraryOffset() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibraryOffset(offset: Int) {
|
||||
val currentLibraryId = preferences.getPreferredLibrary()?.id
|
||||
|
||||
if (null == currentLibraryId) {
|
||||
clearLibraryOffset()
|
||||
return
|
||||
}
|
||||
|
||||
val libraryOffset = LibraryOffset(
|
||||
libraryId = currentLibraryId,
|
||||
offset = offset
|
||||
)
|
||||
|
||||
_libraryOffset.postValue(libraryOffset)
|
||||
preferences.saveLibraryOffset(libraryOffset)
|
||||
}
|
||||
|
||||
fun fetchPreferredLibraryTitle(): String? =
|
||||
preferences
|
||||
.getPreferredLibrary()
|
||||
?.title
|
||||
|
||||
fun fetchPreferredLibraryType() =
|
||||
preferences
|
||||
.getPreferredLibrary()
|
||||
?.type
|
||||
?: LibraryType.UNKNOWN
|
||||
|
||||
fun refreshRecentListening() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
fetchRecentListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshLibrary() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
when (searchRequested.value) {
|
||||
true -> searchPagingSource?.invalidate()
|
||||
else -> defaultPagingSource?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchRecentListening() {
|
||||
_recentBookUpdating.postValue(true)
|
||||
|
||||
val preferredLibrary =
|
||||
preferences.getPreferredLibrary()?.id ?: run {
|
||||
_recentBookUpdating.postValue(false)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
mediaChannel
|
||||
.fetchRecentListenedBooks(preferredLibrary)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
_recentBooks.postValue(it)
|
||||
_recentBookUpdating.postValue(false)
|
||||
},
|
||||
onFailure = {
|
||||
_recentBookUpdating.postValue(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMPTY_SEARCH = ""
|
||||
private const val PAGE_SIZE = 20
|
||||
private const val PAGE_SEARCH_SIZE = 50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.grakovne.lissen.lib.domain
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LibraryOffset(
|
||||
val libraryId: String,
|
||||
val offset: Int
|
||||
)
|
||||
Reference in New Issue
Block a user