package org.moire.ultrasonic.fragment import android.app.Application import android.content.SharedPreferences import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSettingDao import timber.log.Timber /** * ViewModel to be used in Activities which will handle Server Settings */ class ServerSettingsModel( private val repository: ServerSettingDao, private val activeServerProvider: ActiveServerProvider, application: Application ) : AndroidViewModel(application) { private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) /** * This function will try and convert settings from the Preferences to the Database * @return True, if the migration was executed, False otherwise */ fun migrateFromPreferences(): Boolean { var migrated = true runBlocking { val rowCount = repository.count() if (rowCount == null || rowCount == 0) { // First time load up the server settings from the Preferences val dbServerList = mutableListOf() val context = getApplication().applicationContext val settings = PreferenceManager.getDefaultSharedPreferences(context) val serverNum = settings.getInt(PREFERENCES_KEY_ACTIVE_SERVERS, 0) if (serverNum != 0) { var index = 1 for (x in 1 until serverNum + 1) { val newServerSetting = loadServerSettingFromPreferences(x, index, settings) if (newServerSetting != null) { dbServerList.add(newServerSetting) repository.insert(newServerSetting) index++ Timber.i( "Imported server from Preferences to Database: %s", newServerSetting.name ) } } } else { migrated = false } } } return migrated } /** * Retrieves the list of the configured servers from the database. * This function is asynchronous, uses LiveData to provide the Setting. */ fun getServerList(): LiveData> { // This check should run before returning any result runBlocking { if (areIndexesMissing()) { reindexSettings() } } return repository.loadAllServerSettings() } /** * Retrieves a single Server Setting by its index * This function is asynchronous, uses LiveData to provide the Setting. */ fun getServerSetting(index: Int): LiveData { return repository.getLiveServerSettingByIndex(index) } /** * Moves a Setting up in the Server List by decreasing its index */ fun moveItemUp(index: Int) { if (index <= 1) return viewModelScope.launch { val itemToBeMoved = repository.findByIndex(index) val previousItem = repository.findByIndex(index - 1) if (itemToBeMoved != null && previousItem != null) { itemToBeMoved.index = previousItem.index previousItem.index = index repository.update(itemToBeMoved, previousItem) activeServerProvider.invalidateCache() } } } /** * Moves a Setting down in the Server List by increasing its index */ fun moveItemDown(index: Int) { viewModelScope.launch { if (index < repository.getMaxIndex() ?: 0) { val itemToBeMoved = repository.findByIndex(index) val nextItem = repository.findByIndex(index + 1) if (itemToBeMoved != null && nextItem != null) { itemToBeMoved.index = nextItem.index nextItem.index = index repository.update(itemToBeMoved, nextItem) activeServerProvider.invalidateCache() } } } } /** * Removes a Setting from the database */ fun deleteItem(index: Int) { if (index == 0) return viewModelScope.launch { val itemToBeDeleted = repository.findByIndex(index) if (itemToBeDeleted != null) { repository.delete(itemToBeDeleted) Timber.d("deleteItem deleted index: $index") reindexSettings() activeServerProvider.invalidateCache() } } } /** * Updates a Setting in the database */ fun updateItem(serverSetting: ServerSetting?) { if (serverSetting == null) return appScope.launch { repository.update(serverSetting) activeServerProvider.invalidateCache() Timber.d("updateItem updated server setting: $serverSetting") } } /** * Inserts a new Setting into the database */ fun saveNewItem(serverSetting: ServerSetting?) { if (serverSetting == null) return appScope.launch { serverSetting.index = (repository.count() ?: 0) + 1 serverSetting.id = (repository.getMaxId() ?: 0) + 1 repository.insert(serverSetting) Timber.d("saveNewItem saved server setting: $serverSetting") } } /** * Inserts a new Setting into the database * @return The id of the demo server */ fun addDemoServer(): Int { val demo = DEMO_SERVER_CONFIG.copy() runBlocking { demo.index = (repository.count() ?: 0) + 1 demo.id = (repository.getMaxId() ?: 0) + 1 repository.insert(demo) Timber.d("Added demo server") } return demo.id } /** * Reads up a Server Setting stored in the obsolete Preferences */ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? { val url = settings.getString(PREFERENCES_KEY_SERVER_URL + preferenceId, "") val userName = settings.getString(PREFERENCES_KEY_USERNAME + preferenceId, "") val isMigrated = settings.getBoolean(PREFERENCES_KEY_SERVER_MIGRATED + preferenceId, false) if (url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated) return null setServerMigrated(settings, preferenceId) return ServerSetting( preferenceId, serverId, settings.getString(PREFERENCES_KEY_SERVER_NAME + preferenceId, "")!!, url, null, userName, settings.getString(PREFERENCES_KEY_PASSWORD + preferenceId, "")!!, settings.getBoolean(PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + preferenceId, false), settings.getBoolean( PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + preferenceId, false ), settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + preferenceId, false), settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null), null ) } /** * Checks if there are any missing indexes in the ServerSetting list * For displaying the Server Settings in a ListView, it is mandatory that their indexes * aren't missing. Ideally the indexes are continuous, but some circumstances (e.g. * concurrency or migration errors) may get them out of order. * This would make the List Adapter crash, so it is best to prepare and check the list. */ private suspend fun areIndexesMissing(): Boolean { for (i in 1 until getMaximumIndexToCheck() + 1) { if (repository.findByIndex(i) == null) return true } return false } /** * This function updates all the Server Settings in the DB so their indexing is continuous. */ private suspend fun reindexSettings() { var newIndex = 1 for (i in 1 until getMaximumIndexToCheck() + 1) { val setting = repository.findByIndex(i) if (setting != null) { setting.index = newIndex newIndex++ repository.update(setting) Timber.d("reindexSettings saved $setting") } } } private suspend fun getMaximumIndexToCheck(): Int { val rowsInDatabase = repository.count() ?: 0 val indexesInDatabase = repository.getMaxIndex() ?: 0 if (rowsInDatabase > indexesInDatabase) return rowsInDatabase return indexesInDatabase } private fun setServerMigrated(settings: SharedPreferences, preferenceId: Int) { val editor = settings.edit() editor.putBoolean(PREFERENCES_KEY_SERVER_MIGRATED + preferenceId, true) editor.apply() } companion object { private const val PREFERENCES_KEY_SERVER_MIGRATED = "serverMigrated" // These constants were removed from Constants.java as they are deprecated and only used here private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled" private const val PREFERENCES_KEY_SERVER_NAME = "serverName" private const val PREFERENCES_KEY_SERVER_URL = "serverUrl" private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers" private const val PREFERENCES_KEY_USERNAME = "username" private const val PREFERENCES_KEY_PASSWORD = "password" private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate" private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport" private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId" private val DEMO_SERVER_CONFIG = ServerSetting( id = 0, index = 0, name = UApp.applicationContext().getString(R.string.server_menu_demo), url = "https://demo.ampache.dev", userName = "ultrasonic_demo", password = "W7DumQ3ZUR89Se3", jukeboxByDefault = false, allowSelfSignedCertificate = false, ldapSupport = false, musicFolderId = null, minimumApiVersion = "1.13.0", chatSupport = true, bookmarkSupport = true, shareSupport = true, podcastSupport = true ) } }