mirror of
https://github.com/clangen/musikcube.git
synced 2025-01-31 00:32:42 +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 {
|
||||
flatDir { dirs 'libs' }
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url 'https://maven.google.com' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@ -45,6 +46,9 @@ dependencies {
|
||||
compile(name:'videocache-2.8.0-pre', ext:'aar')
|
||||
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.squareup.okhttp3:okhttp: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(
|
||||
streaming ? R.mipmap.ic_toolbar_streaming : R.mipmap.ic_toolbar_remote);
|
||||
|
||||
menu.findItem(R.id.action_offline_tracks).setEnabled(streaming);
|
||||
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@ -145,6 +147,10 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
startActivity(CategoryBrowseActivity.getStartIntent(
|
||||
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.DeepLink.TRACKS));
|
||||
return true;
|
||||
|
||||
case R.id.action_offline_tracks:
|
||||
startActivity(TrackListActivity.getOfflineStartIntent(this));
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
@ -295,18 +301,19 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
final PlaybackState playbackState = playback.getPlaybackState();
|
||||
final boolean streaming = prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
|
||||
final boolean connected = (wss.getState() == WebSocketService.State.Connected);
|
||||
final boolean stopped = (playback.getPlaybackState() == PlaybackState.Stopped);
|
||||
final boolean playing = (playback.getPlaybackState() == PlaybackState.Playing);
|
||||
final boolean buffering = (playback.getPlaybackState() == PlaybackState.Buffering);
|
||||
final boolean showMetadataView = !stopped && connected && playback.getQueueCount() > 0;
|
||||
final boolean stopped = (playbackState == PlaybackState.Stopped);
|
||||
final boolean playing = (playbackState == PlaybackState.Playing);
|
||||
final boolean buffering = (playbackState == PlaybackState.Buffering);
|
||||
final boolean showMetadataView = !stopped && /*connected &&*/ playback.getQueueCount() > 0;
|
||||
|
||||
/* bottom section: transport controls */
|
||||
this.playPause.setText(playing || buffering ? R.string.button_pause : R.string.button_play);
|
||||
|
||||
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 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.net.Uri
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory
|
||||
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 okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
class ExoPlayerWrapper : PlayerWrapper() {
|
||||
@ -33,12 +35,13 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
private val extractors: ExtractorsFactory
|
||||
private var source: MediaSource? = null
|
||||
private val player: SimpleExoPlayer?
|
||||
private var metadata: JSONObject? = null
|
||||
private var prefetch: Boolean = false
|
||||
private val context: Context
|
||||
private var lastPosition: Long = -1
|
||||
private var percentAvailable = 0
|
||||
private var originalUri: String? = null
|
||||
private var resolvedUri: String? = null
|
||||
private var proxyUri: String? = null
|
||||
private val transcoding: Boolean
|
||||
|
||||
private fun initHttpClient(uri: String) {
|
||||
@ -88,26 +91,31 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
}
|
||||
|
||||
init {
|
||||
this.context = Application.getInstance()
|
||||
this.context = Application.instance!!
|
||||
val bandwidth = DefaultBandwidthMeter()
|
||||
val trackFactory = AdaptiveTrackSelection.Factory(bandwidth)
|
||||
val trackSelector = DefaultTrackSelector(trackFactory)
|
||||
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector)
|
||||
this.extractors = DefaultExtractorsFactory()
|
||||
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
|
||||
}
|
||||
|
||||
override fun play(uri: String) {
|
||||
override fun play(uri: String, metadata: JSONObject) {
|
||||
Preconditions.throwIfNotOnMainThread()
|
||||
|
||||
if (!dead()) {
|
||||
initHttpClient(uri)
|
||||
|
||||
this.metadata = metadata
|
||||
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()
|
||||
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.prepare(this.source)
|
||||
PlayerWrapper.addActivePlayer(this)
|
||||
@ -115,16 +123,22 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun prefetch(uri: String) {
|
||||
override fun prefetch(uri: String, metadata: JSONObject) {
|
||||
Preconditions.throwIfNotOnMainThread()
|
||||
|
||||
if (!dead()) {
|
||||
initHttpClient(uri)
|
||||
|
||||
this.metadata = metadata
|
||||
this.originalUri = uri
|
||||
this.proxyUri = StreamProxy.getProxyUrl(context, uri)
|
||||
Log.d("ExoPlayerWrapper", "originalUri: ${this.originalUri} proxyUri: ${this.proxyUri}")
|
||||
|
||||
this.prefetch = true
|
||||
this.resolvedUri = StreamProxy.getProxyUrl(context, uri)
|
||||
|
||||
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.prepare(this.source)
|
||||
PlayerWrapper.addActivePlayer(this)
|
||||
@ -239,6 +253,10 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
if (StreamProxy.ENABLED) {
|
||||
if (StreamProxy.isCached(this.originalUri)) {
|
||||
percentAvailable = 100
|
||||
|
||||
if (originalUri != null && metadata != null) {
|
||||
PlayerWrapper.storeOffline(originalUri!!, metadata!!)
|
||||
}
|
||||
}
|
||||
else {
|
||||
StreamProxy.registerCacheListener(this.cacheListener, this.originalUri)
|
||||
@ -258,6 +276,12 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
private val cacheListener = { _: File, _: String, percent: Int ->
|
||||
//Log.e("CLCLCL", String.format("%d", percent));
|
||||
percentAvailable = percent
|
||||
|
||||
if (percentAvailable >= 100) {
|
||||
if (originalUri != null && metadata != null) {
|
||||
PlayerWrapper.storeOffline(originalUri!!, metadata!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var eventListener = object : ExoPlayer.EventListener {
|
||||
@ -293,7 +317,8 @@ class ExoPlayerWrapper : PlayerWrapper() {
|
||||
if (!prefetch) {
|
||||
player.playWhenReady = true
|
||||
state = State.Playing
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
state = State.Paused
|
||||
}
|
||||
}
|
||||
|
@ -15,21 +15,24 @@ import java.util.HashMap
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.util.Preconditions
|
||||
import io.casey.musikcube.remote.websocket.Prefs
|
||||
import org.json.JSONObject
|
||||
|
||||
class MediaPlayerWrapper : PlayerWrapper() {
|
||||
|
||||
private val player = MediaPlayer()
|
||||
private var seekTo: Int = 0
|
||||
private var prefetching: Boolean = false
|
||||
private val context = Application.getInstance()
|
||||
private val context = Application.instance
|
||||
private val prefs: SharedPreferences
|
||||
private var metadata: JSONObject? = null
|
||||
private var proxyUri: String? = null
|
||||
private var originalUri: String? = null
|
||||
override var bufferedPercent: Int = 0
|
||||
|
||||
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()
|
||||
|
||||
try {
|
||||
@ -40,17 +43,17 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
||||
val headers = HashMap<String, String>()
|
||||
headers.put("Authorization", "Basic " + encoded)
|
||||
|
||||
player.setDataSource(
|
||||
context,
|
||||
Uri.parse(StreamProxy.getProxyUrl(context, uri)),
|
||||
headers)
|
||||
this.metadata = metadata
|
||||
this.originalUri = uri
|
||||
this.proxyUri = StreamProxy.getProxyUrl(context, uri)
|
||||
|
||||
player.setDataSource(context, Uri.parse(proxyUri), headers)
|
||||
player.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
||||
player.setOnPreparedListener(onPrepared)
|
||||
player.setOnErrorListener(onError)
|
||||
player.setOnCompletionListener(onCompleted)
|
||||
player.setOnBufferingUpdateListener(onBuffering)
|
||||
player.setWakeMode(Application.getInstance(), PowerManager.PARTIAL_WAKE_LOCK)
|
||||
player.setWakeMode(Application.instance, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
player.prepareAsync()
|
||||
}
|
||||
catch (e: IOException) {
|
||||
@ -58,11 +61,11 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun prefetch(uri: String) {
|
||||
override fun prefetch(uri: String, metadata: JSONObject) {
|
||||
Preconditions.throwIfNotOnMainThread()
|
||||
|
||||
this.prefetching = true
|
||||
play(uri)
|
||||
play(uri, metadata)
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
@ -186,7 +189,7 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
||||
}
|
||||
}
|
||||
|
||||
private val onPrepared = { mediaPlayer: MediaPlayer ->
|
||||
private val onPrepared = { _: MediaPlayer ->
|
||||
if (this.state === State.Killing) {
|
||||
dispose()
|
||||
}
|
||||
@ -224,7 +227,15 @@ class MediaPlayerWrapper : PlayerWrapper() {
|
||||
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 {
|
||||
private val TAG = "MediaPlayerWrapper"
|
||||
|
@ -3,12 +3,15 @@ package io.casey.musikcube.remote.playback;
|
||||
public class Metadata {
|
||||
public interface Track {
|
||||
String ID = "id";
|
||||
String EXTERNAL_ID = "external_id";
|
||||
String URI = "uri";
|
||||
String TITLE = "title";
|
||||
String ALBUM = "album";
|
||||
String ALBUM_ID = "album_id";
|
||||
String ALBUM_ARTIST = "album_artist";
|
||||
String ALBUM_ARTIST_ID = "album_artist_id";
|
||||
String GENRE = "genre";
|
||||
String TRACK_NUM = "track_num";
|
||||
String GENRE_ID = "visual_genre_id";
|
||||
String ARTIST = "artist";
|
||||
String ARTIST_ID = "visual_artist_id";
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.casey.musikcube.remote.playback;
|
||||
|
||||
public enum PlaybackState {
|
||||
Unknown("unknown"),
|
||||
Stopped("stopped"),
|
||||
Buffering("buffering"), /* streaming only */
|
||||
Playing("playing"),
|
||||
@ -19,7 +18,7 @@ public enum PlaybackState {
|
||||
}
|
||||
|
||||
static PlaybackState from(final String rawValue) {
|
||||
if (Stopped.rawValue.equals(rawValue)) {
|
||||
if (Stopped.rawValue.equals(rawValue) || "unknown".equals(rawValue)) {
|
||||
return Stopped;
|
||||
}
|
||||
else if (Playing.rawValue.equals(rawValue)) {
|
||||
|
@ -1,8 +1,14 @@
|
||||
package io.casey.musikcube.remote.playback
|
||||
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.offline.OfflineTrack
|
||||
import java.util.HashSet
|
||||
|
||||
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 {
|
||||
private enum class Type {
|
||||
@ -38,8 +44,8 @@ abstract class PlayerWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun play(uri: String)
|
||||
abstract fun prefetch(uri: String)
|
||||
abstract fun play(uri: String, metadata: JSONObject)
|
||||
abstract fun prefetch(uri: String, metadata: JSONObject)
|
||||
abstract fun pause()
|
||||
abstract fun resume()
|
||||
abstract fun updateVolume()
|
||||
@ -69,6 +75,19 @@ abstract class PlayerWrapper {
|
||||
private var globalMuted = false
|
||||
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() {
|
||||
Preconditions.throwIfNotOnMainThread()
|
||||
|
||||
|
@ -95,7 +95,7 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
private Handler handler = new Handler();
|
||||
private WebSocketService wss;
|
||||
private EstimatedPosition currentTime = new EstimatedPosition();
|
||||
private PlaybackState playbackState = PlaybackState.Unknown;
|
||||
private PlaybackState playbackState = PlaybackState.Stopped;
|
||||
private Set<EventListener> listeners = new HashSet<>();
|
||||
private RepeatMode repeatMode;
|
||||
private boolean shuffled;
|
||||
@ -333,7 +333,7 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
playbackState = PlaybackState.Unknown;
|
||||
playbackState = PlaybackState.Stopped;
|
||||
repeatMode = RepeatMode.None;
|
||||
shuffled = muted = false;
|
||||
volume = 0.0f;
|
||||
@ -439,6 +439,11 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
.addOption(Messages.Key.LIMIT, limit)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectionRequired() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private WebSocketService.Client client = new WebSocketService.Client() {
|
||||
|
@ -156,7 +156,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
||||
public StreamingPlaybackService(final Context context) {
|
||||
this.wss = WebSocketService.getInstance(context.getApplicationContext());
|
||||
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.repeatMode = RepeatMode.from(this.prefs.getString(REPEAT_MODE_PREF, RepeatMode.None.toString()));
|
||||
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
@ -570,7 +570,12 @@ public class StreamingPlaybackService implements PlaybackService {
|
||||
|
||||
private String getUri(final JSONObject track) {
|
||||
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)) {
|
||||
final String protocol = prefs.getBoolean(
|
||||
Prefs.Key.SSL_ENABLED, Prefs.Default.SSL_ENABLED) ? "https" : "http";
|
||||
@ -582,7 +587,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
||||
Prefs.Default.TRANSCODER_BITRATE_INDEX);
|
||||
|
||||
if (bitrateIndex > 0) {
|
||||
final Resources r = Application.getInstance().getResources();
|
||||
final Resources r = Application.Companion.getInstance().getResources();
|
||||
|
||||
bitrateQueryParam = String.format(
|
||||
Locale.ENGLISH,
|
||||
@ -701,7 +706,7 @@ public class StreamingPlaybackService implements PlaybackService {
|
||||
this.context.reset(this.context.nextPlayer);
|
||||
this.context.nextPlayer = PlayerWrapper.Companion.newInstance();
|
||||
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) {
|
||||
this.context.currentPlayer = PlayerWrapper.Companion.newInstance();
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectionRequired() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private WebSocketService.Client wssClient = new WebSocketService.Client() {
|
||||
|
@ -71,17 +71,17 @@ public class SystemService extends Service {
|
||||
private SimpleTarget<Bitmap> albumArtRequest;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,8 @@ import io.casey.musikcube.remote.websocket.Messages;
|
||||
import io.casey.musikcube.remote.websocket.SocketMessage;
|
||||
import io.casey.musikcube.remote.websocket.WebSocketService;
|
||||
|
||||
import static io.casey.musikcube.remote.ui.model.TrackListSlidingWindow.QueryFactory;
|
||||
|
||||
public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
private static String EXTRA_PLAYING_INDEX = "extra_playing_index";
|
||||
|
||||
@ -35,6 +37,7 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
private TrackListSlidingWindow<JSONObject> tracks;
|
||||
private PlaybackService playback;
|
||||
private Adapter adapter;
|
||||
private boolean offlineQueue;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@ -54,11 +57,16 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
|
||||
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<>(
|
||||
recyclerView,
|
||||
fastScroller,
|
||||
this.wss,
|
||||
this.playback.getPlaylistQueryFactory(),
|
||||
queryFactory,
|
||||
(JSONObject obj) -> obj);
|
||||
|
||||
this.tracks.setInitialPosition(
|
||||
@ -110,7 +118,7 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
private final WebSocketService.Client webSocketClient = new WebSocketService.Client() {
|
||||
@Override
|
||||
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
||||
if (newState == WebSocketService.State.Connected) {
|
||||
if (newState == WebSocketService.State.Connected || offlineQueue) {
|
||||
tracks.requery();
|
||||
}
|
||||
}
|
||||
@ -147,10 +155,13 @@ public class PlayQueueActivity extends WebSocketActivityBase {
|
||||
subtitle.setText("-");
|
||||
}
|
||||
else {
|
||||
long playingId = playback.getTrackLong(Messages.Key.ID, -1);
|
||||
long entryId = entry.optLong(Messages.Key.ID, -1);
|
||||
final String entryExternalId = entry
|
||||
.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;
|
||||
subtitleColor = R.color.theme_yellow;
|
||||
}
|
||||
|
@ -40,6 +40,10 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
.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,
|
||||
final String type,
|
||||
final long id,
|
||||
@ -135,7 +139,7 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
private WebSocketService.Client socketServiceClient = new WebSocketService.Client() {
|
||||
@Override
|
||||
public void onStateChanged(WebSocketService.State newState, WebSocketService.State oldState) {
|
||||
if (newState == WebSocketService.State.Connected) {
|
||||
if (canRequery()) {
|
||||
filterDebouncer.cancel();
|
||||
tracks.requery();
|
||||
}
|
||||
@ -192,10 +196,13 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
int subtitleColor = R.color.theme_disabled_foreground;
|
||||
|
||||
if (entry != null) {
|
||||
long playingId = transport.getPlaybackService().getTrackLong(Messages.Key.ID, -1);
|
||||
long entryId = entry.optLong(Messages.Key.ID, -1);
|
||||
final String entryExternalId = entry
|
||||
.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;
|
||||
subtitleColor = R.color.theme_yellow;
|
||||
}
|
||||
@ -237,6 +244,12 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
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(
|
||||
final String categoryType, long categoryId) {
|
||||
|
||||
@ -265,6 +278,11 @@ public class TrackListActivity extends WebSocketActivityBase implements Filterab
|
||||
.addOption(Messages.Key.FILTER, lastFilter)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connectionRequired() {
|
||||
return Messages.Category.OFFLINE.equals(categoryType);
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
@ -54,9 +54,13 @@ public class TrackListSlidingWindow<TrackType> {
|
||||
void onReloaded(int count);
|
||||
}
|
||||
|
||||
public interface QueryFactory {
|
||||
SocketMessage getRequeryMessage();
|
||||
SocketMessage getPageAroundMessage(int offset, int limit);
|
||||
public static abstract class QueryFactory {
|
||||
public abstract SocketMessage getRequeryMessage();
|
||||
public abstract SocketMessage getPageAroundMessage(int offset, int limit);
|
||||
|
||||
public boolean connectionRequired() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public TrackListSlidingWindow(RecyclerView recyclerView,
|
||||
@ -86,7 +90,9 @@ public class TrackListSlidingWindow<TrackType> {
|
||||
};
|
||||
|
||||
public void requery() {
|
||||
if (connected) {
|
||||
boolean connectionRequired = (queryFactory != null) && queryFactory.connectionRequired();
|
||||
|
||||
if (!connectionRequired || connected) {
|
||||
cancelMessages();
|
||||
|
||||
boolean queried = false;
|
||||
|
@ -108,6 +108,7 @@ public class Messages {
|
||||
}
|
||||
|
||||
public interface Category {
|
||||
String OFFLINE = "offline";
|
||||
String ALBUM = "album";
|
||||
String ARTIST = "artist";
|
||||
String ALBUM_ARTIST = "album_artist";
|
||||
|
@ -286,6 +286,16 @@ public class SocketMessage {
|
||||
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) {
|
||||
try {
|
||||
options.put(key, value);
|
||||
|
@ -73,6 +73,14 @@ public class WebSocketService {
|
||||
boolean check(T value);
|
||||
}
|
||||
|
||||
public interface Interceptor {
|
||||
boolean process(SocketMessage message, Responder responder);
|
||||
}
|
||||
|
||||
public interface Responder {
|
||||
void respond(SocketMessage response);
|
||||
}
|
||||
|
||||
public enum State {
|
||||
Connecting,
|
||||
Connected,
|
||||
@ -169,6 +177,7 @@ public class WebSocketService {
|
||||
private boolean autoReconnect = false;
|
||||
private NetworkChangedReceiver networkChanged = new NetworkChangedReceiver();
|
||||
private ConnectThread thread;
|
||||
private Set<Interceptor> interceptors = new HashSet<>();
|
||||
|
||||
public static synchronized WebSocketService getInstance(final Context context) {
|
||||
if (INSTANCE == null) {
|
||||
@ -184,6 +193,16 @@ public class WebSocketService {
|
||||
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) {
|
||||
Preconditions.throwIfNotOnMainThread();
|
||||
|
||||
@ -267,35 +286,47 @@ public class WebSocketService {
|
||||
public long send(final SocketMessage message, Client client, MessageResultCallback callback) {
|
||||
Preconditions.throwIfNotOnMainThread();
|
||||
|
||||
if (this.socket != null) {
|
||||
/* 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();
|
||||
boolean intercepted = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
this.socket.sendText(message.toString());
|
||||
return id;
|
||||
for (final Interceptor i : interceptors) {
|
||||
if (i.process(message, responder)) {
|
||||
intercepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -305,39 +336,52 @@ public class WebSocketService {
|
||||
try {
|
||||
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
|
||||
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 (!socket.isOpen()) {
|
||||
if (socket != null && !socket.isOpen()) {
|
||||
disconnect(true);
|
||||
throw new Exception("socket disconnected");
|
||||
}
|
||||
else {
|
||||
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);
|
||||
socket.sendText(message.toString());
|
||||
else if (socket == null) {
|
||||
throw new Exception("socket not connected");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
emitter.onError(ex);
|
||||
@ -486,6 +530,14 @@ public class WebSocketService {
|
||||
|
||||
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() {
|
||||
@Override
|
||||
public void onTextMessage(WebSocket websocket, String text) throws Exception {
|
||||
|
@ -24,4 +24,9 @@
|
||||
app:showAsAction="never"
|
||||
android:title="@string/menu_settings"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_offline_tracks"
|
||||
app:showAsAction="never"
|
||||
android:title="@string/menu_offline_tracks"/>
|
||||
|
||||
</menu>
|
@ -53,6 +53,7 @@
|
||||
<string name="menu_genres">genres</string>
|
||||
<string name="menu_playlists">playlists</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="snackbar_streaming_enabled">switched to streaming mode</string>
|
||||
<string name="snackbar_remote_enabled">switched to remote control mode</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user