From d4c0f62b1d6cf9aba3434f5741b19ebff2cfe31c Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 21 Jun 2021 19:11:30 +0200 Subject: [PATCH 01/12] Don't keep a reference to context here, it's a leak and not used anyway. --- .../src/main/java/org/moire/ultrasonic/view/UpdateView.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java index d264994d..cbea03bb 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/UpdateView.java @@ -3,7 +3,6 @@ package org.moire.ultrasonic.view; import android.content.Context; import android.os.Handler; import android.os.Looper; -import timber.log.Timber; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.LinearLayout; @@ -14,6 +13,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.WeakHashMap; +import timber.log.Timber; + /** * A View that is periodically refreshed * @deprecated @@ -26,12 +27,10 @@ public class UpdateView extends LinearLayout private static Handler backgroundHandler; private static Handler uiHandler; private static Runnable updateRunnable; - private static Context context; public UpdateView(Context context) { super(context); - UpdateView.context = context; setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); INSTANCES.put(this, null); From d916e937f9a62560d42b4327c6034eb2c62a2d71 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 21 Jun 2021 19:12:11 +0200 Subject: [PATCH 02/12] Move a resource from a versioned folder (v14 is the default now) --- ultrasonic/lint-baseline.xml | 18 ------------------ .../appwidget_dark_bg_trans.9.png | Bin 2 files changed, 18 deletions(-) rename ultrasonic/src/main/res/{drawable-xhdpi-v14 => drawable-hdpi}/appwidget_dark_bg_trans.9.png (100%) diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 037c3e49..e9795cea 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -451,13 +451,6 @@ column="9"/> - - - - - - - - Date: Wed, 23 Jun 2021 17:30:16 +0200 Subject: [PATCH 03/12] Increase memory cache size --- detekt-config.yml | 1 + .../moire/ultrasonic/imageloader/ImageLoader.kt | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/detekt-config.yml b/detekt-config.yml index 7fff31c3..eb76d7f7 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -45,6 +45,7 @@ complexity: thresholdInFiles: 20 thresholdInClasses: 20 thresholdInInterfaces: 20 + thresholdInObjects: 30 LabeledExpression: active: false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 9d40ad28..73862c20 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -1,9 +1,13 @@ package org.moire.ultrasonic.imageloader +import android.app.ActivityManager import android.content.Context +import android.content.pm.ApplicationInfo import android.text.TextUtils import android.view.View import android.widget.ImageView +import androidx.core.content.ContextCompat +import com.squareup.picasso.LruCache import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator import java.io.File @@ -35,6 +39,7 @@ class ImageLoader( private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) .addRequestHandler(AvatarRequestHandler(apiClient)) + .memoryCache(LruCache(calculateMemoryCacheSize(context))) .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } @@ -179,6 +184,18 @@ class ImageLoader( return requested } } + + private fun calculateMemoryCacheSize(context: Context): Int { + val am = ContextCompat.getSystemService( + context, + ActivityManager::class.java + ) + val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0 + val memoryClass = if (largeHeap) am!!.largeMemoryClass else am!!.memoryClass + // Target 25% of the available heap. + @Suppress("MagicNumber") + return (1024L * 1024L * memoryClass / 4).toInt() + } } /** From e3e8d36f5c099afec32ac950b67b838ccf959888 Mon Sep 17 00:00:00 2001 From: tzugen Date: Fri, 25 Jun 2021 17:47:11 +0200 Subject: [PATCH 04/12] Save the correct field to the server preferences --- .../kotlin/org/moire/ultrasonic/service/RESTMusicService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 6e0e44e1..0d6730d8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -708,7 +708,7 @@ open class RESTMusicService( // By registering a callback we ensure this info is saved in the database as well subsonicAPIClient.onProtocolChange = { Timber.i("Server minimum API version set to %s", it) - activeServerProvider.setMinimumApiVersion(it.toString()) + activeServerProvider.setMinimumApiVersion(it.restApiVersion) } } From 68acf3789c40923d3702ec24f53138e62306f6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Garc=C3=ADa=20Amor?= Date: Tue, 29 Jun 2021 11:28:26 +0200 Subject: [PATCH 05/12] Update translations --- ultrasonic/src/main/res/values-es/strings.xml | 11 +++++--- .../src/main/res/values-pt-rBR/strings.xml | 27 +++++++++++++++---- .../src/main/res/values-zh-rTW/strings.xml | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 4ba33dc7..1bef465e 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -1,5 +1,5 @@ - + Cargando… Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde. @@ -111,7 +111,9 @@ Me gusta Canciones Vídeos - ¡Saludos! + Para utilizar Ultrasonic con tu música necesitas un servidor propio. \n\n➤ En caso de que quieras probar la aplicación primero, se puede añadir ahora un servidor de demostración. \n\n➤ En caso contrario, puedes configurar tu servidor personal en la configuración. + ¡Bienvenido a Ultrasonic! + Llévame a la configuración Acerca de Común Eliminada lista de reproducción %s @@ -319,7 +321,7 @@ Usar el método de etiquetas ID3 en lugar del método basado en el sistema de ficheros Mostrar la imagen del artista en la lista de artistas Muestra la imagen del artista en la lista de artistas si está disponible - Vídeo + Vídeo Refresco de la vista .5 segundos 1 segundo @@ -339,7 +341,7 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Toca para seleccionar música Tarjeta SD no disponible @@ -434,6 +436,7 @@ Autenticación Configuración avanzada Una o más funciones se han deshabilitado porque el servidor no las admite.\nPuedes ejecutar esta prueba nuevamente en cualquier momento. + Servidor de demostración 1 canción diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 8e418d39..a7f15c9d 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -1,5 +1,5 @@ - + Carregando… Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde. @@ -15,9 +15,16 @@ Chat Menu Principal Tocando Agora + Tocar + Pausar + Repetir + Misturar + Parar + Próxima + Anterior Podcasts Nenhum canal de podcasts registrado - Podcast + Podcasts Playlists Pesquisa Enviar uma mensagem @@ -32,8 +39,11 @@ Nome OK Fixar + Pausar + Tocar Tocar por Último Tocar na Próxima + Tocar a Anterior Tocar Agora Tocar Aleatoriamente Público @@ -101,7 +111,9 @@ Favoritas Músicas Vídeos - Bem-vindo! + Para usar o Ultrasonic com sua própria música, você precisará de um servidor próprio.\n\n➤ Caso queira experimentar o aplicativo primeiro, você pode adicionar um servidor de demonstração agora.\n\n➤ Caso contrário, configure seu servidor nas configurações. + Bem-vindo ao Ultrasonic! + Vá para as configurações Sobre Comum Playlist excluída %s @@ -250,6 +262,8 @@ O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo. Manter a tela ligada enquanto baixando aumenta a velocidade de download. Manter a Tela Ligada + Lembrar de configurar o usuário e senha no(s) serviço(s) Scrobble do servidor + Scrobble minhas músicas 1 10 100 @@ -307,7 +321,7 @@ Usar as etiquetas ID3 ao invés do sistema de arquivos Mostrar Foto do Artista na Lista Mostrar a imagem do artista na lista de artistas, se disponível - Vídeo + Vídeo Atualização da Tela .5 segundos 1 segundo @@ -327,7 +341,7 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Toque para selecionar a música Cartão SD indisponível @@ -421,6 +435,9 @@ Para baixo Autenticação Configurações avançadas + Um ou mais recursos foram desativados porque o servidor não os suporta.\nVocê pode rodar este teste novamente a qualquer momento. + Servidor Demonstração + %d música %d músicas diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index e1640036..2c98f249 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,5 @@ - + 載入中… 書籤 From bc8a78e772d7e54df630eec27554fa3458ab330b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Garc=C3=ADa=20Amor?= Date: Tue, 29 Jun 2021 11:51:24 +0200 Subject: [PATCH 06/12] Bump version to 2.22.0 --- fastlane/metadata/android/en-US/changelogs/94.txt | 12 ++++++++++++ fastlane/metadata/android/es-ES/changelogs/94.txt | 12 ++++++++++++ ultrasonic/build.gradle | 4 ++-- 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/94.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/94.txt diff --git a/fastlane/metadata/android/en-US/changelogs/94.txt b/fastlane/metadata/android/en-US/changelogs/94.txt new file mode 100644 index 00000000..846edc22 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/94.txt @@ -0,0 +1,12 @@ +Bug fixes +- #368: Empty id in getCoverArt. +- #528: Saving playlists does not work. + +Enhancements +- #514: Fix bugs in new image loader and make it default. +- #517: Cleaner separation of API result handling. +- #519: Better experience for new users. +- #520: Remove flash. +- #525: Properly generate the Video stream url, without actually making a request. +- #530: Use DiffUtil for better performance when refreshing the data. +- #532: Larger image cache. diff --git a/fastlane/metadata/android/es-ES/changelogs/94.txt b/fastlane/metadata/android/es-ES/changelogs/94.txt new file mode 100644 index 00000000..245c8fa8 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/94.txt @@ -0,0 +1,12 @@ +Corrección de errores +- # 368: ID vacío en getCoverArt. +- # 528: Guardar listas de reproducción no funciona. + +Mejoras +- # 514: Corregir errores en el nuevo cargador de imágenes y usarlo por defecto. +- # 517: Separación más limpia del manejo de resultados API. +- # 519: Mejor experiencia para nuevos usuarios. +- # 520: Quitar flash. +- # 525: Generar correctamente la URL de transmisión de video, sin realizar una nueva solicitud. +- # 530: Usar DiffUtil para un mejor rendimiento al actualizar los datos. +- # 532: Caché de imágenes más grande. diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 997ea5fc..da0c4ec2 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 93 - versionName "2.21.0" + versionCode 94 + versionName "2.22.0" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk From f15891cbddcf2425894ce76e0703f5795c4acd42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jun 2021 09:52:14 +0000 Subject: [PATCH 07/12] Bump versions.mockito from 3.11.0 to 3.11.2 Bumps `versions.mockito` from 3.11.0 to 3.11.2. Updates `mockito-core` from 3.11.0 to 3.11.2 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.11.0...v3.11.2) Updates `mockito-inline` from 3.11.0 to 3.11.2 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.11.0...v3.11.2) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index af91ee1f..bca58e6e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -35,7 +35,7 @@ ext.versions = [ junit4 : "4.13.2", junit5 : "5.7.1", - mockito : "3.11.0", + mockito : "3.11.2", mockitoKotlin : "3.2.0", kluent : "1.64", apacheCodecs : "1.15", From c99c4478f22968e35092dae8e8b70eaa45a6b69d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 22 May 2021 11:18:29 +0200 Subject: [PATCH 08/12] Remove file storage code from RESTAPI class --- core/cache/build.gradle | 12 --- .../org/moire/ultrasonic/cache/Directories.kt | 14 ---- .../ultrasonic/cache/PermanentFileStorage.kt | 75 ----------------- .../cache/serializers/ArtistSerializer.kt | 65 --------------- .../cache/serializers/IndexesSerializer.kt | 51 ------------ .../serializers/MusicFolderSerializer.kt | 51 ------------ .../moire/ultrasonic/cache/BaseStorageTest.kt | 42 ---------- .../cache/PermanentFileStorageTest.kt | 80 ------------------- .../cache/serializers/ArtistSerializerTest.kt | 57 ------------- .../serializers/IndexesSerializerTest.kt | 38 --------- .../serializers/MusicFolderSerializerTest.kt | 57 ------------- dependencies.gradle | 2 - settings.gradle | 1 - ultrasonic/build.gradle | 1 - .../kotlin/org/moire/ultrasonic/app/UApp.kt | 2 - .../ultrasonic/cache/AndroidDirectories.kt | 17 ---- .../moire/ultrasonic/di/DirectoriesModule.kt | 13 --- .../moire/ultrasonic/di/MusicServiceModule.kt | 6 -- 18 files changed, 584 deletions(-) delete mode 100644 core/cache/build.gradle delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt diff --git a/core/cache/build.gradle b/core/cache/build.gradle deleted file mode 100644 index 4f9d6446..00000000 --- a/core/cache/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply from: bootstrap.kotlinModule - -dependencies { - api project(':core:domain') - api other.twitterSerial - - testImplementation testing.kotlinJunit - testImplementation testing.mockito - testImplementation testing.mockitoInline - testImplementation testing.mockitoKotlin - testImplementation testing.kluent -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt deleted file mode 100644 index ad0aa6de..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.moire.ultrasonic.cache - -import java.io.File - -/** - * Provides access to generic directories: - * - for temporary caches - * - for permanent data storage - */ -interface Directories { - fun getInternalCacheDir(): File - fun getInternalDataDir(): File - fun getExternalCacheDir(): File? -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt deleted file mode 100644 index 767e23eb..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.moire.ultrasonic.cache - -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.serializer.Serializer -import com.twitter.serial.stream.Serial -import com.twitter.serial.stream.bytebuffer.ByteBufferSerial -import java.io.File - -typealias DomainEntitySerializer = Serializer - -internal const val STORAGE_DIR_NAME = "persistent_storage" - -/** - * Provides access to permanent file based storage. - * - * [serverId] is currently active server. Should be unique per server so stored data will not - * interfere with other server data. - * - * Look at [org.moire.ultrasonic.cache.serializers] package for available [DomainEntitySerializer]s. - */ -class PermanentFileStorage( - private val directories: Directories, - private val serverId: String, - private val debug: Boolean = false -) { - private val serializationContext = object : SerializationContext { - override fun isDebug(): Boolean = debug - override fun isRelease(): Boolean = !debug - } - - private val serializer: Serial = ByteBufferSerial(serializationContext) - - /** - * Stores given [objectToStore] using [name] as a key and [objectSerializer] as serializer. - */ - fun store( - name: String, - objectToStore: T, - objectSerializer: DomainEntitySerializer - ) { - val storeFile = getFile(name) - if (!storeFile.exists()) storeFile.createNewFile() - storeFile.writeBytes(serializer.toByteArray(objectToStore, objectSerializer)) - } - - /** - * Loads object with [name] key using [objectDeserializer] deserializer. - */ - fun load( - name: String, - objectDeserializer: DomainEntitySerializer - ): T? { - val storeFile = getFile(name) - if (!storeFile.exists()) return null - - return serializer.fromByteArray(storeFile.readBytes(), objectDeserializer) - } - - /** - * Clear all files in storage. - */ - fun clearAll() { - val storageDir = getStorageDir() - storageDir.listFiles().forEach { it.deleteRecursively() } - } - - private fun getFile(name: String) = File(getStorageDir(), "$name.ser") - - private fun getStorageDir(): File { - val mainDir = File(directories.getInternalDataDir(), STORAGE_DIR_NAME) - val serverDir = File(mainDir, serverId) - if (!serverDir.exists()) serverDir.mkdirs() - return serverDir - } -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt deleted file mode 100644 index 1fa03bd0..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt +++ /dev/null @@ -1,65 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.CollectionSerializers -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerDefs -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.Artist - -private const val SERIALIZER_VERSION = 1 - -private val artistSerializer get() = object : ObjectSerializer(SERIALIZER_VERSION) { - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: Artist - ) { - output.writeString(item.id) - .writeString(item.name) - .writeString(item.index) - .writeString(item.coverArt) - .apply { - val albumCount = item.albumCount - if (albumCount != null) writeLong(albumCount) else writeNull() - } - .writeInt(item.closeness) - } - - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): Artist? { - if (versionNumber != SERIALIZER_VERSION) return null - - val id = input.readString() - val name = input.readString() - val index = input.readString() - val coverArt = input.readString() - val albumCount = if (input.peekType() == SerializerDefs.TYPE_NULL) { - input.readNull() - null - } else { - input.readLong() - } - val closeness = input.readInt() - return Artist(id, name, index, coverArt, albumCount, closeness) - } -} - -/** - * Serializer/deserializer for [Artist] domain entity. - */ -fun getArtistsSerializer(): DomainEntitySerializer = artistSerializer - -private val artistListSerializer = CollectionSerializers.getListSerializer(artistSerializer) - -/** - * Serializer/deserializer for list of [Artist] domain entities. - */ -fun getArtistListSerializer(): DomainEntitySerializer> = artistListSerializer diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt deleted file mode 100644 index 9683bd25..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt +++ /dev/null @@ -1,51 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.Indexes - -private const val SERIALIZATION_VERSION = 1 - -private val indexesSerializer get() = object : ObjectSerializer(SERIALIZATION_VERSION) { - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: Indexes - ) { - val artistListSerializer = getArtistListSerializer() - output.writeLong(item.lastModified) - .writeString(item.ignoredArticles) - .writeObject>(context, item.shortcuts, artistListSerializer) - .writeObject>(context, item.artists, artistListSerializer) - } - - @Suppress("ReturnCount") - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): Indexes? { - if (versionNumber != SERIALIZATION_VERSION) return null - - val artistListDeserializer = getArtistListSerializer() - val lastModified = input.readLong() - val ignoredArticles = input.readString() ?: return null - val shortcutsList = input.readObject(context, artistListDeserializer) ?: return null - val artistsList = input.readObject(context, artistListDeserializer) ?: return null - return Indexes( - lastModified, ignoredArticles, shortcutsList.toMutableList(), - artistsList.toMutableList() - ) - } -} - -/** - * Get serializer/deserializer for [Indexes] entity. - */ -fun getIndexesSerializer(): DomainEntitySerializer = indexesSerializer diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt deleted file mode 100644 index 0af30335..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt +++ /dev/null @@ -1,51 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.CollectionSerializers -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.MusicFolder - -private const val SERIALIZATION_VERSION = 1 - -private val musicFolderSerializer = object : ObjectSerializer(SERIALIZATION_VERSION) { - - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: MusicFolder - ) { - output.writeString(item.id).writeString(item.name) - } - - @Suppress("ReturnCount") - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): MusicFolder? { - if (versionNumber != SERIALIZATION_VERSION) return null - - val id = input.readString() ?: return null - val name = input.readString() ?: return null - return MusicFolder(id, name) - } -} - -/** - * Serializer/deserializer for [MusicFolder] domain entity. - */ -fun getMusicFolderSerializer(): DomainEntitySerializer = musicFolderSerializer - -private val musicFolderListSerializer = - CollectionSerializers.getListSerializer(musicFolderSerializer) - -/** - * Serializer/deserializer for [List] of [MusicFolder] items. - */ -fun getMusicFolderListSerializer(): DomainEntitySerializer> = - musicFolderListSerializer diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt deleted file mode 100644 index 36e2f74c..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.moire.ultrasonic.cache - -import com.twitter.serial.util.SerializationUtils -import java.io.File -import org.amshove.kluent.`it returns` -import org.junit.Before -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import org.mockito.kotlin.mock - -internal const val INTERNAL_DATA_FOLDER = "data" -internal const val INTERNAL_CACHE_FOLDER = "cache" -internal const val EXTERNAL_CACHE_FOLDER = "external_cache" - -/** - * Base test class that inits the storage - */ -abstract class BaseStorageTest { - @get:Rule val tempFileRule = TemporaryFolder() - - protected lateinit var mockDirectories: Directories - protected lateinit var storage: PermanentFileStorage - - open val serverId: String = "" - - @Before - fun setUp() { - mockDirectories = mock { - on { getInternalDataDir() } `it returns` tempFileRule.newFolder(INTERNAL_DATA_FOLDER) - on { getInternalCacheDir() } `it returns` tempFileRule.newFolder(INTERNAL_CACHE_FOLDER) - on { getExternalCacheDir() } `it returns` tempFileRule.newFolder(EXTERNAL_CACHE_FOLDER) - } - storage = PermanentFileStorage(mockDirectories, serverId, true) - } - - protected val storageDir get() = File(mockDirectories.getInternalDataDir(), STORAGE_DIR_NAME) - - protected fun validateSerializedData(index: Int = 0) { - val serializedFileBytes = storageDir.listFiles()[index].readBytes() - SerializationUtils.validateSerializedData(serializedFileBytes) - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt deleted file mode 100644 index 1deb5fa0..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.moire.ultrasonic.cache - -import java.io.File -import org.amshove.kluent.`should be equal to` -import org.amshove.kluent.`should contain` -import org.junit.Test -import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer -import org.moire.ultrasonic.domain.MusicFolder - -/** - * Integration test for [PermanentFileStorage]. - */ -class PermanentFileStorageTest : BaseStorageTest() { - override val serverId: String - get() = "some-server-id" - - @Test - fun `Should create storage dir if it is not exist`() { - val item = MusicFolder("1", "2") - storage.store("test", item, getMusicFolderSerializer()) - - storageDir.exists() `should be equal to` true - getServerStorageDir().exists() `should be equal to` true - } - - @Test - fun `Should serialize to file`() { - val item = MusicFolder("1", "23") - val name = "some-name" - - storage.store(name, item, getMusicFolderSerializer()) - - val storageFiles = getServerStorageDir().listFiles() - storageFiles.size `should be equal to` 1 - storageFiles[0].name `should contain` name - } - - @Test - fun `Should deserialize stored object`() { - val item = MusicFolder("some", "nice") - val name = "some-name" - storage.store(name, item, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should overwrite existing stored object`() { - val name = "some-nice-name" - val item1 = MusicFolder("1", "1") - val item2 = MusicFolder("2", "2") - storage.store(name, item1, getMusicFolderSerializer()) - storage.store(name, item2, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item2 - } - - @Test - fun `Should clear all files when clearAll is called`() { - storage.store("name1", MusicFolder("1", "1"), getMusicFolderSerializer()) - storage.store("name2", MusicFolder("2", "2"), getMusicFolderSerializer()) - - storage.clearAll() - - getServerStorageDir().listFiles().size `should be equal to` 0 - } - - @Test - fun `Should return null if serialized file not available`() { - val loadedItem = storage.load("some-name", getMusicFolderSerializer()) - - loadedItem `should be equal to` null - } - - private fun getServerStorageDir() = File(storageDir, serverId) -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt deleted file mode 100644 index 967bbdb5..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.Artist - -/** - * [Artist] serializers test. - */ -class ArtistSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize Artist object`() { - val item = Artist("id", "name", "index", "coverArt", 1, 0) - - storage.store("some-name", item, getArtistsSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize Artist object`() { - val itemName = "some-name" - val item = Artist("id", "name", "index", "coverArt", null, 0) - storage.store(itemName, item, getArtistsSerializer()) - - val loadedItem = storage.load(itemName, getArtistsSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should correctly serialize list of Artists`() { - val itemsList = listOf( - Artist(id = "1"), - Artist(id = "2", name = "some") - ) - - storage.store("some-name", itemsList, getArtistListSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize list of Artists`() { - val name = "some-name" - val itemsList = listOf( - Artist(id = "1"), - Artist(id = "2", name = "some") - ) - storage.store(name, itemsList, getArtistListSerializer()) - - val loadedItems = storage.load(name, getArtistListSerializer()) - - loadedItems `should be equal to` itemsList - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt deleted file mode 100644 index 5a4cc1a1..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.Indexes - -/** - * Test [Indexes] domain entity serializer. - */ -class IndexesSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize Indexes object`() { - val item = Indexes( - 220L, "", mutableListOf(Artist("12")), - mutableListOf(Artist("233", "some")) - ) - - storage.store("some-name", item, getIndexesSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize Indexes object`() { - val name = "some-name" - val item = Indexes( - 220L, "", mutableListOf(Artist("12")), - mutableListOf(Artist("233", "some")) - ) - storage.store(name, item, getIndexesSerializer()) - - val loadedItem = storage.load(name, getIndexesSerializer()) - - loadedItem `should be equal to` item - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt deleted file mode 100644 index 1935dac8..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.MusicFolder - -/** - * [MusicFolder] serializers test. - */ -class MusicFolderSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize MusicFolder object`() { - val item = MusicFolder("Music", "Folder") - - storage.store("some-name", item, getMusicFolderSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize MusicFolder object`() { - val name = "name" - val item = MusicFolder("some", "none") - storage.store(name, item, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should correctly serialize list of MusicFolders objects`() { - val itemsList = listOf( - MusicFolder("1", "1"), - MusicFolder("2", "2") - ) - - storage.store("some-name", itemsList, getMusicFolderListSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize list of MusicFolder objects`() { - val name = "some-name" - val itemsList = listOf( - MusicFolder("1", "1"), - MusicFolder("2", "2") - ) - storage.store(name, itemsList, getMusicFolderListSerializer()) - - val loadedItem = storage.load(name, getMusicFolderListSerializer()) - - loadedItem `should be equal to` itemsList - } -} diff --git a/dependencies.gradle b/dependencies.gradle index bca58e6e..d8a629bb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -28,7 +28,6 @@ ext.versions = [ retrofit : "2.6.4", jackson : "2.9.5", okhttp : "3.12.13", - twitterSerial : "0.1.6", koin : "3.0.2", picasso : "2.71828", sortListView : "1.0.1", @@ -82,7 +81,6 @@ ext.other = [ jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit", jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson", okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp", - twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", koinCore : "io.insert-koin:koin-core:$versions.koin", koinAndroid : "io.insert-koin:koin-android:$versions.koin", koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin", diff --git a/settings.gradle b/settings.gradle index a4a9a1e1..8fdc7dca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,3 @@ include ':core:domain' include ':core:subsonic-api' -include ':core:cache' include ':ultrasonic' diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index da0c4ec2..fdd0aaa9 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -70,7 +70,6 @@ tasks.withType(Test) { dependencies { implementation project(':core:domain') implementation project(':core:subsonic-api') - implementation project(':core:cache') api(other.picasso) { exclude group: "com.android.support" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 78ddfe89..191cc5b2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -9,7 +9,6 @@ import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.di.appPermanentStorage import org.moire.ultrasonic.di.applicationModule import org.moire.ultrasonic.di.baseNetworkModule -import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.featureFlagsModule import org.moire.ultrasonic.di.mediaPlayerModule import org.moire.ultrasonic.di.musicServiceModule @@ -46,7 +45,6 @@ class UApp : MultiDexApplication() { // declare modules to use modules( applicationModule, - directoriesModule, appPermanentStorage, baseNetworkModule, featureFlagsModule, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt deleted file mode 100644 index d8a815a7..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.moire.ultrasonic.cache - -import android.content.Context -import java.io.File - -/** - * Provides specific to Android implementation of [Directories]. - */ -class AndroidDirectories( - private val context: Context -) : Directories { - override fun getInternalCacheDir(): File = context.cacheDir - - override fun getInternalDataDir(): File = context.filesDir - - override fun getExternalCacheDir(): File? = context.externalCacheDir -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt deleted file mode 100644 index 5e9b415e..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.moire.ultrasonic.di - -import org.koin.dsl.bind -import org.koin.dsl.module -import org.moire.ultrasonic.cache.AndroidDirectories -import org.moire.ultrasonic.cache.Directories - -/** - * This Koin module contains the registration for Directories - */ -val directoriesModule = module { - single { AndroidDirectories(get()) } bind Directories::class -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index c6d18ee5..bb4c3575 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -10,7 +10,6 @@ import org.moire.ultrasonic.BuildConfig 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.cache.PermanentFileStorage import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.log.TimberOkHttpLogger @@ -43,11 +42,6 @@ val musicServiceModule = module { return@single abs("$serverUrl$serverInstance".hashCode()).toString() } - single { - val serverId = get(named("ServerID")) - return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG) - } - single { val server = get().getActiveServer() From fa94cd24da681eb62f5d496c87cdff11e1fba37b Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 20 Jun 2021 16:45:34 +0200 Subject: [PATCH 09/12] Export schema --- ultrasonic/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index fdd0aaa9..05fc8468 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -61,6 +61,13 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kapt { + arguments { + arg("room.schemaLocation", "$buildDir/schemas".toString()) + } + } + } tasks.withType(Test) { From dbdb59bbff436ce43dc6e386e654e523d4ba3709 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 20 Jun 2021 16:31:08 +0200 Subject: [PATCH 10/12] Add a Room database for Artists, Indexes and MusicFolders. * There is one database for each Server * Index items are saved with a "musicFolderId" prop, which makes it possible to filter the database by musicFolder without necessarily having to query the server for it. * Databases for new Servers are created on the fly * If the user removes a server, the respective database is deleted. --- core/domain/build.gradle | 9 +- core/domain/src/main/AndroidManifest.xml | 4 + .../org/moire/ultrasonic/domain/Artist.kt | 19 ++- .../moire/ultrasonic/domain/ArtistOrIndex.kt | 18 +++ .../moire/ultrasonic/domain/GenericEntry.kt | 10 +- .../org/moire/ultrasonic/domain/Genre.kt | 9 +- .../org/moire/ultrasonic/domain/Index.kt | 15 ++ .../org/moire/ultrasonic/domain/Indexes.kt | 14 -- .../moire/ultrasonic/domain/MusicDirectory.kt | 5 +- .../moire/ultrasonic/domain/MusicFolder.kt | 6 +- .../android-module-bootstrap.gradle | 4 +- gradle_scripts/kotlin-module-bootstrap.gradle | 5 +- .../ultrasonic/data/ActiveServerProvider.kt | 34 ++++ .../org/moire/ultrasonic/data/AppDatabase.kt | 3 +- .../org/moire/ultrasonic/data/ArtistsDao.kt | 31 ++++ .../org/moire/ultrasonic/data/BasicDaos.kt | 146 ++++++++++++++++++ .../org/moire/ultrasonic/data/MetaDatabase.kt | 19 +++ .../moire/ultrasonic/di/MusicServiceModule.kt | 2 +- .../ultrasonic/domain/APIArtistConverter.kt | 8 + .../ultrasonic/domain/APIIndexesConverter.kt | 36 ++++- .../ultrasonic/fragment/ArtistListFragment.kt | 6 +- .../ultrasonic/fragment/ArtistListModel.kt | 22 +-- .../ultrasonic/fragment/ArtistRowAdapter.kt | 12 +- .../ultrasonic/fragment/GenericListModel.kt | 2 +- .../fragment/ServerSelectorFragment.kt | 9 +- .../ultrasonic/service/CachedMusicService.kt | 92 +++++++---- .../moire/ultrasonic/service/MusicService.kt | 7 +- .../ultrasonic/service/OfflineMusicService.kt | 25 ++- .../ultrasonic/service/RESTMusicService.kt | 86 +++-------- .../org/moire/ultrasonic/util/FileUtilKt.kt | 47 ++++++ ...verterTest.kt => APIIndexConverterTest.kt} | 11 +- 31 files changed, 525 insertions(+), 191 deletions(-) create mode 100644 core/domain/src/main/AndroidManifest.xml create mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt create mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt delete mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt rename ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/{APIIndexesConverterTest.kt => APIIndexConverterTest.kt} (72%) diff --git a/core/domain/build.gradle b/core/domain/build.gradle index bc19d74c..734c977b 100644 --- a/core/domain/build.gradle +++ b/core/domain/build.gradle @@ -1,7 +1,14 @@ -apply from: bootstrap.kotlinModule +apply from: bootstrap.androidModule +apply plugin: 'kotlin-kapt' ext { jacocoExclude = [ '**/domain/**' ] } + +dependencies { + implementation androidSupport.roomRuntime + implementation androidSupport.roomKtx + kapt androidSupport.room +} diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1d8d8efa --- /dev/null +++ b/core/domain/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index b7112e2f..6c0537d3 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -1,18 +1,17 @@ package org.moire.ultrasonic.domain -import java.io.Serializable +import androidx.room.Entity +import androidx.room.PrimaryKey +@Entity(tableName = "artists") data class Artist( - override var id: String? = null, + @PrimaryKey override var id: String, override var name: String? = null, - var index: String? = null, - var coverArt: String? = null, - var albumCount: Long? = null, - var closeness: Int = 0 -) : Serializable, GenericEntry(), Comparable { - companion object { - private const val serialVersionUID = -5790532593784846982L - } + override var index: String? = null, + override var coverArt: String? = null, + override var albumCount: Long? = null, + override var closeness: Int = 0 +) : ArtistOrIndex(id), Comparable { override fun compareTo(other: Artist): Int { when { diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt new file mode 100644 index 00000000..63decd3a --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt @@ -0,0 +1,18 @@ +package org.moire.ultrasonic.domain + +import androidx.room.Ignore + +open class ArtistOrIndex( + @Ignore + override var id: String, + @Ignore + override var name: String? = null, + @Ignore + open var index: String? = null, + @Ignore + open var coverArt: String? = null, + @Ignore + open var albumCount: Long? = null, + @Ignore + open var closeness: Int = 0 +) : GenericEntry() diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt index 37bd863f..e731b769 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt @@ -1,8 +1,12 @@ package org.moire.ultrasonic.domain -abstract class GenericEntry { - // TODO: Should be non-null! - abstract val id: String? +import androidx.room.Ignore + +open class GenericEntry { + // TODO Should be non-null! + @Ignore + open val id: String? = null + @Ignore open val name: String? = null // These are just a formality and will never be called, diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt index e80ed2b2..49045571 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt @@ -1,11 +1,14 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey import java.io.Serializable +@Entity data class Genre( - val name: String, - val index: String -) : Serializable { + @PrimaryKey val index: String, + override val name: String +) : Serializable, GenericEntry() { companion object { private const val serialVersionUID = -3943025175219134028L } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt new file mode 100644 index 00000000..e56399b1 --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt @@ -0,0 +1,15 @@ +package org.moire.ultrasonic.domain + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "indexes") +data class Index( + @PrimaryKey override var id: String, + override var name: String? = null, + override var index: String? = null, + override var coverArt: String? = null, + override var albumCount: Long? = null, + override var closeness: Int = 0, + var musicFolderId: String? = null +) : ArtistOrIndex(id) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt deleted file mode 100644 index 3d5ef194..00000000 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.moire.ultrasonic.domain - -import java.io.Serializable - -data class Indexes( - val lastModified: Long, - val ignoredArticles: String, - val shortcuts: MutableList = mutableListOf(), - val artists: MutableList = mutableListOf() -) : Serializable { - companion object { - private const val serialVersionUID = 8156117238598414701L - } -} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index cdd035a5..1d0bc146 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -1,5 +1,7 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey import java.io.Serializable import java.util.Date @@ -35,8 +37,9 @@ class MusicDirectory { return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } } + @Entity data class Entry( - override var id: String, + @PrimaryKey override var id: String, var parent: String? = null, var isDirectory: Boolean = false, var title: String? = null, diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt index 1c23e86c..c4f94fb6 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt @@ -1,9 +1,13 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey + /** * Represents a top level directory in which music or other media is stored. */ +@Entity(tableName = "music_folders") data class MusicFolder( - override val id: String, + @PrimaryKey override val id: String, override val name: String ) : GenericEntry() diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index 847827e7..88519a56 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -1,9 +1,11 @@ +/** + * This module provides a base for for submodules which depend on the Android runtime + */ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'jacoco' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" - android { compileSdkVersion versions.compileSdk diff --git a/gradle_scripts/kotlin-module-bootstrap.gradle b/gradle_scripts/kotlin-module-bootstrap.gradle index ac732a7f..1eb26ec6 100644 --- a/gradle_scripts/kotlin-module-bootstrap.gradle +++ b/gradle_scripts/kotlin-module-bootstrap.gradle @@ -1,5 +1,8 @@ -apply plugin: 'java-library' +/** + * This module provides a base for for pure kotlin modules + */ apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" 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 d9c11c86..d1a5c18b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.data +import androidx.room.Room import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -7,6 +8,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.di.DB_FILENAME import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Util @@ -20,6 +22,8 @@ class ActiveServerProvider( private val repository: ServerSettingDao ) { private var cachedServer: ServerSetting? = null + private var cachedDatabase: MetaDatabase? = null + private var cachedServerId: Int? = null /** * Get the settings of the current Active Server @@ -82,6 +86,33 @@ class ActiveServerProvider( } } + @Synchronized + fun getActiveMetaDatabase(): MetaDatabase { + val activeServer = getActiveServerId() + + if (activeServer == cachedServerId && cachedDatabase != null) { + return cachedDatabase!! + } + + Timber.i("Switching to new database, id:$activeServer") + cachedServerId = activeServer + val db = Room.databaseBuilder( + UApp.applicationContext(), + MetaDatabase::class.java, + METADATA_DB + cachedServerId + ) + .fallbackToDestructiveMigrationOnDowngrade() + .build() + return db + } + + @Synchronized + fun deleteMetaDatabase(id: Int) { + cachedDatabase?.close() + UApp.applicationContext().deleteDatabase(METADATA_DB + id) + Timber.i("Deleted metadataBase, id:$id") + } + /** * Sets the minimum Subsonic API version of the current server. */ @@ -130,6 +161,9 @@ class ActiveServerProvider( } companion object { + + const val METADATA_DB = "$DB_FILENAME-meta-" + /** * Queries if the Active Server is the "Offline" mode of Ultrasonic * @return True, if the "Offline" mode is selected diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index e4aa77dc..e8c05cc7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -6,7 +6,8 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase /** - * Room Database to be used to store data for Ultrasonic + * Room Database to be used to store global data for the whole app. + * This could be settings or data that are not specific to any remote music database */ @Database(entities = [ServerSetting::class], version = 3) abstract class AppDatabase : RoomDatabase() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt new file mode 100644 index 00000000..2a86d496 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt @@ -0,0 +1,31 @@ +package org.moire.ultrasonic.data + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.moire.ultrasonic.domain.Artist + +@Dao +interface ArtistsDao { + /** + * Insert a list in the database. If the item already exists, replace it. + * + * @param objects the items to be inserted. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + fun set(objects: List) + + /** + * Clear the whole database + */ + @Query("DELETE FROM artists") + fun clear() + + /** + * Get all artists + */ + @Query("SELECT * FROM artists") + fun get(): List +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt new file mode 100644 index 00000000..ab69089f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt @@ -0,0 +1,146 @@ +package org.moire.ultrasonic.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicFolder + +@Dao +interface MusicFoldersDao : GenericDao { + /** + * Clear the whole database + */ + @Query("DELETE FROM music_folders") + fun clear() + + /** + * Get all folders + */ + @Query("SELECT * FROM music_folders") + fun get(): List +} + +@Dao +interface IndexDao : GenericDao { + + /** + * Clear the whole database + */ + @Query("DELETE FROM indexes") + fun clear() + + /** + * Get all indexes + */ + @Query("SELECT * FROM indexes") + fun get(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(vararg indexes: Index) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertArray(arr: Array) + + /** + * Get all indexes for a specific folder id + */ + @Query("SELECT * FROM indexes where musicFolderId LIKE :musicFolderId") + fun get(musicFolderId: String): List + + /** + * Upserts (insert or update) an object to the database + * + * @param obj the object to upsert + */ + @Transaction + @JvmSuppressWildcards + fun upsert(obj: Index) { + val id = insertIgnoring(obj) + if (id == -1L) { + update(obj) + } + } + + /** + * Upserts (insert or update) a list of objects + * + * @param objList the object to be upserted + */ + @Transaction + @JvmSuppressWildcards + fun upsert(objList: List) { + val insertResult = insertIgnoring(objList) + val updateList: MutableList = ArrayList() + for (i in insertResult.indices) { + if (insertResult[i] == -1L) { + updateList.add(objList[i]) + } + } + if (updateList.isNotEmpty()) { + update(updateList) + } + } +} + +interface GenericDao { + /** + * Replaces the list with a new collection + * + * @param objects the items to be inserted. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + fun set(objects: List) + + /** + * Insert an object in the database. + * + * @param obj the object to be inserted. + * @return The SQLite row id + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + @JvmSuppressWildcards + fun insertIgnoring(obj: T): Long + + /** + * Insert an array of objects in the database. + * + * @param obj the objects to be inserted. + * @return The SQLite row ids + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + @JvmSuppressWildcards + fun insertIgnoring(obj: List?): List + + /** + * Update an object from the database. + * + * @param obj the object to be updated + */ + @Update + @JvmSuppressWildcards + fun update(obj: T) + + /** + * Update an array of objects from the database. + * + * @param obj the object to be updated + */ + @Update + @JvmSuppressWildcards + fun update(obj: List?) + + /** + * Delete an object from the database + * + * @param obj the object to be deleted + */ + @Delete + @JvmSuppressWildcards + fun delete(obj: T) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt new file mode 100644 index 00000000..6abdc85a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt @@ -0,0 +1,19 @@ +package org.moire.ultrasonic.data + +import androidx.room.Database +import androidx.room.RoomDatabase +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicFolder + +@Database( + entities = [Artist::class, Index::class, MusicFolder::class], + version = 1 +) +abstract class MetaDatabase : RoomDatabase() { + abstract fun artistsDao(): ArtistsDao + + abstract fun musicFoldersDao(): MusicFoldersDao + + abstract fun indexDao(): IndexDao +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index bb4c3575..39c3fe91 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -65,7 +65,7 @@ val musicServiceModule = module { single { SubsonicAPIClient(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get(), get())) + CachedMusicService(RESTMusicService(get(), get())) } single(named(OFFLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt index cec72778..51c2c72f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -5,12 +5,20 @@ package org.moire.ultrasonic.domain import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist +// When we like to convert to an Artist fun APIArtist.toDomainEntity(): Artist = Artist( id = this@toDomainEntity.id, coverArt = this@toDomainEntity.coverArt, name = this@toDomainEntity.name ) +// When we like to convert to an index (eg. a single directory). +fun APIArtist.toIndexEntity(): Index = Index( + id = this@toIndexEntity.id, + coverArt = this@toIndexEntity.coverArt, + name = this@toIndexEntity.name +) + fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { name = this@toMusicDirectoryDomainEntity.name addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt index b41bcb66..fb718204 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt @@ -3,15 +3,37 @@ @file:JvmName("APIIndexesConverter") package org.moire.ultrasonic.domain -import org.moire.ultrasonic.api.subsonic.models.Index +import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes -fun APIIndexes.toDomainEntity(): Indexes = Indexes( - this.lastModified, this.ignoredArticles, - this.shortcutList.map { it.toDomainEntity() }.toMutableList(), - this.indexList.foldIndexToArtistList().toMutableList() +fun APIIndexes.toArtistList(): List { + val list = this.shortcutList.map { it.toDomainEntity() }.toMutableList() + list.addAll(this.indexList.foldIndexToArtistList()) + return list +} + +fun APIIndexes.toIndexList(musicFolderId: String?): List { + val list = this.shortcutList.map { it.toIndexEntity() }.toMutableList() + list.addAll(this.indexList.foldIndexToIndexList(musicFolderId)) + return list +} + +private fun List.foldIndexToArtistList(): List = this.fold( + listOf(), + { acc, index -> + acc + index.artists.map { + it.toDomainEntity() + } + } ) -private fun List.foldIndexToArtistList(): List = this.fold( - listOf(), { acc, index -> acc + index.artists.map { it.toDomainEntity() } } +private fun List.foldIndexToIndexList(musicFolderId: String?): List = this.fold( + listOf(), + { acc, index -> + acc + index.artists.map { + val ret = it.toIndexEntity() + ret.musicFolderId = musicFolderId + ret + } + } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 6b48979c..51d42f65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -4,13 +4,13 @@ import android.os.Bundle import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library */ -class ArtistListFragment : GenericListFragment() { +class ArtistListFragment : GenericListFragment() { /** * The ViewModel to use to get the data @@ -41,7 +41,7 @@ class ArtistListFragment : GenericListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?): LiveData> { val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false return listModel.getItems(refresh, refreshListView!!) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index a774ebcd..c014a059 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -23,19 +23,19 @@ import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.service.MusicService /** * Provides ViewModel which contains the list of available Artists */ class ArtistListModel(application: Application) : GenericListModel(application) { - private val artists: MutableLiveData> = MutableLiveData(listOf()) + private val artists: MutableLiveData> = MutableLiveData(listOf()) /** * Retrieves all available Artists in a LiveData */ - fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { + fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position if (artists.value!!.isEmpty() || refresh) { @@ -55,14 +55,14 @@ class ArtistListModel(application: Application) : GenericListModel(application) val musicFolderId = activeServer.musicFolderId - val result = if (!isOffline && useId3Tags) - musicService.getArtists(refresh) - else musicService.getIndexes(musicFolderId, refresh) + val result: List - val retrievedArtists: MutableList = - ArrayList(result.shortcuts.size + result.artists.size) - retrievedArtists.addAll(result.shortcuts) - retrievedArtists.addAll(result.artists) - artists.postValue(retrievedArtists) + if (!isOffline && useId3Tags) { + result = musicService.getArtists(refresh) + } else { + result = musicService.getIndexes(musicFolderId, refresh) + } + + artists.postValue(result.toMutableList()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 69988607..0b375bfc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter import java.text.Collator import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.util.Util @@ -22,12 +22,12 @@ import org.moire.ultrasonic.util.Util * Creates a Row in a RecyclerView which contains the details of an Artist */ class ArtistRowAdapter( - artistList: List, - onItemClick: (Artist) -> Unit, - onContextMenuClick: (MenuItem, Artist) -> Boolean, + artistList: List, + onItemClick: (ArtistOrIndex) -> Unit, + onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, onMusicFolderUpdate: (String?) -> Unit -) : GenericRowAdapter( +) : GenericRowAdapter( onItemClick, onContextMenuClick, onMusicFolderUpdate @@ -43,7 +43,7 @@ class ArtistRowAdapter( /** * Sets the data to be displayed in the RecyclerView */ - override fun setData(data: List) { + override fun setData(data: List) { itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name }) super.notifyDataSetChanged() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index 468802a2..958db756 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -45,7 +45,7 @@ open class GenericListModel(application: Application) : return true } - internal val musicFolders: MutableLiveData> = MutableLiveData() + internal val musicFolders: MutableLiveData> = MutableLiveData(listOf()) /** * Helper function to check online status diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index bde3efd5..9d66f6ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import android.widget.AdapterView import android.widget.ListView import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.Dispatchers @@ -104,7 +103,7 @@ class ServerSelectorFragment : Fragment() { val serverList = serverSettingsModel.getServerList() serverList.observe( this, - Observer { t -> + { t -> serverRowAdapter!!.setData(t.toTypedArray()) } ) @@ -141,10 +140,16 @@ class ServerSelectorFragment : Fragment() { dialog.dismiss() val activeServerIndex = activeServerProvider.getActiveServer().index + val id = ActiveServerProvider.getActiveServerId() + // If the currently active server is deleted, go offline if (index == activeServerIndex) setActiveServer(-1) serverSettingsModel.deleteItem(index) + + // Clear the metadata cache + activeServerProvider.deleteMetaDatabase(id) + Timber.i("Server deleted: $index") } .setNegativeButton(R.string.common_cancel) { dialog, _ -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index f18a0f26..7ee29a55 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -11,10 +11,12 @@ import java.util.concurrent.TimeUnit import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.MetaDatabase +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -33,19 +35,24 @@ import org.moire.ultrasonic.util.Util @Suppress("TooManyFunctions") class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent { private val activeServerProvider: ActiveServerProvider by inject() + private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase() + + // Old style TimeLimitedCache + private val cachedMusicDirectories: LRUCache> + private val cachedArtist: LRUCache> + private val cachedAlbum: LRUCache> + private val cachedUserInfo: LRUCache> + private val cachedLicenseValid = TimeLimitedCache(120, TimeUnit.SECONDS) + private val cachedPlaylists = TimeLimitedCache?>(3600, TimeUnit.SECONDS) + private val cachedPodcastsChannels = + TimeLimitedCache?>(3600, TimeUnit.SECONDS) + private val cachedGenres = TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS) + + // New Room Database + private var cachedArtists = metaDatabase.artistsDao() + private var cachedIndexes = metaDatabase.indexDao() + private val cachedMusicFolders = metaDatabase.musicFoldersDao() - private val cachedMusicDirectories: LRUCache> - private val cachedArtist: LRUCache> - private val cachedAlbum: LRUCache> - private val cachedUserInfo: LRUCache> - private val cachedLicenseValid = TimeLimitedCache(expiresAfter = 10, TimeUnit.MINUTES) - private val cachedIndexes = TimeLimitedCache() - private val cachedArtists = TimeLimitedCache() - private val cachedPlaylists = TimeLimitedCache?>() - private val cachedPodcastsChannels = TimeLimitedCache>() - private val cachedMusicFolders = - TimeLimitedCache?>(10, TimeUnit.HOURS) - private val cachedGenres = TimeLimitedCache?>(10, TimeUnit.HOURS) private var restUrl: String? = null private var cachedMusicFolderId: String? = null @@ -72,41 +79,51 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, if (refresh) { cachedMusicFolders.clear() } + var result = cachedMusicFolders.get() - val cache = cachedMusicFolders.get() - if (cache != null) return cache - - val result = musicService.getMusicFolders(refresh) - cachedMusicFolders.set(result) - + if (result.isEmpty()) { + result = musicService.getMusicFolders(refresh) + cachedMusicFolders.set(result) + } return result } @Throws(Exception::class) - override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { + override fun getIndexes(musicFolderId: String?, refresh: Boolean): List { checkSettingsChanged() + if (refresh) { cachedIndexes.clear() - cachedMusicFolders.clear() cachedMusicDirectories.clear() } - var result = cachedIndexes.get() - if (result == null) { - result = musicService.getIndexes(musicFolderId, refresh) - cachedIndexes.set(result) + + var indexes: List + + if (musicFolderId == null) { + indexes = cachedIndexes.get() + } else { + indexes = cachedIndexes.get(musicFolderId) } - return result + + if (indexes.isEmpty()) { + indexes = musicService.getIndexes(musicFolderId, refresh) + cachedIndexes.upsert(indexes) + } + + return indexes } @Throws(Exception::class) - override fun getArtists(refresh: Boolean): Indexes { + override fun getArtists(refresh: Boolean): List { checkSettingsChanged() if (refresh) { cachedArtists.clear() } var result = cachedArtists.get() - if (result == null) { + + if (result.isEmpty()) { result = musicService.getArtists(refresh) + cachedArtist.clear() cachedArtists.set(result) } return result @@ -296,19 +313,26 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, return musicService.setJukeboxGain(gain) } + @Synchronized private fun checkSettingsChanged() { val newUrl = activeServerProvider.getRestUrl(null) val newFolderId = activeServerProvider.getActiveServer().musicFolderId if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) { - cachedMusicFolders.clear() + // Switch database + metaDatabase = activeServerProvider.getActiveMetaDatabase() + cachedArtists = metaDatabase.artistsDao() + cachedIndexes = metaDatabase.indexDao() + + // Clear in memory caches cachedMusicDirectories.clear() cachedLicenseValid.clear() - cachedIndexes.clear() cachedPlaylists.clear() cachedGenres.clear() cachedAlbum.clear() cachedArtist.clear() cachedUserInfo.clear() + + // Set the cache keys restUrl = newUrl cachedMusicFolderId = newFolderId } @@ -330,7 +354,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getGenres(refresh: Boolean): List? { + override fun getGenres(refresh: Boolean): List { checkSettingsChanged() if (refresh) { cachedGenres.clear() @@ -338,11 +362,11 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, var result = cachedGenres.get() if (result == null) { result = musicService.getGenres(refresh) - cachedGenres.set(result) + cachedGenres.set(result!!) } - val sorted = result?.toMutableList() - sorted?.sortWith { genre, genre2 -> + val sorted = result.toMutableList() + sorted.sortWith { genre, genre2 -> genre.name.compareTo( genre2.name, ignoreCase = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 6e417358..73521d6e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -7,10 +7,11 @@ package org.moire.ultrasonic.service import java.io.InputStream +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -46,10 +47,10 @@ interface MusicService { fun getMusicFolders(refresh: Boolean): List @Throws(Exception::class) - fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes + fun getIndexes(musicFolderId: String?, refresh: Boolean): List @Throws(Exception::class) - fun getArtists(refresh: Boolean): Indexes + fun getArtists(refresh: Boolean): List @Throws(Exception::class) fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index e06a3a22..044fdc2d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -28,7 +28,7 @@ import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -50,21 +50,21 @@ import timber.log.Timber class OfflineMusicService : MusicService, KoinComponent { private val activeServerProvider: ActiveServerProvider by inject() - override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { - val artists: MutableList = ArrayList() + override fun getIndexes(musicFolderId: String?, refresh: Boolean): List { + val indexes: MutableList = ArrayList() val root = FileUtil.getMusicDirectory() for (file in FileUtil.listFiles(root)) { if (file.isDirectory) { - val artist = Artist() - artist.id = file.path - artist.index = file.name.substring(0, 1) - artist.name = file.name - artists.add(artist) + val index = Index(file.path) + index.id = file.path + index.index = file.name.substring(0, 1) + index.name = file.name + indexes.add(index) } } val ignoredArticlesString = "The El La Los Las Le Les" val ignoredArticles = COMPILE.split(ignoredArticlesString) - artists.sortWith { lhsArtist, rhsArtist -> + indexes.sortWith { lhsArtist, rhsArtist -> var lhs = lhsArtist.name!!.lowercase(Locale.ROOT) var rhs = rhsArtist.name!!.lowercase(Locale.ROOT) val lhs1 = lhs[0] @@ -92,7 +92,7 @@ class OfflineMusicService : MusicService, KoinComponent { lhs.compareTo(rhs) } - return Indexes(0L, ignoredArticlesString, artists = artists) + return indexes } override fun getMusicDirectory( @@ -127,8 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent { val artistName = artistFile.name if (artistFile.isDirectory) { if (matchCriteria(criteria, artistName).also { closeness = it } > 0) { - val artist = Artist() - artist.id = artistFile.path + val artist = Artist(artistFile.path) artist.index = artistFile.name.substring(0, 1) artist.name = artistName artist.closeness = closeness @@ -442,7 +441,7 @@ class OfflineMusicService : MusicService, KoinComponent { override fun isLicenseValid(): Boolean = true @Throws(OfflineException::class) - override fun getArtists(refresh: Boolean): Indexes { + override fun getArtists(refresh: Boolean): List { throw OfflineException("getArtists isn't available in offline mode") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 0d6730d8..b964a90c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -6,9 +6,6 @@ */ package org.moire.ultrasonic.service -import java.io.BufferedWriter -import java.io.File -import java.io.FileWriter import java.io.IOException import java.io.InputStream import okhttp3.Protocol @@ -20,15 +17,13 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.toStreamResponse -import org.moire.ultrasonic.cache.PermanentFileStorage -import org.moire.ultrasonic.cache.serializers.getIndexesSerializer -import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -39,11 +34,14 @@ import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.domain.toArtistList import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntityList +import org.moire.ultrasonic.domain.toIndexList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.FileUtilKt import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -53,7 +51,6 @@ import timber.log.Timber @Suppress("LargeClass") open class RESTMusicService( val subsonicAPIClient: SubsonicAPIClient, - private val fileStorage: PermanentFileStorage, private val activeServerProvider: ActiveServerProvider ) : MusicService { @@ -77,49 +74,31 @@ open class RESTMusicService( override fun getMusicFolders( refresh: Boolean ): List { - val cachedMusicFolders = fileStorage.load( - MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer() - ) - - if (cachedMusicFolders != null && !refresh) return cachedMusicFolders - val response = API.getMusicFolders().execute().throwOnFailure() - val musicFolders = response.body()!!.musicFolders.toDomainEntityList() - fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer()) - - return musicFolders + return response.body()!!.musicFolders.toDomainEntityList() } + /** + * Retrieves the artists for a given music folder * + */ @Throws(Exception::class) override fun getIndexes( musicFolderId: String?, refresh: Boolean - ): Indexes { - val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "") - - val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer()) - if (cachedIndexes != null && !refresh) return cachedIndexes - + ): List { val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure() - val indexes = response.body()!!.indexes.toDomainEntity() - fileStorage.store(indexName, indexes, getIndexesSerializer()) - return indexes + return response.body()!!.indexes.toIndexList(musicFolderId) } @Throws(Exception::class) override fun getArtists( refresh: Boolean - ): Indexes { - val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer()) - if (cachedArtists != null && !refresh) return cachedArtists - + ): List { val response = API.getArtists(null).execute().throwOnFailure() - val indexes = response.body()!!.indexes.toDomainEntity() - fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer()) - return indexes + return response.body()!!.indexes.toArtistList() } @Throws(Exception::class) @@ -186,11 +165,11 @@ open class RESTMusicService( criteria: SearchCriteria ): SearchResult { return try { - if ( - !isOffline() && - Util.getShouldUseId3Tags() - ) search3(criteria) - else search2(criteria) + if (!isOffline() && Util.getShouldUseId3Tags()) { + search3(criteria) + } else { + search2(criteria) + } } catch (ignored: ApiNotSupportedException) { // Ensure backward compatibility with REST 1.3. searchOld(criteria) @@ -262,28 +241,7 @@ open class RESTMusicService( activeServerProvider.getActiveServer().name, name ) - val fw = FileWriter(playlistFile) - val bw = BufferedWriter(fw) - - try { - fw.write("#EXTM3U\n") - for (e in playlist.getChildren()) { - var filePath = FileUtil.getSongFile(e).absolutePath - - if (!File(filePath).exists()) { - val ext = FileUtil.getExtension(filePath) - val base = FileUtil.getBaseName(filePath) - filePath = "$base.complete.$ext" - } - fw.write(filePath + "\n") - } - } catch (e: IOException) { - Timber.w("Failed to save playlist: %s", name) - throw e - } finally { - bw.close() - fw.close() - } + FileUtilKt.savePlaylist(playlistFile, playlist, name) } @Throws(Exception::class) @@ -711,10 +669,4 @@ open class RESTMusicService( activeServerProvider.setMinimumApiVersion(it.restApiVersion) } } - - companion object { - private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" - private const val INDEXES_STORAGE_NAME = "indexes" - private const val ARTISTS_STORAGE_NAME = "artists" - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt new file mode 100644 index 00000000..fc527c15 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt @@ -0,0 +1,47 @@ +/* + * FileUtil.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import org.moire.ultrasonic.domain.MusicDirectory +import timber.log.Timber + +// TODO: Convert FileUtil.java and merge into here. +object FileUtilKt { + fun savePlaylist( + playlistFile: File?, + playlist: MusicDirectory, + name: String + ) { + val fw = FileWriter(playlistFile) + val bw = BufferedWriter(fw) + + try { + fw.write("#EXTM3U\n") + for (e in playlist.getChildren()) { + var filePath = FileUtil.getSongFile(e).absolutePath + + if (!File(filePath).exists()) { + val ext = FileUtil.getExtension(filePath) + val base = FileUtil.getBaseName(filePath) + filePath = "$base.complete.$ext" + } + fw.write(filePath + "\n") + } + } catch (e: IOException) { + Timber.w("Failed to save playlist: %s", name) + throw e + } finally { + bw.close() + fw.close() + } + } +} diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt similarity index 72% rename from ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt index d5b052c4..ca23b15f 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt @@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes /** * Unit tests for extension functions in [APIIndexesConverter.kt]. */ -class APIIndexesConverterTest { +class APIIndexConverterTest { @Test fun `Should convert Indexes entity`() { val artistsA = listOf( @@ -31,15 +31,12 @@ class APIIndexesConverterTest { shortcutList = artistsA ) - val convertedEntity = entity.toDomainEntity() + val convertedEntity = entity.toIndexList(null) val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList() with(convertedEntity) { - lastModified `should be equal to` entity.lastModified - ignoredArticles `should be equal to` entity.ignoredArticles - artists.size `should be equal to` expectedArtists.size - artists `should be equal to` expectedArtists - shortcuts `should be equal to` artistsA.map { it.toDomainEntity() }.toMutableList() + size `should be equal to` expectedArtists.size + this `should be equal to` expectedArtists } } } From fe9b2f9700b4bbd2dfd95e7e7d9b843cc61bcdd9 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 23 Jun 2021 16:32:05 +0200 Subject: [PATCH 11/12] Update Baseline file --- detekt-baseline.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 7235a790..779abc17 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -10,7 +10,6 @@ ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - EmptyFunctionBlock:SongView.kt$SongView${} FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song) From b546f2c2fb01e0a530d47a243086f3f579977845 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 23 Jun 2021 17:09:33 +0200 Subject: [PATCH 12/12] The Tests actually caught an error :) If shortcuts were set, these were added as duplicates to the list. --- .../api/subsonic/models/AlbumListTypeTest.kt | 3 ++- .../ultrasonic/domain/APIIndexesConverter.kt | 26 ++++++++++++++----- .../domain/APIIndexConverterTest.kt | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt index bde2ead6..932ee62f 100644 --- a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt +++ b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.api.subsonic.models +import java.util.Locale import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should throw` import org.junit.Test @@ -10,7 +11,7 @@ import org.junit.Test class AlbumListTypeTest { @Test fun `Should create type from string ignoring case`() { - val type = AlbumListType.SORTED_BY_NAME.typeName.toLowerCase() + val type = AlbumListType.SORTED_BY_NAME.typeName.lowercase(Locale.ROOT) val albumListType = AlbumListType.fromName(type) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt index fb718204..2567ee67 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt @@ -7,15 +7,29 @@ import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes fun APIIndexes.toArtistList(): List { - val list = this.shortcutList.map { it.toDomainEntity() }.toMutableList() - list.addAll(this.indexList.foldIndexToArtistList()) - return list + val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList() + val indexes = this.indexList.foldIndexToArtistList() + + indexes.forEach { + if (!shortcuts.contains(it)) { + shortcuts.add(it) + } + } + + return shortcuts } fun APIIndexes.toIndexList(musicFolderId: String?): List { - val list = this.shortcutList.map { it.toIndexEntity() }.toMutableList() - list.addAll(this.indexList.foldIndexToIndexList(musicFolderId)) - return list + val shortcuts = this.shortcutList.map { it.toIndexEntity() }.toMutableList() + val indexes = this.indexList.foldIndexToIndexList(musicFolderId) + + indexes.forEach { + if (!shortcuts.contains(it)) { + shortcuts.add(it) + } + } + + return shortcuts } private fun List.foldIndexToArtistList(): List = this.fold( diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt index ca23b15f..06e2cbf9 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt @@ -31,7 +31,7 @@ class APIIndexConverterTest { shortcutList = artistsA ) - val convertedEntity = entity.toIndexList(null) + val convertedEntity = entity.toArtistList() val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList() with(convertedEntity) {