Refactor and additions to support play queue snapshotting, and the

ability to transfer playback context seamlessly between server and
device.
This commit is contained in:
casey langen 2018-01-21 23:38:45 -08:00
parent 0b3e1b2f19
commit 5b356c4991
22 changed files with 618 additions and 375 deletions

View File

@ -3,13 +3,12 @@ package io.casey.musikcube.remote.injection
import android.content.Context
import dagger.Component
import io.casey.musikcube.remote.service.playback.impl.streaming.db.OfflineDb
import io.casey.musikcube.remote.service.websocket.model.impl.remote.IdListTrackListQueryFactory
@DataScope
@Component(
dependencies = arrayOf(AppComponent::class),
modules = arrayOf(DataModule::class))
@Component(dependencies = arrayOf(AppComponent::class))
interface DataComponent {
fun inject(db: OfflineDb)
fun inject(slidingWindow: IdListTrackListQueryFactory)
fun context(): Context /* via AppComponent */
}

View File

@ -1,7 +1,7 @@
package io.casey.musikcube.remote.service.playback
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
interface IPlaybackService {
fun connect(listener: () -> Unit)
@ -12,6 +12,8 @@ interface IPlaybackService {
fun play(category: String, categoryId: Long, index: Int = 0, filter: String = "")
fun playAt(index: Int)
fun playFrom(service: IPlaybackService)
fun pauseOrResume()
fun pause()
fun resume()
@ -45,7 +47,8 @@ interface IPlaybackService {
fun toggleRepeatMode()
val repeatMode: RepeatMode
val playlistQueryFactory: TrackListSlidingWindow.QueryFactory
val playlistQueryFactory: ITrackListQueryFactory
val queryContext: QueryContext?
val playingTrack: ITrack
}

View File

@ -0,0 +1,25 @@
package io.casey.musikcube.remote.service.playback
import io.casey.musikcube.remote.service.websocket.Messages
class QueryContext {
val category: String?
val categoryId: Long
val filter: String
val type: Messages.Request?
constructor(type: Messages.Request? = null)
: this("", type)
constructor(filter: String, type: Messages.Request? = null)
: this("", -1L, filter, type)
constructor(category: String, categoryId: Long, filter: String, type: Messages.Request? = null) {
this.category = category
this.categoryId = categoryId
this.filter = filter
this.type = type
}
fun hasCategory(): Boolean = (category?.isNotBlank() ?: false && (categoryId >= 0))
}

View File

@ -15,6 +15,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.util.Util
import io.casey.musikcube.remote.Application
@ -25,9 +26,7 @@ import io.casey.musikcube.remote.util.Preconditions
import java.io.File
class GaplessExoPlayerWrapper : PlayerWrapper() {
private var sourceFactory: DataSource.Factory = DefaultHttpDataSourceFactory(
Util.getUserAgent(context, "musikdroid"), null, TIMEOUT, TIMEOUT, true)
private var sourceFactory: DataSource.Factory
private val extractorsFactory = DefaultExtractorsFactory()
private var source: MediaSource? = null
private var metadata: ITrack? = null
@ -40,6 +39,12 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
private var initialOffsetMs: Int = 0
init {
val userAgent = Util.getUserAgent(context, "musikdroid")
val httpFactory: DataSource.Factory = DefaultHttpDataSourceFactory(
userAgent, null, TIMEOUT, TIMEOUT, true)
this.sourceFactory = DefaultDataSourceFactory(context, null, httpFactory)
this.transcoding = prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, 0) != 0
}
@ -58,7 +63,7 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
this.source = ExtractorMediaSource(Uri.parse(proxyUri), sourceFactory, extractorsFactory, null, null)
addPlayer(this, this.source!!, playNow = true)
addPlayer(this, this.source!!)
state = State.Preparing
}
@ -126,7 +131,7 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
if (gaplessPlayer?.playbackState != ExoPlayer.STATE_IDLE) {
if (gaplessPlayer?.isCurrentWindowSeekable == true) {
var offset = millis.toLong()
val isInitialSeek = initialOffsetMs > 0 && (position == initialOffsetMs)
val isInitialSeek = initialOffsetMs > 0 && (millis == initialOffsetMs)
/* if we're transcoding we don't want to seek arbitrarily because it may put
a lot of pressure on the backend. just allow seeking up to what we currently
@ -242,6 +247,9 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
gaplessPlayer?.seekTo(lastPosition)
lastPosition = -1
}
else if (initialOffsetMs > 0) {
position = initialOffsetMs
}
if (!prefetch) {
gaplessPlayer?.playWhenReady = true
@ -334,7 +342,7 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
}
}
private fun addPlayer(wrapper: GaplessExoPlayerWrapper, source: MediaSource, playNow: Boolean = false) {
private fun addPlayer(wrapper: GaplessExoPlayerWrapper, source: MediaSource) {
addActivePlayer(wrapper)
if (all.size == 0) {
@ -344,10 +352,6 @@ class GaplessExoPlayerWrapper : PlayerWrapper() {
dcms.addMediaSource(source)
all.add(wrapper)
if (playNow) {
gaplessPlayer?.playWhenReady = true
}
if (dcms.size == 1) {
gaplessPlayer?.prepare(dcms)
}

View File

@ -5,6 +5,7 @@ import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.injection.DaggerServiceComponent
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.QueryContext
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.SocketMessage
@ -12,7 +13,7 @@ import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.service.websocket.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
import io.reactivex.Observable
import org.json.JSONObject
import java.util.*
@ -21,16 +22,16 @@ import javax.inject.Inject
class RemotePlaybackService : IPlaybackService {
private interface Key {
companion object {
val STATE = "state"
val REPEAT_MODE = "repeat_mode"
val VOLUME = "volume"
val SHUFFLED = "shuffled"
val MUTED = "muted"
val PLAY_QUEUE_COUNT = "track_count"
val PLAY_QUEUE_POSITION = "play_queue_position"
val PLAYING_DURATION = "playing_duration"
val PLAYING_CURRENT_TIME = "playing_current_time"
val PLAYING_TRACK = "playing_track"
const val STATE = "state"
const val REPEAT_MODE = "repeat_mode"
const val VOLUME = "volume"
const val SHUFFLED = "shuffled"
const val MUTED = "muted"
const val PLAY_QUEUE_COUNT = "track_count"
const val PLAY_QUEUE_POSITION = "play_queue_position"
const val PLAYING_DURATION = "playing_duration"
const val PLAYING_CURRENT_TIME = "playing_current_time"
const val PLAYING_TRACK = "playing_track"
}
}
@ -175,6 +176,35 @@ class RemotePlaybackService : IPlaybackService {
.build())
}
override fun playFrom(service: IPlaybackService) {
service.queryContext?.let {qc ->
val time = service.currentTime
val index = service.queuePosition
when (qc.type) {
Messages.Request.PlaySnapshotTracks -> {
wss.send(SocketMessage.Builder
.request(Messages.Request.PlaySnapshotTracks)
.addOption(Messages.Key.TIME, time)
.addOption(Messages.Key.INDEX, index)
.build())
}
Messages.Request.QueryTracks,
Messages.Request.QueryTracksByCategory -> {
wss.send(SocketMessage.Builder
.request(Messages.Request.PlayTracksByCategory)
.addOption(Messages.Key.CATEGORY, qc.category)
.addOption(Messages.Key.ID, qc.categoryId)
.addOption(Messages.Key.FILTER, qc.filter)
.addOption(Messages.Key.TIME, time)
.addOption(Messages.Key.INDEX, index)
.build())
}
else -> { }
}
}
}
override fun prev() {
wss.send(SocketMessage.Builder.request(Messages.Request.Previous).build())
}
@ -371,7 +401,10 @@ class RemotePlaybackService : IPlaybackService {
}
}
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
override val queryContext: QueryContext?
get() = QueryContext(Messages.Request.QueryPlayQueueTracks)
override val playlistQueryFactory: ITrackListQueryFactory = object : ITrackListQueryFactory {
override fun count(): Observable<Int> = dataProvider.getPlayQueueTracksCount()
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> = dataProvider.getPlayQueueTracks(limit, offset)
override fun offline(): Boolean = false

View File

@ -12,23 +12,20 @@ import android.util.Log
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.injection.DaggerServiceComponent
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.PlayerWrapper
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.service.playback.*
import io.casey.musikcube.remote.service.system.SystemService
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
import io.casey.musikcube.remote.service.websocket.model.PlayQueueType
import io.casey.musikcube.remote.service.websocket.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.util.Strings
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.subscribeBy
import org.json.JSONObject
import java.net.URLEncoder
import java.util.*
import javax.inject.Inject
@ -37,7 +34,6 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
private val prefs: SharedPreferences = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
private val listeners = HashSet<() -> Unit>()
private var params: QueueParams? = null
private var playContext = PlaybackContext()
private var audioManager: AudioManager? = null
private var lastSystemVolume: Int = 0
@ -120,24 +116,6 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
}
private class QueueParams {
internal val category: String?
internal val categoryId: Long
internal val filter: String
constructor(filter: String) {
this.filter = filter
this.categoryId = -1
this.category = null
}
constructor(category: String, categoryId: Long, filter: String) {
this.category = category
this.categoryId = categoryId
this.filter = filter
}
}
init {
DaggerServiceComponent.builder()
.appComponent(Application.appComponent)
@ -172,22 +150,62 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
override fun playAll(index: Int, filter: String) {
if (requestAudioFocus()) {
trackMetadataCache.clear()
loadQueueAndPlay(QueueParams(filter), index)
resetPlayContextAndQueryFactory()
val type = Messages.Request.QueryTracks
loadQueueAndPlay(QueryContext(filter, type), index)
}
}
override fun play(category: String, categoryId: Long, index: Int, filter: String) {
if (requestAudioFocus()) {
trackMetadataCache.clear()
loadQueueAndPlay(QueueParams(category, categoryId, filter), index)
resetPlayContextAndQueryFactory()
val type = Messages.Request.QueryTracksByCategory
loadQueueAndPlay(QueryContext(category, categoryId, filter, type), index)
}
}
override fun playAt(index: Int) {
if (params != null) {
if (queryContext != null) {
if (requestAudioFocus()) {
playContext.stopPlaybackAndReset()
loadQueueAndPlay(params!!, index)
resetPlayContextAndQueryFactory()
loadQueueAndPlay(queryContext!!, index)
}
}
}
override fun playFrom(service: IPlaybackService) {
/* we only support switching from a play queue context! */
if (service.queryContext?.type == Messages.Request.QueryPlayQueueTracks) {
val dummyListener: (() -> Unit) = { }
connect(dummyListener)
service.queryContext?.let { _ ->
dataProvider.snapshotPlayQueue().subscribeBy(
onNext = {
disconnect(dummyListener)
resetPlayContextAndQueryFactory()
val index = service.queuePosition
val offsetMs = (service.currentTime * 1000).toInt()
val context = QueryContext(Messages.Request.PlaySnapshotTracks)
val type = PlayQueueType.Snapshot
snapshotQueryFactory = object: ITrackListQueryFactory {
override fun count(): Observable<Int>? =
dataProvider.getPlayQueueTracksCount(type)
override fun page(offset: Int, limit: Int): Observable<List<ITrack>>? =
dataProvider.getPlayQueueTracks(limit, offset, type)
override fun offline(): Boolean = false
}
service.pause()
loadQueueAndPlay(context, index, offsetMs)
},
onError = {
disconnect(dummyListener)
})
}
}
}
@ -377,6 +395,11 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
return 0.0
}
private fun resetPlayContextAndQueryFactory() {
playContext.stopPlaybackAndReset()
snapshotQueryFactory = null
}
private fun pauseTransient() {
if (state !== PlaybackState.Paused) {
pausedByTransientLoss = true
@ -439,7 +462,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
private fun moveToPrevTrack() {
if (playContext.queueCount > 0) {
loadQueueAndPlay(params!!, resolvePrevIndex(
loadQueueAndPlay(queryContext!!, resolvePrevIndex(
playContext.currentIndex, playContext.queueCount))
}
}
@ -455,7 +478,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
manually (this will automatically load the current and next tracks */
val next = resolveNextIndex(index, playContext.queueCount, userInitiated)
if (next >= 0) {
loadQueueAndPlay(params!!, next)
loadQueueAndPlay(queryContext!!, next)
}
else {
stop()
@ -642,7 +665,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
private fun prefetchNextTrackMetadata() {
if (playContext.nextMetadata == null) {
val originalParams = params
val originalParams = queryContext
val nextIndex = resolveNextIndex(playContext.currentIndex, playContext.queueCount, false)
if (trackMetadataCache.containsKey(nextIndex)) {
@ -660,7 +683,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(
{ track ->
if (originalParams === params && playContext.currentIndex == currentIndex) {
if (originalParams === queryContext && playContext.currentIndex == currentIndex) {
if (playContext.nextMetadata == null) {
playContext.nextIndex = nextIndex
playContext.nextMetadata = track.firstOrNull()
@ -676,7 +699,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
}
private fun loadQueueAndPlay(newParams: QueueParams, startIndex: Int) {
private fun loadQueueAndPlay(newParams: QueryContext, startIndex: Int, offsetMs: Int = 0) {
state = PlaybackState.Buffering
cancelScheduledPausedSleep()
@ -689,7 +712,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
playContext = newPlayContext
playContext.currentIndex = startIndex
params = newParams
queryContext = newParams
val countMessage = playlistQueryFactory.count() ?: return
@ -711,7 +734,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
state = PlaybackState.Stopped
},
onComplete = {
if (this.params === newParams && playContext === newPlayContext) {
if (this.queryContext === newParams && playContext === newPlayContext) {
notifyEventListeners()
val uri = getUri(playContext.currentMetadata)
@ -719,11 +742,11 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
if (uri != null) {
playContext.currentPlayer = PlayerWrapper.newInstance(prefs)
playContext.currentPlayer?.setOnStateChangedListener(onCurrentPlayerStateChanged)
playContext.currentPlayer?.play(uri, playContext.currentMetadata!!)
playContext.currentPlayer?.play(uri, playContext.currentMetadata!!, offsetMs)
}
}
else {
Log.d(TAG, "onComplete fired, but params/context changed. discarding!")
Log.d(TAG, "onComplete fired, but queryContext/context changed. discarding!")
}
})
}
@ -738,7 +761,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
private fun precacheTrackMetadata(start: Int, count: Int) {
val originalParams = params
val originalParams = queryContext
val query = playlistQueryFactory.page(start, count)
if (query != null) {
@ -746,7 +769,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
if (originalParams === this.params) {
if (originalParams === this.queryContext) {
response.forEachIndexed { i, track ->
trackMetadataCache.put(start + i, track)
}
@ -758,11 +781,16 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
}
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
override var queryContext: QueryContext? = null
private set(value) { field = value }
var snapshotQueryFactory: ITrackListQueryFactory? = null
val defaultQueryFactory: ITrackListQueryFactory = object : ITrackListQueryFactory {
override fun count(): Observable<Int>? {
val params = params
val params = queryContext
if (params != null) {
if (Strings.notEmpty(params.category) && (params.categoryId >= 0)) {
if (params.hasCategory()) {
return dataProvider.getTrackCountByCategory(
params.category ?: "", params.categoryId, params.filter)
}
@ -774,9 +802,9 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
override fun page(offset: Int, limit: Int): Observable<List<ITrack>>? {
val params = params
val params = queryContext
if (params != null) {
if (Strings.notEmpty(params.category) && (params.categoryId >= 0)) {
if (params.hasCategory()) {
return dataProvider.getTracksByCategory(
params.category ?: "", params.categoryId, limit, offset, params.filter)
}
@ -788,10 +816,21 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
override fun offline(): Boolean {
return params?.category == Messages.Category.OFFLINE
return queryContext?.category == Messages.Category.OFFLINE
}
}
override val playlistQueryFactory: ITrackListQueryFactory = object : ITrackListQueryFactory {
override fun count(): Observable<Int>? =
snapshotQueryFactory?.count() ?: defaultQueryFactory.count()
override fun page(offset: Int, limit: Int): Observable<List<ITrack>>? =
snapshotQueryFactory?.page(offset, limit) ?: defaultQueryFactory.page(offset, limit)
override fun offline(): Boolean =
snapshotQueryFactory?.offline() ?: defaultQueryFactory.offline()
}
init {
this.audioManager = Application.instance?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
this.lastSystemVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC) ?: 0
@ -862,12 +901,12 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
companion object {
private val TAG = "StreamingPlayback"
private val REPEAT_MODE_PREF = "streaming_playback_repeat_mode"
private val PREV_TRACK_GRACE_PERIOD_MILLIS = 3500
private val MAX_TRACK_METADATA_CACHE_SIZE = 50
private val PRECACHE_METADATA_SIZE = 10
private val PAUSED_SERVICE_SLEEP_DELAY_MS = 1000 * 60 * 5 /* 5 minutes */
private val DATA_PROVIDER_DISCONNECT_DELAY_MS = 5000
private const val TAG = "StreamingPlayback"
private const val REPEAT_MODE_PREF = "streaming_playback_repeat_mode"
private const val PREV_TRACK_GRACE_PERIOD_MILLIS = 3500
private const val MAX_TRACK_METADATA_CACHE_SIZE = 50
private const val PRECACHE_METADATA_SIZE = 10
private const val PAUSED_SERVICE_SLEEP_DELAY_MS = 1000 * 60 * 5 /* 5 minutes */
private const val DATA_PROVIDER_DISCONNECT_DELAY_MS = 5000
}
}

View File

@ -20,6 +20,7 @@ class Messages {
PlayAllTracks("play_all_tracks"),
PlayTracks("play_tracks"),
PlayTracksByCategory("play_tracks_by_category"),
PlaySnapshotTracks("play_snapshot_tracks"),
QueryTracks("query_tracks"),
ListCategories("list_categories"),
QueryTracksByCategory("query_tracks_by_category"),
@ -38,7 +39,8 @@ class Messages {
SetGainSettings("set_gain_settings"),
RunIndexer("run_indexer"),
GetTransportType("get_transport_type"),
SetTransportType("set_transport_type");
SetTransportType("set_transport_type"),
SnapshotPlayQueue("snapshot_play_queue");
override fun toString(): String = rawValue
fun matches(name: String): Boolean = (rawValue == name)
@ -76,58 +78,59 @@ class Messages {
class Key {
companion object {
val CATEGORY = "category"
val CATEGORY_ID = "category_id"
val DATA = "data"
val ALL = "all"
val SELECTED = "selected"
val ID = "id"
val COUNT = "count"
val COUNT_ONLY = "count_only"
val IDS_ONLY = "ids_only"
val OFFSET = "offset"
val LIMIT = "limit"
val INDEX = "index"
val DELTA = "delta"
val POSITION = "position"
val VALUE = "value"
val FILTER = "filter"
val RELATIVE = "relative"
val PLAYING_CURRENT_TIME = "playing_current_time"
val PLAYLIST_ID = "playlist_id"
val PLAYLIST_NAME = "playlist_name"
val PREDICATE_CATEGORY = "predicate_category"
val PREDICATE_ID = "predicate_id"
val SUBQUERY = "subquery"
val TYPE = "type"
val OPTIONS = "options"
val SUCCESS = "success"
val EXTERNAL_IDS = "external_ids"
val SORT_ORDERS = "sort_orders"
val DRIVER_NAME = "driver_name"
val DEVICE_ID = "device_id"
val REPLAYGAIN_MODE = "replaygain_mode"
val PREAMP_GAIN = "preamp_gain"
const val CATEGORY = "category"
const val CATEGORY_ID = "category_id"
const val DATA = "data"
const val ALL = "all"
const val SELECTED = "selected"
const val ID = "id"
const val COUNT = "count"
const val COUNT_ONLY = "count_only"
const val IDS_ONLY = "ids_only"
const val OFFSET = "offset"
const val LIMIT = "limit"
const val INDEX = "index"
const val DELTA = "delta"
const val POSITION = "position"
const val VALUE = "value"
const val FILTER = "filter"
const val RELATIVE = "relative"
const val PLAYING_CURRENT_TIME = "playing_current_time"
const val PLAYLIST_ID = "playlist_id"
const val PLAYLIST_NAME = "playlist_name"
const val PREDICATE_CATEGORY = "predicate_category"
const val PREDICATE_ID = "predicate_id"
const val SUBQUERY = "subquery"
const val TYPE = "type"
const val TIME = "time"
const val OPTIONS = "options"
const val SUCCESS = "success"
const val EXTERNAL_IDS = "external_ids"
const val SORT_ORDERS = "sort_orders"
const val DRIVER_NAME = "driver_name"
const val DEVICE_ID = "device_id"
const val REPLAYGAIN_MODE = "replaygain_mode"
const val PREAMP_GAIN = "preamp_gain"
}
}
interface Value {
companion object {
val UP = "up"
val DOWN = "down"
val REINDEX = "reindex"
val REBUILD = "rebuild"
const val UP = "up"
const val DOWN = "down"
const val REINDEX = "reindex"
const val REBUILD = "rebuild"
}
}
interface Category {
companion object {
val OFFLINE = "offline"
val ALBUM = "album"
val ARTIST = "artist"
val ALBUM_ARTIST = "album_artist"
val GENRE = "genre"
val PLAYLISTS = "playlists"
const val OFFLINE = "offline"
const val ALBUM = "album"
const val ARTIST = "artist"
const val ALBUM_ARTIST = "album_artist"
const val GENRE = "genre"
const val PLAYLISTS = "playlists"
}
}
}

View File

@ -1,5 +1,6 @@
package io.casey.musikcube.remote.service.websocket.model
import io.casey.musikcube.remote.service.websocket.Messages
import io.reactivex.Observable
interface IDataProvider {
@ -28,9 +29,12 @@ interface IDataProvider {
fun getTracksByCategory(category: String, id: Long, filter: String = ""): Observable<List<ITrack>>
fun getTracksByCategory(category: String, id: Long, limit: Int, offset: Int, filter: String = ""): Observable<List<ITrack>>
fun getPlayQueueTracksCount(filter: String = ""): Observable<Int>
fun getPlayQueueTracks(filter: String = ""): Observable<List<ITrack>>
fun getPlayQueueTracks(limit: Int, offset: Int, filter: String = ""): Observable<List<ITrack>>
fun getPlayQueueTracksCount(type: PlayQueueType = PlayQueueType.Live): Observable<Int>
fun getPlayQueueTracks(type: PlayQueueType = PlayQueueType.Live): Observable<List<ITrack>>
fun getPlayQueueTracks(limit: Int, offset: Int, type: PlayQueueType = PlayQueueType.Live): Observable<List<ITrack>>
fun getPlayQueueTrackIds(limit: Int, offset: Int, type: PlayQueueType = PlayQueueType.Live): Observable<List<String>>
fun getPlayQueueTrackIds(type: PlayQueueType = PlayQueueType.Live): Observable<List<String>>
fun snapshotPlayQueue(): Observable<Boolean>
fun getPlaylists(): Observable<List<IPlaylist>>
@ -49,7 +53,7 @@ interface IDataProvider {
fun removeTracksFromPlaylist(playlistId: Long, externalIds: List<String>, sortOrders: List<Int>): Observable<Int>
fun listOutputDrivers(): Observable<IOutputs>
fun setDefaultOutputDriver(driverName: String, deviceId: String = "default"): Observable<Boolean>
fun setDefaultOutputDriver(driverName: String, deviceId: String = ""): Observable<Boolean>
fun getGainSettings(): Observable<IGainSettings>
fun updateGainSettings(replayGainMode: ReplayGainMode, preampGain: Float): Observable<Boolean>

View File

@ -0,0 +1,9 @@
package io.casey.musikcube.remote.service.websocket.model
import io.reactivex.Observable
interface ITrackListQueryFactory {
fun count(): Observable<Int>?
fun page(offset: Int, limit: Int): Observable<List<ITrack>>?
fun offline(): Boolean
}

View File

@ -0,0 +1,6 @@
package io.casey.musikcube.remote.service.websocket.model
enum class PlayQueueType(val rawValue: String) {
Live("live"),
Snapshot("snapshot");
}

View File

@ -0,0 +1,48 @@
package io.casey.musikcube.remote.service.websocket.model.impl.remote
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.injection.DaggerDataComponent
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
import io.reactivex.Observable
import org.json.JSONObject
import javax.inject.Inject
class IdListTrackListQueryFactory(private val idList: List<String>): ITrackListQueryFactory {
@Inject protected lateinit var dataProvider: IDataProvider
init {
DaggerDataComponent.builder()
.appComponent(Application.appComponent)
.build().inject(this)
dataProvider.attach()
}
override fun page(offset: Int, limit: Int): Observable<List<ITrack>>? {
val window = mutableSetOf<String>()
val max = Math.min(limit, idList.size)
for (i in 0 until max) {
window.add(idList[offset + i])
}
val missing = RemoteTrack(JSONObject())
return dataProvider.getTracks(window)
.flatMap{ it ->
val result = mutableListOf<ITrack>()
for (i in 0 until max) {
result.add(it[idList[offset + i]] ?: missing)
}
Observable.just(result)
}
}
override fun count(): Observable<Int> = Observable.just(idList.size)
override fun offline(): Boolean = false
fun destroy() {
dataProvider.destroy()
}
}

View File

@ -163,11 +163,11 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlayQueueTracksCount(filter: String): Observable<Int> {
override fun getPlayQueueTracksCount(type: PlayQueueType): Observable<Int> {
val message = SocketMessage.Builder
.request(Messages.Request.QueryPlayQueueTracks)
.addOption(Messages.Key.FILTER, filter)
.addOption(Messages.Key.COUNT_ONLY, true)
.addOption(Messages.Key.TYPE, type.rawValue)
.build()
return service.observe(message, client)
@ -175,13 +175,13 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlayQueueTracks(filter: String): Observable<List<ITrack>> =
getPlayQueueTracks(-1, -1, filter)
override fun getPlayQueueTracks(type: PlayQueueType): Observable<List<ITrack>> =
getPlayQueueTracks(-1, -1, type)
override fun getPlayQueueTracks(limit: Int, offset: Int, filter: String): Observable<List<ITrack>> {
override fun getPlayQueueTracks(limit: Int, offset: Int, type: PlayQueueType): Observable<List<ITrack>> {
val builder = SocketMessage.Builder
.request(Messages.Request.QueryPlayQueueTracks)
.addOption(Messages.Key.FILTER, filter)
.addOption(Messages.Key.TYPE, type.rawValue)
if (limit > 0 && offset >= 0) {
builder.addOption(Messages.Key.LIMIT, limit)
@ -194,6 +194,36 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlayQueueTrackIds(type: PlayQueueType): Observable<List<String>> =
getPlayQueueTrackIds(-1, -1, type)
override fun snapshotPlayQueue(): Observable<Boolean> {
val message = SocketMessage.Builder
.request(Messages.Request.SnapshotPlayQueue)
.build()
return service.observe(message, client)
.flatMap<Boolean> { socketMessage -> isSuccessful(socketMessage) }
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlayQueueTrackIds(limit: Int, offset: Int, type: PlayQueueType): Observable<List<String>> {
val builder = SocketMessage.Builder
.request(Messages.Request.QueryPlayQueueTracks)
.addOption(Messages.Key.IDS_ONLY, true)
.addOption(Messages.Key.TYPE, type.rawValue)
if (limit > 0 && offset >= 0) {
builder.addOption(Messages.Key.LIMIT, limit)
builder.addOption(Messages.Key.OFFSET, offset)
}
return service.observe(builder.build(), client)
.observeOn(Schedulers.computation())
.flatMap<List<String>> { socketMessage -> toStringList(socketMessage) }
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlaylists(): Observable<List<IPlaylist>> {
val message = SocketMessage.Builder
.request(Messages.Request.QueryCategory)

View File

@ -17,6 +17,7 @@ import android.widget.SeekBar
import android.widget.TextView
import com.wooplr.spotlight.SpotlightView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.service.websocket.Messages
@ -118,8 +119,11 @@ class MainActivity : BaseActivity() {
menu.findItem(R.id.action_categories).isEnabled = connected
menu.findItem(R.id.action_remote_manage).isEnabled = connected
menu.findItem(R.id.action_remote_toggle).setIcon(
if (streaming) R.drawable.ic_toolbar_streaming else R.drawable.ic_toolbar_remote)
val remoteToggle = menu.findItem(R.id.action_remote_toggle)
remoteToggle.setIcon(
if (streaming) R.drawable.ic_toolbar_streaming
else R.drawable.ic_toolbar_remote)
return super.onPrepareOptionsMenu(menu)
}
@ -204,22 +208,32 @@ class MainActivity : BaseActivity() {
Prefs.Key.STREAMING_PLAYBACK,
Prefs.Default.STREAMING_PLAYBACK)
private fun togglePlaybackService() {
val streaming = isStreamingSelected
private fun togglePlaybackService(transfer: Boolean = false) {
val isStreaming = isStreamingSelected
prefs.edit().putBoolean(Prefs.Key.STREAMING_PLAYBACK, !isStreaming)?.apply()
if (streaming) {
playback.service.stop()
}
prefs.edit().putBoolean(Prefs.Key.STREAMING_PLAYBACK, !streaming)?.apply()
val messageId = if (streaming)
val messageId = if (isStreaming)
R.string.snackbar_remote_enabled
else
R.string.snackbar_streaming_enabled
showSnackbar(mainLayout, messageId)
if (transfer) {
val streaming = PlaybackServiceFactory.streaming(this)
val remote = PlaybackServiceFactory.remote(this)
if (!isStreaming) {
streaming.playFrom(remote)
} else {
remote.playFrom(streaming)
}
}
if (isStreaming) {
playback.service.stop()
}
playback.reload()
invalidateOptionsMenu()

View File

@ -141,6 +141,7 @@ class MainMetadataView : FrameLayout {
if (Strings.empty(artist) || Strings.empty(album)) {
setMetadataDisplayMode(DisplayMode.NoArtwork)
loadedAlbumArtUrl = null
}
else {
val newUrl = getAlbumArtUrl(playing, Size.Mega) ?: ""

View File

@ -18,7 +18,8 @@ import io.casey.musikcube.remote.ui.shared.extension.setupDefaultRecyclerView
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.ITrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.DefaultSlidingWindow
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.reactivex.rxkotlin.subscribeBy
@ -26,7 +27,7 @@ class PlayQueueActivity : BaseActivity() {
private var offlineQueue: Boolean = false
private lateinit var data: DataProviderMixin
private lateinit var playback: PlaybackMixin
private lateinit var tracks: TrackListSlidingWindow
private lateinit var tracks: DefaultSlidingWindow
private lateinit var adapter: PlayQueueAdapter
private lateinit var emptyView: EmptyListView
@ -45,7 +46,7 @@ class PlayQueueActivity : BaseActivity() {
offlineQueue = playback.service.playlistQueryFactory.offline()
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
tracks = DefaultSlidingWindow(recyclerView, data.provider, queryFactory)
tracks.setInitialPosition(intent.getIntExtra(EXTRA_PLAYING_INDEX, -1))
tracks.setOnMetadataLoadedListener(slidingWindowListener)
adapter = PlayQueueAdapter(tracks, playback, adapterListener)
@ -120,7 +121,7 @@ class PlayQueueActivity : BaseActivity() {
adapter.notifyDataSetChanged()
}
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
private val slidingWindowListener = object : ITrackListSlidingWindow.OnMetadataLoadedListener {
override fun onReloaded(count: Int) =
emptyView.update(data.provider.state, count)
@ -128,7 +129,7 @@ class PlayQueueActivity : BaseActivity() {
}
companion object {
private val EXTRA_PLAYING_INDEX = "extra_playing_index"
private const val EXTRA_PLAYING_INDEX = "extra_playing_index"
fun getStartIntent(context: Context, playingIndex: Int): Intent {
return Intent(context, PlayQueueActivity::class.java)

View File

@ -10,9 +10,9 @@ import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.DefaultSlidingWindow
class PlayQueueAdapter(val tracks: TrackListSlidingWindow,
class PlayQueueAdapter(val tracks: DefaultSlidingWindow,
val playback: PlaybackMixin,
val listener: EventListener): RecyclerView.Adapter<PlayQueueAdapter.ViewHolder>()
{

View File

@ -0,0 +1,86 @@
package io.casey.musikcube.remote.ui.shared.model
import android.support.v7.widget.RecyclerView
import com.simplecityapps.recyclerview_fastscroll.interfaces.OnFastScrollStateChangeListener
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.reactivex.disposables.CompositeDisposable
abstract class BaseSlidingWindow(
private val recyclerView: FastScrollRecyclerView,
private val dataProvider: IDataProvider) : ITrackListSlidingWindow
{
private var scrollState = RecyclerView.SCROLL_STATE_IDLE
private var fastScrollerActive = false
protected var disposables = CompositeDisposable()
protected var loadedListener: ITrackListSlidingWindow.OnMetadataLoadedListener? = null
protected var connected = false
protected class CacheEntry {
internal var value: ITrack? = null
internal var dirty: Boolean = false
}
final override var count = 0
protected set(count) {
field = count
invalidate()
notifyAdapterChanged()
notifyMetadataLoaded(0, 0)
}
override fun pause() {
connected = false
recyclerView.removeOnScrollListener(recyclerViewScrollListener)
disposables.dispose()
disposables = CompositeDisposable()
}
override fun resume() {
disposables.add(dataProvider.observePlayQueue()
.subscribe({ requery() }, { /* error */ }))
recyclerView.setStateChangeListener(fastScrollStateChangeListener)
recyclerView.addOnScrollListener(recyclerViewScrollListener)
connected = true
fastScrollerActive = false
}
override fun setOnMetadataLoadedListener(loadedListener: ITrackListSlidingWindow.OnMetadataLoadedListener) {
this.loadedListener = loadedListener
}
protected abstract fun invalidate()
protected abstract fun getPageAround(index: Int)
protected fun notifyAdapterChanged() =
recyclerView.adapter.notifyDataSetChanged()
protected fun notifyMetadataLoaded(offset: Int, count: Int) =
loadedListener?.onMetadataLoaded(offset, count)
protected fun scrolling(): Boolean =
scrollState != RecyclerView.SCROLL_STATE_IDLE || fastScrollerActive
private val recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
scrollState = newState
if (!scrolling()) {
notifyAdapterChanged()
}
}
}
private val fastScrollStateChangeListener = object: OnFastScrollStateChangeListener {
override fun onFastScrollStop() {
fastScrollerActive = false
requery()
}
override fun onFastScrollStart() {
fastScrollerActive = true
}
}
}

View File

@ -0,0 +1,132 @@
package io.casey.musikcube.remote.ui.shared.model
import android.util.Log
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
import io.reactivex.rxkotlin.subscribeBy
class DefaultSlidingWindow(
private val recyclerView: FastScrollRecyclerView,
dataProvider: IDataProvider,
private val queryFactory: ITrackListQueryFactory)
: BaseSlidingWindow(recyclerView, dataProvider)
{
private var queryOffset = -1
private var queryLimit = -1
private var initialPosition = -1
private var windowSize = DEFAULT_WINDOW_SIZE
private val cache = object : LinkedHashMap<Int, CacheEntry>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, CacheEntry>): Boolean = size >= MAX_SIZE
}
override fun requery() {
if (queryFactory.offline() || connected) {
cancelMessages()
var queried = false
val countObservable = queryFactory.count()
if (countObservable != null) {
countObservable.subscribeBy(
onNext = { newCount ->
count = newCount
if (initialPosition != -1) {
recyclerView.scrollToPosition(initialPosition)
initialPosition = -1
}
loadedListener?.onReloaded(count)
},
onError = { _ ->
Log.d("DefaultSlidingWindow", "message send failed, likely canceled")
})
queried = true
}
if (!queried) {
count = 0
loadedListener?.onReloaded(0)
}
}
}
override fun getTrack(index: Int): ITrack? {
val track = cache[index]
if (track == null || track.dirty) {
if (!scrolling()) {
getPageAround(index)
}
}
return track?.value
}
fun setInitialPosition(initialIndex: Int) {
initialPosition = initialIndex
}
override fun invalidate() {
cancelMessages()
for (entry in cache.values) {
entry.dirty = true
}
}
private fun cancelMessages() {
queryLimit = -1
queryOffset = queryLimit
}
override fun getPageAround(index: Int) {
if (!connected || scrolling()) {
return
}
if (index >= queryOffset && index <= queryOffset + queryLimit) {
return /* already in flight */
}
val offset = Math.max(0, index - 10) /* snag a couple before */
val limit = windowSize
val pageRequest = queryFactory.page(offset, limit)
if (pageRequest != null) {
cancelMessages()
queryOffset = offset
queryLimit = limit
pageRequest.subscribeBy(
onNext = { response ->
queryLimit = -1
queryOffset = queryLimit
var i = 0
response.forEach { track ->
val entry = CacheEntry()
entry.dirty = false
entry.value = track
cache[offset + i++] = entry
}
notifyAdapterChanged()
notifyMetadataLoaded(offset, i)
},
onError = { _ ->
Log.d("DefaultSlidingWindow", "message send failed, likely canceled")
})
}
}
companion object {
private const val MAX_SIZE = 150
private const val DEFAULT_WINDOW_SIZE = 75
}
}

View File

@ -0,0 +1,17 @@
package io.casey.musikcube.remote.ui.shared.model
import io.casey.musikcube.remote.service.websocket.model.ITrack
interface ITrackListSlidingWindow {
interface OnMetadataLoadedListener {
fun onMetadataLoaded(offset: Int, count: Int)
fun onReloaded(count: Int)
}
val count: Int
fun requery()
fun pause()
fun resume()
fun getTrack(index: Int): ITrack?
fun setOnMetadataLoadedListener(loadedListener: OnMetadataLoadedListener)
}

View File

@ -1,212 +0,0 @@
package io.casey.musikcube.remote.ui.shared.model
import android.support.v7.widget.RecyclerView
import android.util.Log
import com.simplecityapps.recyclerview_fastscroll.interfaces.OnFastScrollStateChangeListener
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.subscribeBy
class TrackListSlidingWindow(private val recyclerView: FastScrollRecyclerView,
private val dataProvider: IDataProvider,
private val queryFactory: TrackListSlidingWindow.QueryFactory)
{
private var scrollState = RecyclerView.SCROLL_STATE_IDLE
private var fastScrollerActive = false
private var disposables = CompositeDisposable()
private var queryOffset = -1
private var queryLimit = -1
private var initialPosition = -1
private var windowSize = DEFAULT_WINDOW_SIZE
private var loadedListener: OnMetadataLoadedListener? = null
internal var connected = false
private class CacheEntry {
internal var value: ITrack? = null
internal var dirty: Boolean = false
}
private val cache = object : LinkedHashMap<Int, CacheEntry>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, CacheEntry>): Boolean = size >= MAX_SIZE
}
interface OnMetadataLoadedListener {
fun onMetadataLoaded(offset: Int, count: Int)
fun onReloaded(count: Int)
}
abstract class QueryFactory {
abstract fun count(): Observable<Int>?
abstract fun page(offset: Int, limit: Int): Observable<List<ITrack>>?
abstract fun offline(): Boolean
}
var count = 0
set(count) {
field = count
invalidateCache()
cancelMessages()
notifyAdapterChanged()
notifyMetadataLoaded(0, 0)
}
private val fastScrollStateChangeListener = object: OnFastScrollStateChangeListener {
override fun onFastScrollStop() {
fastScrollerActive = false
requery()
}
override fun onFastScrollStart() {
fastScrollerActive = true
}
}
fun requery() {
if (queryFactory.offline() || connected) {
cancelMessages()
var queried = false
val countObservable = queryFactory.count()
if (countObservable != null) {
countObservable.subscribeBy(
onNext = { newCount ->
count = newCount
if (initialPosition != -1) {
recyclerView.scrollToPosition(initialPosition)
initialPosition = -1
}
loadedListener?.onReloaded(count)
},
onError = { _ ->
Log.d("TrackListSlidingWindow", "message send failed, likely canceled")
})
queried = true
}
if (!queried) {
count = 0
loadedListener?.onReloaded(0)
}
}
}
fun pause() {
connected = false
recyclerView.removeOnScrollListener(recyclerViewScrollListener)
disposables.dispose()
disposables = CompositeDisposable()
}
fun resume() {
disposables.add(dataProvider.observePlayQueue()
.subscribe({ requery() }, { /* error */ }))
recyclerView.setStateChangeListener(fastScrollStateChangeListener)
recyclerView.addOnScrollListener(recyclerViewScrollListener)
connected = true
fastScrollerActive = false
}
fun setInitialPosition(initialIndex: Int) {
initialPosition = initialIndex
}
fun setOnMetadataLoadedListener(loadedListener: OnMetadataLoadedListener) {
this.loadedListener = loadedListener
}
fun getTrack(index: Int): ITrack? {
val track = cache[index]
if (track == null || track.dirty) {
if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
getPageAround(index)
}
}
return track?.value
}
private fun invalidateCache() {
for (entry in cache.values) {
entry.dirty = true
}
}
private fun cancelMessages() {
queryLimit = -1
queryOffset = queryLimit
}
private fun getPageAround(index: Int) {
if (!connected || scrolling()) {
return
}
if (index >= queryOffset && index <= queryOffset + queryLimit) {
return /* already in flight */
}
val offset = Math.max(0, index - 10) /* snag a couple before */
val limit = windowSize
val pageRequest = queryFactory.page(offset, limit)
if (pageRequest != null) {
cancelMessages()
queryOffset = offset
queryLimit = limit
pageRequest.subscribeBy(
onNext = { response ->
queryLimit = -1
queryOffset = queryLimit
var i = 0
response.forEach { track ->
val entry = CacheEntry()
entry.dirty = false
entry.value = track
cache.put(offset + i++, entry)
}
notifyAdapterChanged()
notifyMetadataLoaded(offset, i)
},
onError = { _ ->
Log.d("TrackListSlidingWindow", "message send failed, likely canceled")
})
}
}
private fun notifyAdapterChanged() =
recyclerView.adapter.notifyDataSetChanged()
private fun notifyMetadataLoaded(offset: Int, count: Int) =
loadedListener?.onMetadataLoaded(offset, count)
private fun scrolling(): Boolean =
scrollState != RecyclerView.SCROLL_STATE_IDLE || fastScrollerActive
private val recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
scrollState = newState
if (!scrolling()) {
notifyAdapterChanged()
}
}
}
companion object {
private val MAX_SIZE = 150
val DEFAULT_WINDOW_SIZE = 75
}
}

View File

@ -19,8 +19,9 @@ import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow.QueryFactory
import io.casey.musikcube.remote.ui.shared.model.ITrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.DefaultSlidingWindow
import io.casey.musikcube.remote.service.websocket.model.ITrackListQueryFactory
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.ui.shared.view.EmptyListView.Capability
import io.casey.musikcube.remote.ui.tracks.adapter.TrackListAdapter
@ -30,7 +31,7 @@ import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
class TrackListActivity : BaseActivity(), Filterable {
private lateinit var tracks: TrackListSlidingWindow
private lateinit var tracks: DefaultSlidingWindow
private lateinit var emptyView: EmptyListView
private lateinit var transport: TransportFragment
private lateinit var adapter: TrackListAdapter
@ -66,7 +67,7 @@ class TrackListActivity : BaseActivity(), Filterable {
val queryFactory = createCategoryQueryFactory(categoryType, categoryId)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
tracks = DefaultSlidingWindow(recyclerView, data.provider, queryFactory)
adapter = TrackListAdapter(tracks, eventListener, playback)
setupDefaultRecyclerView(recyclerView, adapter)
@ -209,10 +210,10 @@ class TrackListActivity : BaseActivity(), Filterable {
}
}
private fun createCategoryQueryFactory(categoryType: String?, categoryId: Long): QueryFactory {
private fun createCategoryQueryFactory(categoryType: String?, categoryId: Long): ITrackListQueryFactory {
if (isValidCategory(categoryType, categoryId)) {
/* tracks for a specified category (album, artists, genres, etc */
return object : QueryFactory() {
return object : ITrackListQueryFactory {
override fun count(): Observable<Int> =
data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
@ -225,7 +226,7 @@ class TrackListActivity : BaseActivity(), Filterable {
}
else {
/* all tracks */
return object : QueryFactory() {
return object : ITrackListQueryFactory {
override fun count(): Observable<Int> =
data.provider.getTrackCount(lastFilter)
@ -238,7 +239,7 @@ class TrackListActivity : BaseActivity(), Filterable {
}
}
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
private val slidingWindowListener = object : ITrackListSlidingWindow.OnMetadataLoadedListener {
override fun onReloaded(count: Int) {
emptyView.update(data.provider.state, count)
}

View File

@ -10,9 +10,9 @@ import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.DefaultSlidingWindow
class TrackListAdapter(private val tracks: TrackListSlidingWindow,
class TrackListAdapter(private val tracks: DefaultSlidingWindow,
private val listener: EventListener?,
private var playback: PlaybackMixin) : RecyclerView.Adapter<TrackListAdapter.ViewHolder>()
{