Merge pull request #173 from ultrasonic/persistent-cache

Persistent cache
This commit is contained in:
Yahor Berdnikau 2018-03-18 17:42:35 +01:00 committed by GitHub
commit 3de18ed282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 718 additions and 73 deletions

View File

@ -22,7 +22,7 @@ jobs:
- run: - run:
name: unit-tests name: unit-tests
command: | command: |
./gradlew :subsonic-api:test :ultrasonic:testDebugUnitTest ./gradlew :subsonic-api:test :cache:test :ultrasonic:testDebugUnitTest
./gradlew jacocoFullReport ./gradlew jacocoFullReport
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
- run: - run:

46
cache/build.gradle vendored Normal file
View File

@ -0,0 +1,46 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'jacoco'
apply from: '../gradle_scripts/code_quality.gradle'
dependencies {
api project(':domain')
api other.kotlinStdlib
api other.twitterSerial
testImplementation testing.junit
testImplementation testing.kotlinJunit
testImplementation testing.mockito
testImplementation testing.mockitoInline
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
}
jacoco {
toolVersion(versions.jacoco)
}
ext {
jacocoExclude = []
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled false
xml.enabled true
}
afterEvaluate {
classDirectories = files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude)
})
}
}
test.finalizedBy jacocoTestReport
test {
jacoco {
excludes += jacocoExclude
}
}

View File

@ -0,0 +1,14 @@
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?
}

View File

@ -0,0 +1,75 @@
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<T> = Serializer<T>
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 <T> store(
name: String,
objectToStore: T,
objectSerializer: DomainEntitySerializer<T>
) {
val storeFile = getFile(name)
if (!storeFile.exists()) storeFile.createNewFile()
storeFile.writeBytes(serializer.toByteArray(objectToStore, objectSerializer))
}
/**
* Loads object with [name] key using [objectDeserializer] deserializer.
*/
fun <T> load(
name: String,
objectDeserializer: DomainEntitySerializer<T>
): 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
}
}

View File

@ -0,0 +1,65 @@
@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<Artist>(SERIALIZER_VERSION) {
override fun serializeObject(
context: SerializationContext,
output: SerializerOutput<out 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<Artist> = artistSerializer
private val artistListSerializer = CollectionSerializers.getListSerializer(artistSerializer)
/**
* Serializer/deserializer for list of [Artist] domain entities.
*/
fun getArtistListSerializer(): DomainEntitySerializer<List<Artist>> = artistListSerializer

View File

@ -0,0 +1,48 @@
@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<Indexes>(SERIALIZATION_VERSION) {
override fun serializeObject(
context: SerializationContext,
output: SerializerOutput<out SerializerOutput<*>>,
item: Indexes
) {
val artistListSerializer = getArtistListSerializer()
output.writeLong(item.lastModified)
.writeString(item.ignoredArticles)
.writeObject<MutableList<Artist>>(context, item.shortcuts, artistListSerializer)
.writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer)
}
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<Indexes> = indexesSerializer

View File

@ -0,0 +1,50 @@
@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<MusicFolder>(SERIALIZATION_VERSION) {
override fun serializeObject(
context: SerializationContext,
output: SerializerOutput<out SerializerOutput<*>>,
item: MusicFolder
) {
output.writeString(item.id).writeString(item.name)
}
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<MusicFolder> = musicFolderSerializer
private val musicFolderListSerializer =
CollectionSerializers.getListSerializer(musicFolderSerializer)
/**
* Serializer/deserializer for [List] of [MusicFolder] items.
*/
fun getMusicFolderListSerializer(): DomainEntitySerializer<List<MusicFolder>> =
musicFolderListSerializer

View File

@ -0,0 +1,42 @@
package org.moire.ultrasonic.cache
import com.nhaarman.mockito_kotlin.mock
import com.twitter.serial.util.SerializationUtils
import org.amshove.kluent.`it returns`
import org.junit.Before
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
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<Directories> {
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)
}
}

View File

@ -0,0 +1,81 @@
package org.moire.ultrasonic.cache
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer
import org.moire.ultrasonic.domain.MusicFolder
import java.io.File
/**
* 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 equal to` true
getServerStorageDir().exists() `should 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 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 equal` 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 equal` 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 equal to` 0
}
@Test
fun `Should return null if serialized file not available`() {
val loadedItem = storage.load("some-name", getMusicFolderSerializer())
loadedItem `should equal` null
}
private fun getServerStorageDir() = File(storageDir, serverId)
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.cache.serializers
import org.amshove.kluent.`should equal`
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 equal` 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 equal` itemsList
}
}

View File

@ -0,0 +1,40 @@
package org.moire.ultrasonic.cache.serializers
import org.amshove.kluent.`should equal`
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 equal` item
}
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.cache.serializers
import org.amshove.kluent.`should equal`
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 equal` 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 equal` itemsList
}
}

View File

@ -22,6 +22,7 @@ ext.versions = [
jackson : "2.9.0", jackson : "2.9.0",
okhttp : "3.9.0", okhttp : "3.9.0",
semver : "1.0.0", semver : "1.0.0",
twitterSerial : "0.1.6",
junit : "4.12", junit : "4.12",
mockito : "2.12.0", mockito : "2.12.0",
@ -52,6 +53,7 @@ ext.other = [
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson", jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp", okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
semver : "net.swiftzer.semver:semver:$versions.semver", semver : "net.swiftzer.semver:semver:$versions.semver",
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
] ]
ext.testing = [ ext.testing = [

38
domain/build.gradle Normal file
View File

@ -0,0 +1,38 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'jacoco'
apply from: '../gradle_scripts/code_quality.gradle'
dependencies {
api other.kotlinStdlib
api other.semver
}
jacoco {
toolVersion(versions.jacoco)
}
ext {
jacocoExclude = []
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled false
xml.enabled true
}
afterEvaluate {
classDirectories = files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude)
})
}
}
test.finalizedBy jacocoTestReport
test {
jacoco {
excludes += jacocoExclude
}
}

View File

@ -1,10 +1,12 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Entry
/** /**
* The result of a search. Contains matching artists, albums and songs. * The result of a search. Contains matching artists, albums and songs.
*/ */
data class SearchResult( data class SearchResult(
val artists: List<Artist>, val artists: List<Artist>,
val albums: List<MusicDirectory.Entry>, val albums: List<Entry>,
val songs: List<MusicDirectory.Entry> val songs: List<Entry>
) )

View File

@ -6,9 +6,11 @@ task jacocoMergeReports(type: JacocoMerge) {
def subsonicApi = project.findProject("subsonic-api") def subsonicApi = project.findProject("subsonic-api")
def ultrasonicApp = project.findProject("ultrasonic") def ultrasonicApp = project.findProject("ultrasonic")
def cache = project.findProject("cache")
executionData( executionData(
"${subsonicApi.buildDir}/jacoco/test.exec", "${subsonicApi.buildDir}/jacoco/test.exec",
"${ultrasonicApp.buildDir}/jacoco/testDebugUnitTest.exec", "${ultrasonicApp.buildDir}/jacoco/testDebugUnitTest.exec",
"${cache.buildDir}/jacoco/test.exec"
) )
destinationFile(file("${project.buildDir}/jacoco/jacoco.exec")) destinationFile(file("${project.buildDir}/jacoco/jacoco.exec"))
} }
@ -20,6 +22,7 @@ def createJacocoFullReportTask() {
def subsonicApi = project.findProject("subsonic-api") def subsonicApi = project.findProject("subsonic-api")
def ultrasonicApp = project.findProject("ultrasonic") def ultrasonicApp = project.findProject("ultrasonic")
def cache = project.findProject("cache")
classDirectories = files( classDirectories = files(
fileTree( fileTree(
@ -29,6 +32,10 @@ def createJacocoFullReportTask() {
fileTree( fileTree(
dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org", dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org",
excludes: ultrasonicApp.jacocoExclude excludes: ultrasonicApp.jacocoExclude
),
fileTree(
dir: "${cache.buildDir}/classes/kotlin/main",
excludes: cache.jacocoExclude
) )
) )
sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(), sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(),

View File

@ -1,4 +1,7 @@
include ':library', ':subsonic-api' include ':library'
include ':domain'
include ':subsonic-api'
include ':cache'
include ':menudrawer' include ':menudrawer'
include ':pulltorefresh' include ':pulltorefresh'
include ':ultrasonic' include ':ultrasonic'

View File

@ -47,15 +47,15 @@ dependencies {
implementation project(':menudrawer') implementation project(':menudrawer')
implementation project(':pulltorefresh') implementation project(':pulltorefresh')
implementation project(':library') implementation project(':library')
implementation project(':domain')
implementation project(':subsonic-api') implementation project(':subsonic-api')
implementation project(':cache')
implementation androidSupport.support implementation androidSupport.support
implementation androidSupport.design implementation androidSupport.design
implementation other.kotlinStdlib implementation other.kotlinStdlib
implementation other.semver
testImplementation other.kotlinReflect testImplementation other.kotlinReflect
testImplementation testing.junit testImplementation testing.junit
testImplementation testing.kotlinJunit testImplementation testing.kotlinJunit

View File

@ -12,7 +12,9 @@ import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import org.moire.ultrasonic.BuildConfig;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
@ -279,6 +281,15 @@ public class ServerSettingsFragment extends PreferenceFragment
int activeServers = sharedPreferences int activeServers = sharedPreferences
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0); .getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
// Clear permanent storage
final String storageServerId = MusicServiceFactory.getServerId(sharedPreferences, serverId);
final PermanentFileStorage fileStorage = new PermanentFileStorage(
MusicServiceFactory.getDirectories(getActivity()),
storageServerId,
BuildConfig.DEBUG
);
fileStorage.clearAll();
// Reset values to null so when we ask for them again they are new // Reset values to null so when we ask for them again they are new
sharedPreferences.edit() sharedPreferences.edit()
.remove(Constants.PREFERENCES_KEY_SERVER_NAME + serverId) .remove(Constants.PREFERENCES_KEY_SERVER_NAME + serverId)

View File

@ -22,12 +22,17 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.BuildConfig; import org.moire.ultrasonic.BuildConfig;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions; import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions;
import org.moire.ultrasonic.cache.Directories;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.Util;
import java.io.File;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
* @version $Id$ * @version $Id$
@ -44,7 +49,9 @@ public class MusicServiceFactory {
synchronized (MusicServiceFactory.class) { synchronized (MusicServiceFactory.class) {
if (OFFLINE_MUSIC_SERVICE == null) { if (OFFLINE_MUSIC_SERVICE == null) {
Log.d(LOG_TAG, "Creating new offline music service"); Log.d(LOG_TAG, "Creating new offline music service");
OFFLINE_MUSIC_SERVICE = new OfflineMusicService(createSubsonicApiClient(context)); OFFLINE_MUSIC_SERVICE = new OfflineMusicService(
createSubsonicApiClient(context),
getPermanentFileStorage(context));
} }
} }
} }
@ -57,7 +64,8 @@ public class MusicServiceFactory {
if (REST_MUSIC_SERVICE == null) { if (REST_MUSIC_SERVICE == null) {
Log.d(LOG_TAG, "Creating new rest music service"); Log.d(LOG_TAG, "Creating new rest music service");
REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService( REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService(
createSubsonicApiClient(context))); createSubsonicApiClient(context),
getPermanentFileStorage(context)));
} }
} }
} }
@ -104,4 +112,39 @@ public class MusicServiceFactory {
Constants.REST_CLIENT_ID, allowSelfSignedCertificate, Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
enableLdapUserSupport, BuildConfig.DEBUG); enableLdapUserSupport, BuildConfig.DEBUG);
} }
private static PermanentFileStorage getPermanentFileStorage(final Context context) {
final SharedPreferences preferences = Util.getPreferences(context);
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
final String serverId = getServerId(preferences, instance);
return new PermanentFileStorage(getDirectories(context), serverId, BuildConfig.DEBUG);
}
public static String getServerId(final SharedPreferences sp, final int instance) {
String serverUrl = sp.getString(
Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
return String.valueOf(Math.abs((serverUrl + instance).hashCode()));
}
public static Directories getDirectories(final Context context) {
return new Directories() {
@NotNull
@Override
public File getInternalCacheDir() {
return context.getCacheDir();
}
@NotNull
@Override
public File getInternalDataDir() {
return context.getFilesDir();
}
@Override
public File getExternalCacheDir() {
return context.getExternalCacheDir();
}
};
}
} }

View File

@ -24,6 +24,7 @@ import android.media.MediaMetadataRetriever;
import android.util.Log; import android.util.Log;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.domain.Artist; import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.Genre; import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.domain.Indexes; import org.moire.ultrasonic.domain.Indexes;
@ -67,8 +68,8 @@ public class OfflineMusicService extends RESTMusicService
private static final String TAG = OfflineMusicService.class.getSimpleName(); private static final String TAG = OfflineMusicService.class.getSimpleName();
private static final Pattern COMPILE = Pattern.compile(" "); private static final Pattern COMPILE = Pattern.compile(" ");
public OfflineMusicService(SubsonicAPIClient subsonicAPIClient) { public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) {
super(subsonicAPIClient); super(subsonicAPIClient, storage);
} }
@Override @Override

View File

@ -61,6 +61,8 @@ import org.moire.ultrasonic.api.subsonic.response.SharesResponse;
import org.moire.ultrasonic.api.subsonic.response.StreamResponse; import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
import org.moire.ultrasonic.api.subsonic.response.VideosResponse; import org.moire.ultrasonic.api.subsonic.response.VideosResponse;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.cache.serializers.DomainSerializers;
import org.moire.ultrasonic.domain.APIAlbumConverter; import org.moire.ultrasonic.domain.APIAlbumConverter;
import org.moire.ultrasonic.domain.APIArtistConverter; import org.moire.ultrasonic.domain.APIArtistConverter;
import org.moire.ultrasonic.domain.APIBookmarkConverter; import org.moire.ultrasonic.domain.APIBookmarkConverter;
@ -104,7 +106,6 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -117,10 +118,19 @@ import retrofit2.Response;
public class RESTMusicService implements MusicService { public class RESTMusicService implements MusicService {
private static final String TAG = RESTMusicService.class.getSimpleName(); private static final String TAG = RESTMusicService.class.getSimpleName();
private final SubsonicAPIClient subsonicAPIClient; private static final String MUSIC_FOLDER_STORAGE_NAME = "music_folder";
private static final String INDEXES_STORAGE_NAME = "indexes";
private static final String ARTISTS_STORAGE_NAME = "artists";
public RESTMusicService(SubsonicAPIClient subsonicAPIClient) { private final SubsonicAPIClient subsonicAPIClient;
private final PermanentFileStorage fileStorage;
public RESTMusicService(
final SubsonicAPIClient subsonicAPIClient,
final PermanentFileStorage fileStorage
) {
this.subsonicAPIClient = subsonicAPIClient; this.subsonicAPIClient = subsonicAPIClient;
this.fileStorage = fileStorage;
} }
@Override @Override
@ -146,7 +156,8 @@ public class RESTMusicService implements MusicService {
public List<MusicFolder> getMusicFolders(boolean refresh, public List<MusicFolder> getMusicFolders(boolean refresh,
Context context, Context context,
ProgressListener progressListener) throws Exception { ProgressListener progressListener) throws Exception {
List<MusicFolder> cachedMusicFolders = readCachedMusicFolders(context); List<MusicFolder> cachedMusicFolders = fileStorage.load(MUSIC_FOLDER_STORAGE_NAME,
DomainSerializers.getMusicFolderListSerializer());
if (cachedMusicFolders != null && !refresh) { if (cachedMusicFolders != null && !refresh) {
return cachedMusicFolders; return cachedMusicFolders;
} }
@ -157,31 +168,18 @@ public class RESTMusicService implements MusicService {
List<MusicFolder> musicFolders = APIMusicFolderConverter List<MusicFolder> musicFolders = APIMusicFolderConverter
.toDomainEntityList(response.body().getMusicFolders()); .toDomainEntityList(response.body().getMusicFolders());
writeCachedMusicFolders(context, musicFolders); fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders,
DomainSerializers.getMusicFolderListSerializer());
return musicFolders; return musicFolders;
} }
private static List<MusicFolder> readCachedMusicFolders(Context context) {
String filename = getCachedMusicFoldersFilename(context);
return FileUtil.deserialize(context, filename);
}
private static void writeCachedMusicFolders(Context context, List<MusicFolder> musicFolders) {
String filename = getCachedMusicFoldersFilename(context);
FileUtil.serialize(context, new ArrayList<>(musicFolders), filename);
}
private static String getCachedMusicFoldersFilename(Context context) {
String s = Util.getRestUrl(context, null);
return String.format(Locale.US, "musicFolders-%d.ser", Math.abs(s.hashCode()));
}
@Override @Override
public Indexes getIndexes(String musicFolderId, public Indexes getIndexes(String musicFolderId,
boolean refresh, boolean refresh,
Context context, Context context,
ProgressListener progressListener) throws Exception { ProgressListener progressListener) throws Exception {
Indexes cachedIndexes = readCachedIndexes(context, musicFolderId); Indexes cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME,
DomainSerializers.getIndexesSerializer());
if (cachedIndexes != null && !refresh) { if (cachedIndexes != null && !refresh) {
return cachedIndexes; return cachedIndexes;
} }
@ -192,59 +190,30 @@ public class RESTMusicService implements MusicService {
checkResponseSuccessful(response); checkResponseSuccessful(response);
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes()); Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
writeCachedIndexes(context, indexes, musicFolderId); fileStorage.store(INDEXES_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
return indexes; return indexes;
} }
private static Indexes readCachedIndexes(Context context, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
return FileUtil.deserialize(context, filename);
}
private static void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
FileUtil.serialize(context, indexes, filename);
}
private static String getCachedIndexesFilename(Context context, String musicFolderId) {
String s = Util.getRestUrl(context, null) + musicFolderId;
return String.format(Locale.US, "indexes-%d.ser", Math.abs(s.hashCode()));
}
@Override @Override
public Indexes getArtists(boolean refresh, public Indexes getArtists(boolean refresh,
Context context, Context context,
ProgressListener progressListener) throws Exception { ProgressListener progressListener) throws Exception {
Indexes cachedArtists = readCachedArtists(context); Indexes cachedArtists = fileStorage
if (cachedArtists != null && .load(ARTISTS_STORAGE_NAME, DomainSerializers.getIndexesSerializer());
!refresh) { if (cachedArtists != null && !refresh) {
return cachedArtists; return cachedArtists;
} }
updateProgressListener(progressListener, R.string.parser_reading); updateProgressListener(progressListener, R.string.parser_reading);
Response<GetArtistsResponse> response = subsonicAPIClient.getApi().getArtists(null).execute(); Response<GetArtistsResponse> response = subsonicAPIClient.getApi()
.getArtists(null).execute();
checkResponseSuccessful(response); checkResponseSuccessful(response);
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes()); Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
writeCachedArtists(context, indexes); fileStorage.store(ARTISTS_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
return indexes; return indexes;
} }
private static Indexes readCachedArtists(Context context) {
String filename = getCachedArtistsFilename(context);
return FileUtil.deserialize(context, filename);
}
private static void writeCachedArtists(Context context, Indexes artists) {
String filename = getCachedArtistsFilename(context);
FileUtil.serialize(context, artists, filename);
}
private static String getCachedArtistsFilename(Context context) {
String s = Util.getRestUrl(context, null);
return String.format(Locale.US, "indexes-%d.ser", Math.abs(s.hashCode()));
}
@Override @Override
public void star(String id, public void star(String id,
String albumId, String albumId,

View File

@ -26,7 +26,6 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import java.io.File; import java.io.File;
@ -300,11 +299,6 @@ public class FileUtil
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
} }
public static File getArtistDirectory(Context context, Artist artist)
{
return new File(String.format("%s/%s", getMusicDirectory(context).getPath(), fileSystemSafe(artist.getName())));
}
public static File getAlbumArtDirectory() public static File getAlbumArtDirectory()
{ {
File albumArtDir = new File(getUltraSonicDirectory(), "artwork"); File albumArtDir = new File(getUltraSonicDirectory(), "artwork");