mirror of
https://github.com/clangen/musikcube.git
synced 2024-10-02 13:02:35 +00:00
First chunk of work required to support offline playback.
This commit is contained in:
parent
5222418123
commit
3bce961cb5
@ -30,6 +30,7 @@ android {
|
|||||||
repositories {
|
repositories {
|
||||||
flatDir { dirs 'libs' }
|
flatDir { dirs 'libs' }
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
|
maven { url 'https://maven.google.com' }
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +46,9 @@ dependencies {
|
|||||||
compile(name:'videocache-2.8.0-pre', ext:'aar')
|
compile(name:'videocache-2.8.0-pre', ext:'aar')
|
||||||
compile 'org.slf4j:slf4j-android:1.7.21'
|
compile 'org.slf4j:slf4j-android:1.7.21'
|
||||||
|
|
||||||
|
compile "android.arch.persistence.room:runtime:1.0.0-alpha2"
|
||||||
|
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha2"
|
||||||
|
|
||||||
compile 'com.neovisionaries:nv-websocket-client:1.31'
|
compile 'com.neovisionaries:nv-websocket-client:1.31'
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.8.0'
|
compile 'com.squareup.okhttp3:okhttp:3.8.0'
|
||||||
compile 'com.github.bumptech.glide:glide:3.8.0'
|
compile 'com.github.bumptech.glide:glide:3.8.0'
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package io.casey.musikcube.remote;
|
|
||||||
|
|
||||||
import com.facebook.stetho.Stetho;
|
|
||||||
|
|
||||||
import io.casey.musikcube.remote.playback.StreamProxy;
|
|
||||||
import io.casey.musikcube.remote.util.NetworkUtil;
|
|
||||||
|
|
||||||
public class Application extends android.app.Application {
|
|
||||||
private static Application instance;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
instance = this;
|
|
||||||
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Stetho.initializeWithDefaults(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkUtil.init();
|
|
||||||
StreamProxy.init(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Application getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package io.casey.musikcube.remote
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Room
|
||||||
|
import android.content.Context
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
|
import io.casey.musikcube.remote.offline.OfflineDb
|
||||||
|
import io.casey.musikcube.remote.playback.StreamProxy
|
||||||
|
import io.casey.musikcube.remote.util.NetworkUtil
|
||||||
|
|
||||||
|
class Application : android.app.Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Stetho.initializeWithDefaults(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkUtil.init()
|
||||||
|
StreamProxy.init(this)
|
||||||
|
|
||||||
|
offlineDb = Room.databaseBuilder(
|
||||||
|
applicationContext,
|
||||||
|
OfflineDb::class.java,
|
||||||
|
"offline").build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var instance: Context? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
var offlineDb: OfflineDb? = null
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
@ -123,6 +123,8 @@ public class MainActivity extends WebSocketActivityBase {
|
|||||||
menu.findItem(R.id.action_remote_toggle).setIcon(
|
menu.findItem(R.id.action_remote_toggle).setIcon(
|
||||||
streaming ? R.mipmap.ic_toolbar_streaming : R.mipmap.ic_toolbar_remote);
|
streaming ? R.mipmap.ic_toolbar_streaming : R.mipmap.ic_toolbar_remote);
|
||||||
|
|
||||||
|
menu.findItem(R.id.action_offline_tracks).setEnabled(streaming);
|
||||||
|
|
||||||
return super.onPrepareOptionsMenu(menu);
|
return super.onPrepareOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +147,10 @@ public class MainActivity extends WebSocketActivityBase {
|
|||||||
startActivity(CategoryBrowseActivity.getStartIntent(
|
startActivity(CategoryBrowseActivity.getStartIntent(
|
||||||
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.DeepLink.TRACKS));
|
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.DeepLink.TRACKS));
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case R.id.action_offline_tracks:
|
||||||
|
startActivity(TrackListActivity.getOfflineStartIntent(this));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
@ -295,18 +301,19 @@ public class MainActivity extends WebSocketActivityBase {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final PlaybackState playbackState = playback.getPlaybackState();
|
||||||
final boolean streaming = prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
|
final boolean streaming = prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
|
||||||
final boolean connected = (wss.getState() == WebSocketService.State.Connected);
|
final boolean connected = (wss.getState() == WebSocketService.State.Connected);
|
||||||
final boolean stopped = (playback.getPlaybackState() == PlaybackState.Stopped);
|
final boolean stopped = (playbackState == PlaybackState.Stopped);
|
||||||
final boolean playing = (playback.getPlaybackState() == PlaybackState.Playing);
|
final boolean playing = (playbackState == PlaybackState.Playing);
|
||||||
final boolean buffering = (playback.getPlaybackState() == PlaybackState.Buffering);
|
final boolean buffering = (playbackState == PlaybackState.Buffering);
|
||||||
final boolean showMetadataView = !stopped && connected && playback.getQueueCount() > 0;
|
final boolean showMetadataView = !stopped && /*connected &&*/ playback.getQueueCount() > 0;
|
||||||
|
|
||||||
/* bottom section: transport controls */
|
/* bottom section: transport controls */
|
||||||
this.playPause.setText(playing || buffering ? R.string.button_pause : R.string.button_play);
|
this.playPause.setText(playing || buffering ? R.string.button_pause : R.string.button_play);
|
||||||
|
|
||||||
this.connectedNotPlaying.setVisibility((connected && stopped) ? View.VISIBLE : View.GONE);
|
this.connectedNotPlaying.setVisibility((connected && stopped) ? View.VISIBLE : View.GONE);
|
||||||
this.disconnectedOverlay.setVisibility(connected ? View.GONE : View.VISIBLE);
|
this.disconnectedOverlay.setVisibility(connected || !stopped ? View.GONE : View.VISIBLE);
|
||||||
|
|
||||||
final RepeatMode repeatMode = playback.getRepeatMode();
|
final RepeatMode repeatMode = playback.getRepeatMode();
|
||||||
final boolean repeatChecked = (repeatMode != RepeatMode.None);
|
final boolean repeatChecked = (repeatMode != RepeatMode.None);
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
package io.casey.musikcube.remote.offline;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Database;
|
||||||
|
import android.arch.persistence.room.RoomDatabase;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.casey.musikcube.remote.Application;
|
||||||
|
import io.casey.musikcube.remote.playback.StreamProxy;
|
||||||
|
import io.casey.musikcube.remote.websocket.Messages;
|
||||||
|
import io.casey.musikcube.remote.websocket.SocketMessage;
|
||||||
|
import io.casey.musikcube.remote.websocket.WebSocketService;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
@Database(entities = {OfflineTrack.class}, version = 1)
|
||||||
|
public abstract class OfflineDb extends RoomDatabase {
|
||||||
|
public OfflineDb() {
|
||||||
|
WebSocketService.getInstance(Application.Companion.getInstance())
|
||||||
|
.addInterceptor((message, responder) -> {
|
||||||
|
if (Messages.Request.QueryTracksByCategory.is(message.getName())) {
|
||||||
|
final String category = message.getStringOption(Messages.Key.CATEGORY);
|
||||||
|
if (Messages.Category.OFFLINE.equals(category)) {
|
||||||
|
queryTracks(message, responder);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
prune();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract OfflineTrackDao trackDao();
|
||||||
|
|
||||||
|
public void prune() {
|
||||||
|
Single.fromCallable(() -> {
|
||||||
|
List<String> uris = trackDao().queryUris();
|
||||||
|
List<String> toDelete = new ArrayList<>();
|
||||||
|
|
||||||
|
for (final String uri : uris) {
|
||||||
|
if (!StreamProxy.isCached(uri)) {
|
||||||
|
toDelete.add(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toDelete.size() > 0) {
|
||||||
|
trackDao().deleteByUri(toDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryTracks(final SocketMessage message, final WebSocketService.Responder responder) {
|
||||||
|
Single.fromCallable(() -> {
|
||||||
|
final OfflineTrackDao dao = trackDao();
|
||||||
|
|
||||||
|
final boolean countOnly = message.getBooleanOption(Messages.Key.COUNT_ONLY, false);
|
||||||
|
|
||||||
|
final JSONArray tracks = new JSONArray();
|
||||||
|
final JSONObject options = new JSONObject();
|
||||||
|
|
||||||
|
if (countOnly) {
|
||||||
|
options.put(Messages.Key.COUNT, dao.countTracks());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
final int offset = message.getIntOption(Messages.Key.OFFSET, -1);
|
||||||
|
final int limit = message.getIntOption(Messages.Key.LIMIT, -1);
|
||||||
|
|
||||||
|
final List<OfflineTrack> offlineTracks = (offset == -1 || limit == -1)
|
||||||
|
? dao.queryTracks() : dao.queryTracks(limit, offset);
|
||||||
|
|
||||||
|
for (final OfflineTrack track : offlineTracks) {
|
||||||
|
tracks.put(track.toJSONObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
options.put(Messages.Key.OFFSET, offset);
|
||||||
|
options.put(Messages.Key.LIMIT, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.put(Messages.Key.DATA, tracks);
|
||||||
|
|
||||||
|
responder.respond(SocketMessage.Builder
|
||||||
|
.respondTo(message).withOptions(options).build());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package io.casey.musikcube.remote.offline;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import io.casey.musikcube.remote.playback.Metadata;
|
||||||
|
import io.casey.musikcube.remote.util.Strings;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class OfflineTrack {
|
||||||
|
@PrimaryKey
|
||||||
|
public String externalId;
|
||||||
|
|
||||||
|
public String uri, title, album, artist, albumArtist, genre;
|
||||||
|
public long albumId, artistId, albumArtistId, genreId;
|
||||||
|
public int trackNum;
|
||||||
|
|
||||||
|
public boolean fromJSONObject(final String uri, final JSONObject from) {
|
||||||
|
if (Strings.empty(uri)) {
|
||||||
|
throw new IllegalArgumentException("uri cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String externalId = from.optString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
|
if (Strings.empty(externalId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.externalId = externalId;
|
||||||
|
this.uri = uri;
|
||||||
|
this.title = from.optString(Metadata.Track.TITLE, "");
|
||||||
|
this.album = from.optString(Metadata.Track.ALBUM, "");
|
||||||
|
this.artist = from.optString(Metadata.Track.ARTIST, "");
|
||||||
|
this.albumArtist = from.optString(Metadata.Track.ALBUM_ARTIST, "");
|
||||||
|
this.genre = from.optString(Metadata.Track.GENRE, "");
|
||||||
|
this.albumId = from.optLong(Metadata.Track.ALBUM_ID, -1L);
|
||||||
|
this.artistId = from.optLong(Metadata.Track.ARTIST_ID, -1L);
|
||||||
|
this.albumArtistId = from.optLong(Metadata.Track.ALBUM_ARTIST_ID, -1L);
|
||||||
|
this.genreId = from.optLong(Metadata.Track.GENRE_ID, -1L);
|
||||||
|
this.trackNum = from.optInt(Metadata.Track.TRACK_NUM, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject toJSONObject() {
|
||||||
|
try {
|
||||||
|
final JSONObject json = new JSONObject();
|
||||||
|
json.put(Metadata.Track.TITLE, title);
|
||||||
|
json.put(Metadata.Track.ALBUM, album);
|
||||||
|
json.put(Metadata.Track.ARTIST, artist);
|
||||||
|
json.put(Metadata.Track.ALBUM_ARTIST, albumArtist);
|
||||||
|
json.put(Metadata.Track.GENRE, genre);
|
||||||
|
json.put(Metadata.Track.ALBUM_ID, albumId);
|
||||||
|
json.put(Metadata.Track.ARTIST_ID, artistId);
|
||||||
|
json.put(Metadata.Track.ALBUM_ARTIST_ID, albumArtistId);
|
||||||
|
json.put(Metadata.Track.GENRE_ID, genreId);
|
||||||
|
json.put(Metadata.Track.TRACK_NUM, trackNum);
|
||||||
|
json.put(Metadata.Track.EXTERNAL_ID, externalId);
|
||||||
|
json.put(Metadata.Track.URI, uri);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
catch (JSONException ex) {
|
||||||
|
throw new RuntimeException("json serialization error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package io.casey.musikcube.remote.offline;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Insert;
|
||||||
|
import android.arch.persistence.room.OnConflictStrategy;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface OfflineTrackDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insertTrack(OfflineTrack... track);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM OfflineTrack " +
|
||||||
|
"ORDER BY albumArtist ASC, album ASC, trackNum ASC, TITLE ASC " +
|
||||||
|
"LIMIT :limit OFFSET :offset")
|
||||||
|
List<OfflineTrack> queryTracks(int limit, int offset);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM OfflineTrack " +
|
||||||
|
"ORDER BY albumArtist ASC, album ASC, trackNum ASC, TITLE ASC")
|
||||||
|
List<OfflineTrack> queryTracks();
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM OfflineTrack")
|
||||||
|
int countTracks();
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT uri FROM OfflineTrack")
|
||||||
|
List<String> queryUris();
|
||||||
|
|
||||||
|
@Query("DELETE FROM OfflineTrack WHERE uri IN(:uris)")
|
||||||
|
void deleteByUri(List<String> uris);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory
|
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
||||||
@ -25,6 +26,7 @@ import io.casey.musikcube.remote.websocket.Prefs
|
|||||||
import io.casey.musikcube.remote.playback.StreamProxy.*
|
import io.casey.musikcube.remote.playback.StreamProxy.*
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class ExoPlayerWrapper : PlayerWrapper() {
|
class ExoPlayerWrapper : PlayerWrapper() {
|
||||||
@ -33,12 +35,13 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
private val extractors: ExtractorsFactory
|
private val extractors: ExtractorsFactory
|
||||||
private var source: MediaSource? = null
|
private var source: MediaSource? = null
|
||||||
private val player: SimpleExoPlayer?
|
private val player: SimpleExoPlayer?
|
||||||
|
private var metadata: JSONObject? = null
|
||||||
private var prefetch: Boolean = false
|
private var prefetch: Boolean = false
|
||||||
private val context: Context
|
private val context: Context
|
||||||
private var lastPosition: Long = -1
|
private var lastPosition: Long = -1
|
||||||
private var percentAvailable = 0
|
private var percentAvailable = 0
|
||||||
private var originalUri: String? = null
|
private var originalUri: String? = null
|
||||||
private var resolvedUri: String? = null
|
private var proxyUri: String? = null
|
||||||
private val transcoding: Boolean
|
private val transcoding: Boolean
|
||||||
|
|
||||||
private fun initHttpClient(uri: String) {
|
private fun initHttpClient(uri: String) {
|
||||||
@ -88,26 +91,31 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.context = Application.getInstance()
|
this.context = Application.instance!!
|
||||||
val bandwidth = DefaultBandwidthMeter()
|
val bandwidth = DefaultBandwidthMeter()
|
||||||
val trackFactory = AdaptiveTrackSelection.Factory(bandwidth)
|
val trackFactory = AdaptiveTrackSelection.Factory(bandwidth)
|
||||||
val trackSelector = DefaultTrackSelector(trackFactory)
|
val trackSelector = DefaultTrackSelector(trackFactory)
|
||||||
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector)
|
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector)
|
||||||
this.extractors = DefaultExtractorsFactory()
|
this.extractors = DefaultExtractorsFactory()
|
||||||
this.datasources = DefaultDataSourceFactory(context, Util.getUserAgent(context, "musikdroid"))
|
this.datasources = DefaultDataSourceFactory(context, Util.getUserAgent(context, "musikdroid"))
|
||||||
this.prefs = Application.getInstance().getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
this.prefs = Application.instance!!.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
||||||
this.transcoding = this.prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, 0) != 0
|
this.transcoding = this.prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, 0) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun play(uri: String) {
|
override fun play(uri: String, metadata: JSONObject) {
|
||||||
Preconditions.throwIfNotOnMainThread()
|
Preconditions.throwIfNotOnMainThread()
|
||||||
|
|
||||||
if (!dead()) {
|
if (!dead()) {
|
||||||
initHttpClient(uri)
|
initHttpClient(uri)
|
||||||
|
|
||||||
|
this.metadata = metadata
|
||||||
this.originalUri = uri
|
this.originalUri = uri
|
||||||
this.resolvedUri = StreamProxy.getProxyUrl(context, uri)
|
this.proxyUri = StreamProxy.getProxyUrl(context, uri)
|
||||||
|
Log.d("ExoPlayerWrapper", "originalUri: ${this.originalUri} proxyUri: ${this.proxyUri}")
|
||||||
|
|
||||||
addCacheListener()
|
addCacheListener()
|
||||||
this.source = ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null)
|
|
||||||
|
this.source = ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null)
|
||||||
this.player!!.playWhenReady = true
|
this.player!!.playWhenReady = true
|
||||||
this.player.prepare(this.source)
|
this.player.prepare(this.source)
|
||||||
PlayerWrapper.addActivePlayer(this)
|
PlayerWrapper.addActivePlayer(this)
|
||||||
@ -115,16 +123,22 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prefetch(uri: String) {
|
override fun prefetch(uri: String, metadata: JSONObject) {
|
||||||
Preconditions.throwIfNotOnMainThread()
|
Preconditions.throwIfNotOnMainThread()
|
||||||
|
|
||||||
if (!dead()) {
|
if (!dead()) {
|
||||||
initHttpClient(uri)
|
initHttpClient(uri)
|
||||||
|
|
||||||
|
this.metadata = metadata
|
||||||
this.originalUri = uri
|
this.originalUri = uri
|
||||||
|
this.proxyUri = StreamProxy.getProxyUrl(context, uri)
|
||||||
|
Log.d("ExoPlayerWrapper", "originalUri: ${this.originalUri} proxyUri: ${this.proxyUri}")
|
||||||
|
|
||||||
this.prefetch = true
|
this.prefetch = true
|
||||||
this.resolvedUri = StreamProxy.getProxyUrl(context, uri)
|
|
||||||
addCacheListener()
|
addCacheListener()
|
||||||
this.source = ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null)
|
|
||||||
|
this.source = ExtractorMediaSource(Uri.parse(proxyUri), datasources, extractors, null, null)
|
||||||
this.player!!.playWhenReady = false
|
this.player!!.playWhenReady = false
|
||||||
this.player.prepare(this.source)
|
this.player.prepare(this.source)
|
||||||
PlayerWrapper.addActivePlayer(this)
|
PlayerWrapper.addActivePlayer(this)
|
||||||
@ -239,6 +253,10 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
if (StreamProxy.ENABLED) {
|
if (StreamProxy.ENABLED) {
|
||||||
if (StreamProxy.isCached(this.originalUri)) {
|
if (StreamProxy.isCached(this.originalUri)) {
|
||||||
percentAvailable = 100
|
percentAvailable = 100
|
||||||
|
|
||||||
|
if (originalUri != null && metadata != null) {
|
||||||
|
PlayerWrapper.storeOffline(originalUri!!, metadata!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
StreamProxy.registerCacheListener(this.cacheListener, this.originalUri)
|
StreamProxy.registerCacheListener(this.cacheListener, this.originalUri)
|
||||||
@ -258,6 +276,12 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
private val cacheListener = { _: File, _: String, percent: Int ->
|
private val cacheListener = { _: File, _: String, percent: Int ->
|
||||||
//Log.e("CLCLCL", String.format("%d", percent));
|
//Log.e("CLCLCL", String.format("%d", percent));
|
||||||
percentAvailable = percent
|
percentAvailable = percent
|
||||||
|
|
||||||
|
if (percentAvailable >= 100) {
|
||||||
|
if (originalUri != null && metadata != null) {
|
||||||
|
PlayerWrapper.storeOffline(originalUri!!, metadata!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventListener = object : ExoPlayer.EventListener {
|
private var eventListener = object : ExoPlayer.EventListener {
|
||||||
@ -293,7 +317,8 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
|||||||
if (!prefetch) {
|
if (!prefetch) {
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
state = State.Playing
|
state = State.Playing
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
state = State.Paused
|
state = State.Paused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,24 @@ import java.util.HashMap
|
|||||||
import io.casey.musikcube.remote.Application
|
import io.casey.musikcube.remote.Application
|
||||||
import io.casey.musikcube.remote.util.Preconditions
|
import io.casey.musikcube.remote.util.Preconditions
|
||||||
import io.casey.musikcube.remote.websocket.Prefs
|
import io.casey.musikcube.remote.websocket.Prefs
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
class MediaPlayerWrapper : PlayerWrapper() {
|
class MediaPlayerWrapper : PlayerWrapper() {
|
||||||
|
|
||||||
private val player = MediaPlayer()
|
private val player = MediaPlayer()
|
||||||
private var seekTo: Int = 0
|
private var seekTo: Int = 0
|
||||||
private var prefetching: Boolean = false
|
private var prefetching: Boolean = false
|
||||||
private val context = Application.getInstance()
|
private val context = Application.instance
|
||||||
private val prefs: SharedPreferences
|
private val prefs: SharedPreferences
|
||||||
|
private var metadata: JSONObject? = null
|
||||||
|
private var proxyUri: String? = null
|
||||||
|
private var originalUri: String? = null
|
||||||
override var bufferedPercent: Int = 0
|
override var bufferedPercent: Int = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
this.prefs = context!!.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun play(uri: String) {
|
override fun play(uri: String, metadata: JSONObject) {
|
||||||
Preconditions.throwIfNotOnMainThread()
|
Preconditions.throwIfNotOnMainThread()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -40,17 +43,17 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
|||||||
val headers = HashMap<String, String>()
|
val headers = HashMap<String, String>()
|
||||||
headers.put("Authorization", "Basic " + encoded)
|
headers.put("Authorization", "Basic " + encoded)
|
||||||
|
|
||||||
player.setDataSource(
|
this.metadata = metadata
|
||||||
context,
|
this.originalUri = uri
|
||||||
Uri.parse(StreamProxy.getProxyUrl(context, uri)),
|
this.proxyUri = StreamProxy.getProxyUrl(context, uri)
|
||||||
headers)
|
|
||||||
|
|
||||||
|
player.setDataSource(context, Uri.parse(proxyUri), headers)
|
||||||
player.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
player.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
||||||
player.setOnPreparedListener(onPrepared)
|
player.setOnPreparedListener(onPrepared)
|
||||||
player.setOnErrorListener(onError)
|
player.setOnErrorListener(onError)
|
||||||
player.setOnCompletionListener(onCompleted)
|
player.setOnCompletionListener(onCompleted)
|
||||||
player.setOnBufferingUpdateListener(onBuffering)
|
player.setOnBufferingUpdateListener(onBuffering)
|
||||||
player.setWakeMode(Application.getInstance(), PowerManager.PARTIAL_WAKE_LOCK)
|
player.setWakeMode(Application.instance, PowerManager.PARTIAL_WAKE_LOCK)
|
||||||
player.prepareAsync()
|
player.prepareAsync()
|
||||||
}
|
}
|
||||||
catch (e: IOException) {
|
catch (e: IOException) {
|
||||||
@ -58,11 +61,11 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prefetch(uri: String) {
|
override fun prefetch(uri: String, metadata: JSONObject) {
|
||||||
Preconditions.throwIfNotOnMainThread()
|
Preconditions.throwIfNotOnMainThread()
|
||||||
|
|
||||||
this.prefetching = true
|
this.prefetching = true
|
||||||
play(uri)
|
play(uri, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause() {
|
override fun pause() {
|
||||||
@ -186,7 +189,7 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onPrepared = { mediaPlayer: MediaPlayer ->
|
private val onPrepared = { _: MediaPlayer ->
|
||||||
if (this.state === State.Killing) {
|
if (this.state === State.Killing) {
|
||||||
dispose()
|
dispose()
|
||||||
}
|
}
|
||||||
@ -224,7 +227,15 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
|||||||
dispose()
|
dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onBuffering = { _: MediaPlayer, percent: Int -> bufferedPercent = percent }
|
private val onBuffering = { _: MediaPlayer, percent: Int ->
|
||||||
|
bufferedPercent = percent
|
||||||
|
|
||||||
|
if (bufferedPercent >= 100) {
|
||||||
|
if (originalUri != null && metadata != null) {
|
||||||
|
PlayerWrapper.storeOffline(originalUri!!, metadata!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "MediaPlayerWrapper"
|
private val TAG = "MediaPlayerWrapper"
|
||||||
|
@ -3,12 +3,15 @@ package io.casey.musikcube.remote.playback;
|
|||||||
public class Metadata {
|
public class Metadata {
|
||||||
public interface Track {
|
public interface Track {
|
||||||
String ID = "id";
|
String ID = "id";
|
||||||
|
String EXTERNAL_ID = "external_id";
|
||||||
|
String URI = "uri";
|
||||||
String TITLE = "title";
|
String TITLE = "title";
|
||||||
String ALBUM = "album";
|
String ALBUM = "album";
|
||||||
String ALBUM_ID = "album_id";
|
String ALBUM_ID = "album_id";
|
||||||
String ALBUM_ARTIST = "album_artist";
|
String ALBUM_ARTIST = "album_artist";
|
||||||
String ALBUM_ARTIST_ID = "album_artist_id";
|
String ALBUM_ARTIST_ID = "album_artist_id";
|
||||||
String GENRE = "genre";
|
String GENRE = "genre";
|
||||||
|
String TRACK_NUM = "track_num";
|
||||||
String GENRE_ID = "visual_genre_id";
|
String GENRE_ID = "visual_genre_id";
|
||||||
String ARTIST = "artist";
|
String ARTIST = "artist";
|
||||||
String ARTIST_ID = "visual_artist_id";
|
String ARTIST_ID = "visual_artist_id";
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package io.casey.musikcube.remote.playback;
|
package io.casey.musikcube.remote.playback;
|
||||||
|
|
||||||
public enum PlaybackState {
|
public enum PlaybackState {
|
||||||
Unknown("unknown"),
|
|
||||||
Stopped("stopped"),
|
Stopped("stopped"),
|
||||||
Buffering("buffering"), /* streaming only */
|
Buffering("buffering"), /* streaming only */
|
||||||
Playing("playing"),
|
Playing("playing"),
|
||||||
@ -19,7 +18,7 @@ public enum PlaybackState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PlaybackState from(final String rawValue) {
|
static PlaybackState from(final String rawValue) {
|
||||||
if (Stopped.rawValue.equals(rawValue)) {
|
if (Stopped.rawValue.equals(rawValue) || "unknown".equals(rawValue)) {
|
||||||
return Stopped;
|
return Stopped;
|
||||||
}
|
}
|
||||||
else if (Playing.rawValue.equals(rawValue)) {
|
else if (Playing.rawValue.equals(rawValue)) {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
package io.casey.musikcube.remote.playback
|
package io.casey.musikcube.remote.playback
|
||||||
|
|
||||||
|
import io.casey.musikcube.remote.Application
|
||||||
|
import io.casey.musikcube.remote.offline.OfflineTrack
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
|
|
||||||
import io.casey.musikcube.remote.util.Preconditions
|
import io.casey.musikcube.remote.util.Preconditions
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
abstract class PlayerWrapper {
|
abstract class PlayerWrapper {
|
||||||
private enum class Type {
|
private enum class Type {
|
||||||
@ -38,8 +44,8 @@ abstract class PlayerWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun play(uri: String)
|
abstract fun play(uri: String, metadata: JSONObject)
|
||||||
abstract fun prefetch(uri: String)
|
abstract fun prefetch(uri: String, metadata: JSONObject)
|
||||||
abstract fun pause()
|
abstract fun pause()
|
||||||
abstract fun resume()
|
abstract fun resume()
|
||||||
abstract fun updateVolume()
|
abstract fun updateVolume()
|
||||||
@ -69,6 +75,19 @@ abstract class PlayerWrapper {
|
|||||||
private var globalMuted = false
|
private var globalMuted = false
|
||||||
private var preDuckGlobalVolume = DUCK_NONE
|
private var preDuckGlobalVolume = DUCK_NONE
|
||||||
|
|
||||||
|
fun storeOffline(uri: String, json: JSONObject) {
|
||||||
|
Single.fromCallable {
|
||||||
|
val track = OfflineTrack()
|
||||||
|
if (track.fromJSONObject(uri, json)) {
|
||||||
|
Application.offlineDb?.trackDao()?.insertTrack(track)
|
||||||
|
Application.offlineDb?.prune()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
fun duck() {
|
fun duck() {
|
||||||
Preconditions.throwIfNotOnMainThread()
|
Preconditions.throwIfNotOnMainThread()
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ public class RemotePlaybackService implements PlaybackService {
|
|||||||
private Handler handler = new Handler();
|
private Handler handler = new Handler();
|
||||||
private WebSocketService wss;
|
private WebSocketService wss;
|
||||||
private EstimatedPosition currentTime = new EstimatedPosition();
|
private EstimatedPosition currentTime = new EstimatedPosition();
|
||||||
private PlaybackState playbackState = PlaybackState.Unknown;
|
private PlaybackState playbackState = PlaybackState.Stopped;
|
||||||
private Set<EventListener> listeners = new HashSet<>();
|
private Set<EventListener> listeners = new HashSet<>();
|
||||||
private RepeatMode repeatMode;
|
private RepeatMode repeatMode;
|
||||||
private boolean shuffled;
|
private boolean shuffled;
|
||||||
@ -333,7 +333,7 @@ public class RemotePlaybackService implements PlaybackService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
playbackState = PlaybackState.Unknown;
|
playbackState = PlaybackState.Stopped;
|
||||||
repeatMode = RepeatMode.None;
|
repeatMode = RepeatMode.None;
|
||||||
shuffled = muted = false;
|
shuffled = muted = false;
|
||||||
volume = 0.0f;
|
volume = 0.0f;
|
||||||
@ -439,6 +439,11 @@ public class RemotePlaybackService implements PlaybackService {
|
|||||||
.addOption(Messages.Key.LIMIT, limit)
|
.addOption(Messages.Key.LIMIT, limit)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean connectionRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private WebSocketService.Client client = new WebSocketService.Client() {
|
private WebSocketService.Client client = new WebSocketService.Client() {
|
||||||
|
@ -156,7 +156,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
public StreamingPlaybackService(final Context context) {
|
public StreamingPlaybackService(final Context context) {
|
||||||
this.wss = WebSocketService.getInstance(context.getApplicationContext());
|
this.wss = WebSocketService.getInstance(context.getApplicationContext());
|
||||||
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
|
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
|
||||||
this.audioManager = (AudioManager) Application.getInstance().getSystemService(Context.AUDIO_SERVICE);
|
this.audioManager = (AudioManager) Application.Companion.getInstance().getSystemService(Context.AUDIO_SERVICE);
|
||||||
this.lastSystemVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
|
this.lastSystemVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
|
||||||
this.repeatMode = RepeatMode.from(this.prefs.getString(REPEAT_MODE_PREF, RepeatMode.None.toString()));
|
this.repeatMode = RepeatMode.from(this.prefs.getString(REPEAT_MODE_PREF, RepeatMode.None.toString()));
|
||||||
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
@ -570,7 +570,12 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
|
|
||||||
private String getUri(final JSONObject track) {
|
private String getUri(final JSONObject track) {
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
final String externalId = track.optString("external_id", "");
|
final String existingUri = track.optString(Metadata.Track.URI, "");
|
||||||
|
if (Strings.notEmpty(existingUri)) {
|
||||||
|
return existingUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String externalId = track.optString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
if (Strings.notEmpty(externalId)) {
|
if (Strings.notEmpty(externalId)) {
|
||||||
final String protocol = prefs.getBoolean(
|
final String protocol = prefs.getBoolean(
|
||||||
Prefs.Key.SSL_ENABLED, Prefs.Default.SSL_ENABLED) ? "https" : "http";
|
Prefs.Key.SSL_ENABLED, Prefs.Default.SSL_ENABLED) ? "https" : "http";
|
||||||
@ -582,7 +587,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
Prefs.Default.TRANSCODER_BITRATE_INDEX);
|
Prefs.Default.TRANSCODER_BITRATE_INDEX);
|
||||||
|
|
||||||
if (bitrateIndex > 0) {
|
if (bitrateIndex > 0) {
|
||||||
final Resources r = Application.getInstance().getResources();
|
final Resources r = Application.Companion.getInstance().getResources();
|
||||||
|
|
||||||
bitrateQueryParam = String.format(
|
bitrateQueryParam = String.format(
|
||||||
Locale.ENGLISH,
|
Locale.ENGLISH,
|
||||||
@ -701,7 +706,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
this.context.reset(this.context.nextPlayer);
|
this.context.reset(this.context.nextPlayer);
|
||||||
this.context.nextPlayer = PlayerWrapper.Companion.newInstance();
|
this.context.nextPlayer = PlayerWrapper.Companion.newInstance();
|
||||||
this.context.nextPlayer.setOnStateChangedListener(onNextPlayerStateChanged);
|
this.context.nextPlayer.setOnStateChangedListener(onNextPlayerStateChanged);
|
||||||
this.context.nextPlayer.prefetch(uri);
|
this.context.nextPlayer.prefetch(uri, this.context.nextMetadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -794,7 +799,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
this.context.currentPlayer = PlayerWrapper.Companion.newInstance();
|
this.context.currentPlayer = PlayerWrapper.Companion.newInstance();
|
||||||
this.context.currentPlayer.setOnStateChangedListener(onCurrentPlayerStateChanged);
|
this.context.currentPlayer.setOnStateChangedListener(onCurrentPlayerStateChanged);
|
||||||
this.context.currentPlayer.play(uri);
|
this.context.currentPlayer.play(uri, this.context.currentMetadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -885,6 +890,11 @@ public class StreamingPlaybackService implements PlaybackService {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean connectionRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private WebSocketService.Client wssClient = new WebSocketService.Client() {
|
private WebSocketService.Client wssClient = new WebSocketService.Client() {
|
||||||
|
@ -71,17 +71,17 @@ public class SystemService extends Service {
|
|||||||
private SimpleTarget<Bitmap> albumArtRequest;
|
private SimpleTarget<Bitmap> albumArtRequest;
|
||||||
|
|
||||||
public static void wakeup() {
|
public static void wakeup() {
|
||||||
final Context c = Application.getInstance();
|
final Context c = Application.Companion.getInstance();
|
||||||
c.startService(new Intent(c, SystemService.class).setAction(ACTION_WAKE_UP));
|
c.startService(new Intent(c, SystemService.class).setAction(ACTION_WAKE_UP));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void shutdown() {
|
public static void shutdown() {
|
||||||
final Context c = Application.getInstance();
|
final Context c = Application.Companion.getInstance();
|
||||||
c.startService(new Intent(c, SystemService.class).setAction(ACTION_SHUT_DOWN));
|
c.startService(new Intent(c, SystemService.class).setAction(ACTION_SHUT_DOWN));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void sleep() {
|
public static void sleep() {
|
||||||
final Context c = Application.getInstance();
|
final Context c = Application.Companion.getInstance();
|
||||||
c.startService(new Intent(c, SystemService.class).setAction(ACTION_SLEEP));
|
c.startService(new Intent(c, SystemService.class).setAction(ACTION_SLEEP));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ import io.casey.musikcube.remote.websocket.Messages;
|
|||||||
import io.casey.musikcube.remote.websocket.SocketMessage;
|
import io.casey.musikcube.remote.websocket.SocketMessage;
|
||||||
import io.casey.musikcube.remote.websocket.WebSocketService;
|
import io.casey.musikcube.remote.websocket.WebSocketService;
|
||||||
|
|
||||||
|
import static io.casey.musikcube.remote.ui.model.TrackListSlidingWindow.QueryFactory;
|
||||||
|
|
||||||
public class PlayQueueActivity extends WebSocketActivityBase {
|
public class PlayQueueActivity extends WebSocketActivityBase {
|
||||||
private static String EXTRA_PLAYING_INDEX = "extra_playing_index";
|
private static String EXTRA_PLAYING_INDEX = "extra_playing_index";
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
|||||||
private TrackListSlidingWindow<JSONObject> tracks;
|
private TrackListSlidingWindow<JSONObject> tracks;
|
||||||
private PlaybackService playback;
|
private PlaybackService playback;
|
||||||
private Adapter adapter;
|
private Adapter adapter;
|
||||||
|
private boolean offlineQueue;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@ -54,11 +57,16 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
|||||||
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
|
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
|
||||||
Views.setupDefaultRecyclerView(this, recyclerView, fastScroller, adapter);
|
Views.setupDefaultRecyclerView(this, recyclerView, fastScroller, adapter);
|
||||||
|
|
||||||
|
final QueryFactory queryFactory = this.playback.getPlaylistQueryFactory();
|
||||||
|
|
||||||
|
offlineQueue = Messages.Category.OFFLINE.equals(
|
||||||
|
queryFactory.getRequeryMessage().getStringOption(Messages.Key.CATEGORY));
|
||||||
|
|
||||||
this.tracks = new TrackListSlidingWindow<>(
|
this.tracks = new TrackListSlidingWindow<>(
|
||||||
recyclerView,
|
recyclerView,
|
||||||
fastScroller,
|
fastScroller,
|
||||||
this.wss,
|
this.wss,
|
||||||
this.playback.getPlaylistQueryFactory(),
|
queryFactory,
|
||||||
(JSONObject obj) -> obj);
|
(JSONObject obj) -> obj);
|
||||||
|
|
||||||
this.tracks.setInitialPosition(
|
this.tracks.setInitialPosition(
|
||||||
@ -110,7 +118,7 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
|||||||
private final WebSocketService.Client webSocketClient = new WebSocketService.Client() {
|
private final WebSocketService.Client webSocketClient = new WebSocketService.Client() {
|
||||||
@Override
|
@Override
|
||||||
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
||||||
if (newState == WebSocketService.State.Connected) {
|
if (newState == WebSocketService.State.Connected || offlineQueue) {
|
||||||
tracks.requery();
|
tracks.requery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,10 +155,13 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
|||||||
subtitle.setText("-");
|
subtitle.setText("-");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
long playingId = playback.getTrackLong(Messages.Key.ID, -1);
|
final String entryExternalId = entry
|
||||||
long entryId = entry.optLong(Messages.Key.ID, -1);
|
.optString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
|
|
||||||
if (entryId != -1 && playingId == entryId) {
|
final String playingExternalId = playback
|
||||||
|
.getTrackString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
|
|
||||||
|
if (entryExternalId.equals(playingExternalId)) {
|
||||||
titleColor = R.color.theme_green;
|
titleColor = R.color.theme_green;
|
||||||
subtitleColor = R.color.theme_yellow;
|
subtitleColor = R.color.theme_yellow;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,10 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
|||||||
.putExtra(EXTRA_SELECTED_ID, id);
|
.putExtra(EXTRA_SELECTED_ID, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Intent getOfflineStartIntent(final Context context) {
|
||||||
|
return getStartIntent(context, Messages.Category.OFFLINE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static Intent getStartIntent(final Context context,
|
public static Intent getStartIntent(final Context context,
|
||||||
final String type,
|
final String type,
|
||||||
final long id,
|
final long id,
|
||||||
@ -135,7 +139,7 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
|||||||
private WebSocketService.Client socketServiceClient = new WebSocketService.Client() {
|
private WebSocketService.Client socketServiceClient = new WebSocketService.Client() {
|
||||||
@Override
|
@Override
|
||||||
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
||||||
if (newState == WebSocketService.State.Connected) {
|
if (canRequery()) {
|
||||||
filterDebouncer.cancel();
|
filterDebouncer.cancel();
|
||||||
tracks.requery();
|
tracks.requery();
|
||||||
}
|
}
|
||||||
@ -192,10 +196,13 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
|||||||
int subtitleColor = R.color.theme_disabled_foreground;
|
int subtitleColor = R.color.theme_disabled_foreground;
|
||||||
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
long playingId = transport.getPlaybackService().getTrackLong(Messages.Key.ID, -1);
|
final String entryExternalId = entry
|
||||||
long entryId = entry.optLong(Messages.Key.ID, -1);
|
.optString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
|
|
||||||
if (entryId != -1 && playingId == entryId) {
|
final String playingExternalId = transport.getPlaybackService()
|
||||||
|
.getTrackString(Metadata.Track.EXTERNAL_ID, "");
|
||||||
|
|
||||||
|
if (entryExternalId.equals(playingExternalId)) {
|
||||||
titleColor = R.color.theme_green;
|
titleColor = R.color.theme_green;
|
||||||
subtitleColor = R.color.theme_yellow;
|
subtitleColor = R.color.theme_yellow;
|
||||||
}
|
}
|
||||||
@ -237,6 +244,12 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
|||||||
return categoryType != null && categoryType.length() > 0 && categoryId != -1;
|
return categoryType != null && categoryType.length() > 0 && categoryId != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canRequery() {
|
||||||
|
return
|
||||||
|
getWebSocketService().getState() == WebSocketService.State.Connected ||
|
||||||
|
Messages.Category.OFFLINE.equals(categoryType);
|
||||||
|
}
|
||||||
|
|
||||||
private QueryFactory createCategoryQueryFactory(
|
private QueryFactory createCategoryQueryFactory(
|
||||||
final String categoryType, long categoryId) {
|
final String categoryType, long categoryId) {
|
||||||
|
|
||||||
@ -265,6 +278,11 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
|||||||
.addOption(Messages.Key.FILTER, lastFilter)
|
.addOption(Messages.Key.FILTER, lastFilter)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean connectionRequired() {
|
||||||
|
return Messages.Category.OFFLINE.equals(categoryType);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -54,9 +54,13 @@ public class TrackListSlidingWindow<TrackType> {
|
|||||||
void onReloaded(int count);
|
void onReloaded(int count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface QueryFactory {
|
public static abstract class QueryFactory {
|
||||||
SocketMessage getRequeryMessage();
|
public abstract SocketMessage getRequeryMessage();
|
||||||
SocketMessage getPageAroundMessage(int offset, int limit);
|
public abstract SocketMessage getPageAroundMessage(int offset, int limit);
|
||||||
|
|
||||||
|
public boolean connectionRequired() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackListSlidingWindow(RecyclerView recyclerView,
|
public TrackListSlidingWindow(RecyclerView recyclerView,
|
||||||
@ -86,7 +90,9 @@ public class TrackListSlidingWindow<TrackType> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public void requery() {
|
public void requery() {
|
||||||
if (connected) {
|
boolean connectionRequired = (queryFactory != null) && queryFactory.connectionRequired();
|
||||||
|
|
||||||
|
if (!connectionRequired || connected) {
|
||||||
cancelMessages();
|
cancelMessages();
|
||||||
|
|
||||||
boolean queried = false;
|
boolean queried = false;
|
||||||
|
@ -108,6 +108,7 @@ public class Messages {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface Category {
|
public interface Category {
|
||||||
|
String OFFLINE = "offline";
|
||||||
String ALBUM = "album";
|
String ALBUM = "album";
|
||||||
String ARTIST = "artist";
|
String ARTIST = "artist";
|
||||||
String ALBUM_ARTIST = "album_artist";
|
String ALBUM_ARTIST = "album_artist";
|
||||||
|
@ -286,6 +286,16 @@ public class SocketMessage {
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withOptions(final JSONObject options) {
|
||||||
|
if (options == null) {
|
||||||
|
this.options = new JSONObject();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder addOption(final String key, final Object value) {
|
public Builder addOption(final String key, final Object value) {
|
||||||
try {
|
try {
|
||||||
options.put(key, value);
|
options.put(key, value);
|
||||||
|
@ -73,6 +73,14 @@ public class WebSocketService {
|
|||||||
boolean check(T value);
|
boolean check(T value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface Interceptor {
|
||||||
|
boolean process(SocketMessage message, Responder responder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Responder {
|
||||||
|
void respond(SocketMessage response);
|
||||||
|
}
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
Connecting,
|
Connecting,
|
||||||
Connected,
|
Connected,
|
||||||
@ -169,6 +177,7 @@ public class WebSocketService {
|
|||||||
private boolean autoReconnect = false;
|
private boolean autoReconnect = false;
|
||||||
private NetworkChangedReceiver networkChanged = new NetworkChangedReceiver();
|
private NetworkChangedReceiver networkChanged = new NetworkChangedReceiver();
|
||||||
private ConnectThread thread;
|
private ConnectThread thread;
|
||||||
|
private Set<Interceptor> interceptors = new HashSet<>();
|
||||||
|
|
||||||
public static synchronized WebSocketService getInstance(final Context context) {
|
public static synchronized WebSocketService getInstance(final Context context) {
|
||||||
if (INSTANCE == null) {
|
if (INSTANCE == null) {
|
||||||
@ -184,6 +193,16 @@ public class WebSocketService {
|
|||||||
handler.sendEmptyMessageDelayed(MESSAGE_REMOVE_OLD_CALLBACKS, CALLBACK_TIMEOUT_MILLIS);
|
handler.sendEmptyMessageDelayed(MESSAGE_REMOVE_OLD_CALLBACKS, CALLBACK_TIMEOUT_MILLIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addInterceptor(final Interceptor interceptor) {
|
||||||
|
Preconditions.throwIfNotOnMainThread();
|
||||||
|
interceptors.add(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeInterceptor(final Interceptor interceptor) {
|
||||||
|
Preconditions.throwIfNotOnMainThread();
|
||||||
|
interceptors.remove(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
public void addClient(Client client) {
|
public void addClient(Client client) {
|
||||||
Preconditions.throwIfNotOnMainThread();
|
Preconditions.throwIfNotOnMainThread();
|
||||||
|
|
||||||
@ -267,35 +286,47 @@ public class WebSocketService {
|
|||||||
public long send(final SocketMessage message, Client client, MessageResultCallback callback) {
|
public long send(final SocketMessage message, Client client, MessageResultCallback callback) {
|
||||||
Preconditions.throwIfNotOnMainThread();
|
Preconditions.throwIfNotOnMainThread();
|
||||||
|
|
||||||
if (this.socket != null) {
|
boolean intercepted = false;
|
||||||
/* it seems that sometimes the socket dies, but the onDisconnected() event is not
|
|
||||||
raised. unclear if this is our bug or a bug in the library. disconnect and trigger
|
|
||||||
a reconnect until we can find a better root cause. this is very difficult to repro */
|
|
||||||
if (!this.socket.isOpen()) {
|
|
||||||
this.disconnect(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
long id = NEXT_ID.incrementAndGet();
|
|
||||||
|
|
||||||
if (callback != null) {
|
for (final Interceptor i : interceptors) {
|
||||||
if (!clients.contains(client) && client != INTERNAL_CLIENT) {
|
if (i.process(message, responder)) {
|
||||||
throw new IllegalArgumentException("client is not registered");
|
intercepted = true;
|
||||||
}
|
|
||||||
|
|
||||||
final MessageResultDescriptor mrd = new MessageResultDescriptor();
|
|
||||||
mrd.id = id;
|
|
||||||
mrd.enqueueTime = System.currentTimeMillis();
|
|
||||||
mrd.client = client;
|
|
||||||
mrd.callback = callback;
|
|
||||||
messageCallbacks.put(message.getId(), mrd);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socket.sendText(message.toString());
|
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
if (!intercepted) {
|
||||||
|
/* it seems that sometimes the socket dies, but the onDisconnected() event is not
|
||||||
|
raised. unclear if this is our bug or a bug in the library. disconnect and trigger
|
||||||
|
a reconnect until we can find a better root cause. this is very difficult to repro */
|
||||||
|
if (this.socket != null && !this.socket.isOpen()) {
|
||||||
|
this.disconnect(true);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (this.socket == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final long id = NEXT_ID.incrementAndGet();
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
if (!clients.contains(client) && client != INTERNAL_CLIENT) {
|
||||||
|
throw new IllegalArgumentException("client is not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
final MessageResultDescriptor mrd = new MessageResultDescriptor();
|
||||||
|
mrd.id = id;
|
||||||
|
mrd.enqueueTime = System.currentTimeMillis();
|
||||||
|
mrd.client = client;
|
||||||
|
mrd.callback = callback;
|
||||||
|
messageCallbacks.put(message.getId(), mrd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intercepted) {
|
||||||
|
this.socket.sendText(message.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<SocketMessage> send(final SocketMessage message, Client client) {
|
public Observable<SocketMessage> send(final SocketMessage message, Client client) {
|
||||||
@ -305,39 +336,52 @@ public class WebSocketService {
|
|||||||
try {
|
try {
|
||||||
Preconditions.throwIfNotOnMainThread();
|
Preconditions.throwIfNotOnMainThread();
|
||||||
|
|
||||||
if (socket != null) {
|
boolean intercepted = false;
|
||||||
|
|
||||||
|
for (final Interceptor i : interceptors) {
|
||||||
|
if (i.process(message, responder)) {
|
||||||
|
intercepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intercepted) {
|
||||||
/* it seems that sometimes the socket dies, but the onDisconnected() event is not
|
/* it seems that sometimes the socket dies, but the onDisconnected() event is not
|
||||||
raised. unclear if this is our bug or a bug in the library. disconnect and trigger
|
raised. unclear if this is our bug or a bug in the library. disconnect and trigger
|
||||||
a reconnect until we can find a better root cause. this is very difficult to repro */
|
a reconnect until we can find a better root cause. this is very difficult to repro */
|
||||||
if (!socket.isOpen()) {
|
if (socket != null && !socket.isOpen()) {
|
||||||
disconnect(true);
|
disconnect(true);
|
||||||
throw new Exception("socket disconnected");
|
throw new Exception("socket disconnected");
|
||||||
}
|
}
|
||||||
else {
|
else if (socket == null) {
|
||||||
if (!clients.contains(client) && client != INTERNAL_CLIENT) {
|
throw new Exception("socket not connected");
|
||||||
throw new IllegalArgumentException("client is not registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
final MessageResultDescriptor mrd = new MessageResultDescriptor();
|
|
||||||
mrd.id = NEXT_ID.incrementAndGet();
|
|
||||||
mrd.enqueueTime = System.currentTimeMillis();
|
|
||||||
mrd.client = client;
|
|
||||||
|
|
||||||
mrd.callback = (SocketMessage message) -> {
|
|
||||||
emitter.onNext(message);
|
|
||||||
emitter.onComplete();
|
|
||||||
};
|
|
||||||
|
|
||||||
mrd.error = () -> {
|
|
||||||
final Exception ex = new Exception();
|
|
||||||
ex.fillInStackTrace();
|
|
||||||
emitter.onError(ex);
|
|
||||||
};
|
|
||||||
|
|
||||||
messageCallbacks.put(message.getId(), mrd);
|
|
||||||
socket.sendText(message.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!clients.contains(client) && client != INTERNAL_CLIENT) {
|
||||||
|
throw new IllegalArgumentException("client is not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
final MessageResultDescriptor mrd = new MessageResultDescriptor();
|
||||||
|
mrd.id = NEXT_ID.incrementAndGet();
|
||||||
|
mrd.enqueueTime = System.currentTimeMillis();
|
||||||
|
mrd.client = client;
|
||||||
|
|
||||||
|
mrd.callback = (SocketMessage message) -> {
|
||||||
|
emitter.onNext(message);
|
||||||
|
emitter.onComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
mrd.error = () -> {
|
||||||
|
final Exception ex = new Exception();
|
||||||
|
ex.fillInStackTrace();
|
||||||
|
emitter.onError(ex);
|
||||||
|
};
|
||||||
|
|
||||||
|
messageCallbacks.put(message.getId(), mrd);
|
||||||
|
|
||||||
|
if (!intercepted) {
|
||||||
|
socket.sendText(message.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
emitter.onError(ex);
|
emitter.onError(ex);
|
||||||
@ -486,6 +530,14 @@ public class WebSocketService {
|
|||||||
|
|
||||||
private Runnable autoDisconnectRunnable = () -> disconnect();
|
private Runnable autoDisconnectRunnable = () -> disconnect();
|
||||||
|
|
||||||
|
private Responder responder = (response) -> {
|
||||||
|
/* post to the back of the queue in case the interceptor responded immediately;
|
||||||
|
we need to ensure all of the request book-keeping has been finished. */
|
||||||
|
handler.post(() -> {
|
||||||
|
handler.sendMessage(Message.obtain(handler, MESSAGE_MESSAGE_RECEIVED, response));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private WebSocketAdapter webSocketAdapter = new WebSocketAdapter() {
|
private WebSocketAdapter webSocketAdapter = new WebSocketAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void onTextMessage(WebSocket websocket, String text) throws Exception {
|
public void onTextMessage(WebSocket websocket, String text) throws Exception {
|
||||||
|
@ -24,4 +24,9 @@
|
|||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
android:title="@string/menu_settings"/>
|
android:title="@string/menu_settings"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_offline_tracks"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:title="@string/menu_offline_tracks"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
@ -53,6 +53,7 @@
|
|||||||
<string name="menu_genres">genres</string>
|
<string name="menu_genres">genres</string>
|
||||||
<string name="menu_playlists">playlists</string>
|
<string name="menu_playlists">playlists</string>
|
||||||
<string name="menu_remote_toggle">remote playback</string>
|
<string name="menu_remote_toggle">remote playback</string>
|
||||||
|
<string name="menu_offline_tracks">offline tracks</string>
|
||||||
<string name="unknown_value"><unknown></string>
|
<string name="unknown_value"><unknown></string>
|
||||||
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
|
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
|
||||||
<string name="snackbar_remote_enabled">switched to remote control mode</string>
|
<string name="snackbar_remote_enabled">switched to remote control mode</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user