diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt index bdbab433..4cb914a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt @@ -13,6 +13,7 @@ import com.google.android.material.textfield.TextInputLayout import java.io.IOException import java.net.MalformedURLException import java.net.URL +import org.koin.android.ext.android.inject import org.koin.android.viewmodel.ext.android.viewModel import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.R @@ -20,7 +21,9 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.SubsonicRESTException import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog @@ -41,6 +44,8 @@ internal class EditServerActivity : AppCompatActivity() { } private val serverSettingsModel: ServerSettingsModel by viewModel() + private val activeServerProvider: ActiveServerProvider by inject() + private var currentServerSetting: ServerSetting? = null private var serverNameEditText: TextInputLayout? = null @@ -90,6 +95,13 @@ internal class EditServerActivity : AppCompatActivity() { if (currentServerSetting != null) { if (getFields()) { serverSettingsModel.updateItem(currentServerSetting) + // Apply modifications if the current server was modified + if ( + activeServerProvider.getActiveServer().id == + currentServerSetting!!.id + ) { + MusicServiceFactory.resetMusicService() + } finish() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt index f0baf6ca..fcddc267 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerRowAdapter.kt @@ -53,7 +53,7 @@ internal class ServerRowAdapter( } override fun getCount(): Int { - return if (manageMode) data.size - 1 else data.size + return if (manageMode) data.size else data.size + 1 } override fun getItem(position: Int): Any { @@ -81,8 +81,15 @@ internal class ServerRowAdapter( val image = vi?.findViewById(R.id.server_image) val serverMenu = vi?.findViewById(R.id.server_menu) - text?.text = data.single { setting -> setting.index == index }.name - description?.text = data.single { setting -> setting.index == index }.url + if (index == 0) { + text?.text = context.getString(R.string.main_offline) + description?.text = "" + } else { + val setting = data.singleOrNull { t -> t.index == index } + text?.text = setting?.name ?: "" + description?.text = setting?.url ?: "" + if (setting == null) serverMenu?.visibility = View.INVISIBLE + } // Provide icons for the row if (index == 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt index 49cc94a5..0cc48c2a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSelectorActivity.kt @@ -144,8 +144,9 @@ internal class ServerSelectorActivity : AppCompatActivity() { if (activeServerProvider.getActiveServer().index != index) { service.clearIncomplete() activeServerProvider.setActiveServerByIndex(index) + service.isJukeboxEnabled = + activeServerProvider.getActiveServer().jukeboxByDefault } - service.isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault } } Log.i(TAG, "Active server was set to: $index") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt index 78df5476..ff1d8e8b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ServerSettingsModel.kt @@ -5,12 +5,10 @@ import android.content.SharedPreferences import android.preference.PreferenceManager import android.util.Log import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSettingDao @@ -23,10 +21,10 @@ class ServerSettingsModel( private val activeServerProvider: ActiveServerProvider, private val context: Context ) : ViewModel() { - private var serverList: MutableLiveData> = MutableLiveData() companion object { private val TAG = ServerSettingsModel::class.simpleName + 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" @@ -84,13 +82,13 @@ class ServerSettingsModel( * This function is asynchronous, uses LiveData to provide the Setting. */ fun getServerList(): LiveData> { - viewModelScope.launch { - val dbServerList = repository.loadAllServerSettings().toMutableList() - - dbServerList.add(0, ServerSetting(context.getString(R.string.main_offline), "")) - serverList.value = dbServerList + // This check should run before returning any result + runBlocking { + if (areIndexesMissing()) { + reindexSettings() + } } - return serverList + return repository.loadAllServerSettings() } /** @@ -98,55 +96,47 @@ class ServerSettingsModel( * This function is asynchronous, uses LiveData to provide the Setting. */ fun getServerSetting(index: Int): LiveData { - val result = MutableLiveData() - viewModelScope.launch { - val dbServer = repository.findByIndex(index) - result.value = dbServer - Log.d(TAG, "getServerSetting($index) returning $dbServer") - } - return result + return repository.getLiveServerSettingByIndex(index) } /** * Moves a Setting up in the Server List by decreasing its index */ fun moveItemUp(index: Int) { - if (index == 1) return - - val itemToBeMoved = serverList.value?.single { setting -> setting.index == index } - val previousItem = serverList.value?.single { setting -> setting.index == index - 1 } - - itemToBeMoved?.index = previousItem!!.index - previousItem.index = index + if (index <= 1) return viewModelScope.launch { - repository.update(itemToBeMoved!!, previousItem) - } + val itemToBeMoved = repository.findByIndex(index) + val previousItem = repository.findByIndex(index - 1) - activeServerProvider.invalidateCache() - // Notify the observers of the changed values - serverList.value = serverList.value + 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) { - if (index == (serverList.value!!.size - 1)) return - - val itemToBeMoved = serverList.value?.single { setting -> setting.index == index } - val nextItem = serverList.value?.single { setting -> setting.index == index + 1 } - - itemToBeMoved?.index = nextItem!!.index - nextItem.index = index - viewModelScope.launch { - repository.update(itemToBeMoved!!, nextItem) - } + if (index < repository.getMaxIndex() ?: 0) { + val itemToBeMoved = repository.findByIndex(index) + val nextItem = repository.findByIndex(index + 1) - activeServerProvider.invalidateCache() - // Notify the observers of the changed values - serverList.value = serverList.value + if (itemToBeMoved != null && nextItem != null) { + itemToBeMoved.index = nextItem.index + nextItem.index = index + + repository.update(itemToBeMoved, nextItem) + activeServerProvider.invalidateCache() + } + } + } } /** @@ -155,24 +145,15 @@ class ServerSettingsModel( fun deleteItem(index: Int) { if (index == 0) return - val newList = serverList.value!!.toMutableList() - val itemToBeDeleted = newList.single { setting -> setting.index == index } - newList.remove(itemToBeDeleted) - - for (x in index + 1 until newList.size + 1) { - newList.single { setting -> setting.index == x }.index-- - } - viewModelScope.launch { - repository.delete(itemToBeDeleted) - for (x in index until newList.size) { - repository.update(newList.single { setting -> setting.index == x }) + val itemToBeDeleted = repository.findByIndex(index) + if (itemToBeDeleted != null) { + repository.delete(itemToBeDeleted) + Log.d(TAG, "deleteItem deleted index: $index") + reindexSettings() + activeServerProvider.invalidateCache() } } - - activeServerProvider.invalidateCache() - serverList.value = newList - Log.d(TAG, "deleteItem deleted index: $index") } /** @@ -196,7 +177,7 @@ class ServerSettingsModel( viewModelScope.launch { serverSetting.index = (repository.count() ?: 0) + 1 - serverSetting.id = serverSetting.index + serverSetting.id = (repository.getMaxId() ?: 0) + 1 repository.insert(serverSetting) Log.d(TAG, "saveNewItem saved server setting: $serverSetting") } @@ -212,8 +193,10 @@ class ServerSettingsModel( ): 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()) return null + if (url.isNullOrEmpty() || userName.isNullOrEmpty() || isMigrated) return null + setServerMigrated(settings, preferenceId) return ServerSetting( preferenceId, @@ -231,4 +214,47 @@ class ServerSettingsModel( settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, 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 + * are'nt 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) + Log.d(TAG, "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() + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index feb1fa19..b3f044e6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -75,7 +75,7 @@ class ActiveServerProvider( } GlobalScope.launch(Dispatchers.IO) { - val serverId = repository.findByIndex(index)!!.id + val serverId = repository.findByIndex(index)?.id ?: 0 setActiveServerId(context, serverId) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt index 65c365dd..c715892e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSettingDao.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.data +import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert @@ -35,7 +36,7 @@ interface ServerSettingDao { * Loads all Server Settings from the table */ @Query("SELECT * FROM serverSetting") - suspend fun loadAllServerSettings(): Array + fun loadAllServerSettings(): LiveData> /** * Finds a Server Setting by its unique Id @@ -49,9 +50,28 @@ interface ServerSettingDao { @Query("SELECT * FROM serverSetting WHERE [index] = :index") suspend fun findByIndex(index: Int): ServerSetting? + /** + * Finds a Server Setting by its Index in the Select List + * @return LiveData of the ServerSetting + */ + @Query("SELECT * FROM serverSetting WHERE [index] = :index") + fun getLiveServerSettingByIndex(index: Int): LiveData + /** * Retrieves the count of rows in the table */ @Query("SELECT COUNT(*) FROM serverSetting") suspend fun count(): Int? + + /** + * Retrieves the greatest value of the Id column in the table + */ + @Query("SELECT MAX([id]) FROM serverSetting") + suspend fun getMaxId(): Int? + + /** + * Retrieves the greatest value of the Index column in the table + */ + @Query("SELECT MAX([index]) FROM serverSetting") + suspend fun getMaxIndex(): Int? }