More musikdroid client work:

1. fixed bugs around abusing the StreamingPlaybackService::next()
2. added album art to MediaSessionCompat implementation
3. minor refactors to the way AlbumArtModel works
4. added constants for preferences keys and defaults
This commit is contained in:
casey langen 2017-05-18 23:27:46 -07:00
parent f57881c88e
commit a67e05dcd2
11 changed files with 375 additions and 154 deletions

View File

@ -21,6 +21,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
@ -47,6 +48,7 @@ import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.ui.view.LongPressTextView;
import io.casey.musikcube.remote.util.Strings;
import io.casey.musikcube.remote.websocket.Messages;
import io.casey.musikcube.remote.websocket.Prefs;
import io.casey.musikcube.remote.websocket.SocketMessage;
import io.casey.musikcube.remote.websocket.WebSocketService;
@ -70,7 +72,7 @@ public class MainActivity extends WebSocketActivityBase {
private enum DisplayMode { Artwork, NoArtwork, Stopped }
private View mainTrackMetadataWithAlbumArt, mainTrackMetadataNoAlbumArt;
private ViewPropertyAnimator metadataAnim1, metadataAnim2;
private AlbumArtModel albumArtModel = new AlbumArtModel();
private AlbumArtModel albumArtModel = AlbumArtModel.empty();
private ImageView albumArtImageView;
static {
@ -89,7 +91,7 @@ public class MainActivity extends WebSocketActivityBase {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.prefs = this.getSharedPreferences("prefs", Context.MODE_PRIVATE);
this.prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
this.wss = getWebSocketService();
this.playback = getPlaybackService();
@ -325,7 +327,9 @@ public class MainActivity extends WebSocketActivityBase {
/* state management for UI stuff is starting to get out of hand. we should
refactor things pretty soon before they're completely out of control */
final boolean streaming = prefs.getBoolean("streaming_playback", false);
final boolean streaming = prefs.getBoolean(
Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
final WebSocketService.State state = wss.getState();
final boolean connected = state == WebSocketService.State.Connected;
@ -382,17 +386,20 @@ public class MainActivity extends WebSocketActivityBase {
Views.setCheckWithoutEvent(this.shuffleCb, playback.isShuffled(), this.shuffleListener);
Views.setCheckWithoutEvent(this.muteCb, playback.isMuted(), this.muteListener);
boolean albumArtEnabledInSettings = this.prefs.getBoolean("album_art_enabled", true);
boolean albumArtEnabledInSettings = this.prefs.getBoolean(
Prefs.Key.ALBUM_ART_ENABLED, Prefs.Default.ALBUM_ART_ENABLED);
if (stateIsValidForArtwork) {
if (!albumArtEnabledInSettings || Strings.empty(artist) || Strings.empty(album)) {
this.albumArtModel = new AlbumArtModel();
this.albumArtModel = AlbumArtModel.empty();
setMetadataDisplayMode(DisplayMode.NoArtwork);
}
else {
if (!this.albumArtModel.is(artist, album)) {
this.albumArtModel.destroy();
this.albumArtModel = new AlbumArtModel(title, artist, album, albumArtRetrieved);
this.albumArtModel = new AlbumArtModel(
title, artist, album, AlbumArtModel.Size.Mega, albumArtRetrieved);
}
updateAlbumArt();
}
@ -400,7 +407,7 @@ public class MainActivity extends WebSocketActivityBase {
}
private void clearUi() {
albumArtModel = new AlbumArtModel();
albumArtModel = AlbumArtModel.empty();
updateAlbumArt();
rebindUi();
}
@ -442,7 +449,7 @@ public class MainActivity extends WebSocketActivityBase {
final String album = track.optString(Metadata.Track.ALBUM, "");
if (!albumArtModel.is(artist, album)) {
new AlbumArtModel("", artist, album, (info, url) -> {
new AlbumArtModel("", artist, album, AlbumArtModel.Size.Mega, (info, url) -> {
int width = albumArtImageView.getWidth();
int height = albumArtImageView.getHeight();
Glide.with(MainActivity.this).load(url).downloadOnly(width, height);
@ -468,6 +475,7 @@ public class MainActivity extends WebSocketActivityBase {
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e,
@ -576,12 +584,7 @@ public class MainActivity extends WebSocketActivityBase {
}
};
private PlaybackService.EventListener playbackEvents = new PlaybackService.EventListener() {
@Override
public void onStateUpdated() {
rebindUi();
}
};
private PlaybackService.EventListener playbackEvents = () -> rebindUi();
private WebSocketService.Client serviceClient = new WebSocketService.Client() {
@Override

View File

@ -3,6 +3,7 @@ package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
@ -33,6 +34,8 @@ import java.util.Map;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
@ -70,7 +73,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
public ExoPlayerWrapper() {
this.context = Application.getInstance();
this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
this.bandwidth = new DefaultBandwidthMeter();
final TrackSelection.Factory trackFactory = new AdaptiveTrackSelection.Factory(bandwidth);
final TrackSelector trackSelector = new DefaultTrackSelector(trackFactory);
@ -86,7 +89,9 @@ public class ExoPlayerWrapper extends PlayerWrapper {
if (audioStreamHttpClient == null) {
final File path = new File(context.getExternalCacheDir(), "audio");
int diskCacheIndex = this.prefs.getInt("disk_cache_size_index", 0);
int diskCacheIndex = this.prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX);
if (diskCacheIndex < 0 || diskCacheIndex > CACHE_SETTING_TO_BYTES.size()) {
diskCacheIndex = 0;
}
@ -94,7 +99,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.cache(new Cache(path, CACHE_SETTING_TO_BYTES.get(diskCacheIndex)));
if (this.prefs.getBoolean("cert_validation_disabled", false)) {
if (this.prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(builder);
}
@ -116,27 +121,37 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public void play(String uri) {
initHttpClient(uri);
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
}
}
@Override
public void prefetch(String uri) {
initHttpClient(uri);
this.prefetch = true;
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.prefetch = true;
this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
}
}
@Override
public void pause() {
Preconditions.throwIfNotOnMainThread();
this.prefetch = true;
if (this.getState() == State.Playing) {
@ -147,6 +162,8 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public void resume() {
Preconditions.throwIfNotOnMainThread();
if (this.getState() == State.Paused || this.getState() == State.Prepared) {
this.player.setPlayWhenReady(true);
setState(State.Playing);
@ -157,6 +174,8 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public void setPosition(int millis) {
Preconditions.throwIfNotOnMainThread();
if (this.player.getPlaybackState() != ExoPlayer.STATE_IDLE) {
this.player.seekTo(millis);
}
@ -164,33 +183,43 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public int getPosition() {
Preconditions.throwIfNotOnMainThread();
return (int) this.player.getCurrentPosition();
}
@Override
public int getDuration() {
Preconditions.throwIfNotOnMainThread();
return (int) this.player.getDuration();
}
@Override
public void updateVolume() {
Preconditions.throwIfNotOnMainThread();
this.player.setVolume(getGlobalVolume());
}
@Override
public void setNextMediaPlayer(PlayerWrapper wrapper) {
Preconditions.throwIfNotOnMainThread();
}
@Override
public void dispose() {
if (getState() != State.Disposed) {
removeActivePlayer(this);
setState(State.Killing);
Preconditions.throwIfNotOnMainThread();
setState(State.Killing);
removeActivePlayer(this);
if (this.player != null) {
this.player.setPlayWhenReady(false);
this.player.removeListener(eventListener);
this.player.stop();
this.player.release();
setState(State.Disposed);
}
setState(State.Disposed);
}
@Override
@ -198,6 +227,11 @@ public class ExoPlayerWrapper extends PlayerWrapper {
super.setOnStateChangedListener(listener);
}
private boolean dead() {
final State state = getState();
return (state == State.Killing || state == State.Disposed);
}
private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
@ -216,17 +250,24 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Preconditions.throwIfNotOnMainThread();
if (playbackState == ExoPlayer.STATE_READY) {
setState(State.Prepared);
player.setVolume(getGlobalVolume());
if (!prefetch) {
player.setPlayWhenReady(true);
setState(State.Playing);
if (dead()) {
dispose();
}
else {
setState(State.Paused);
setState(State.Prepared);
player.setVolume(getGlobalVolume());
if (!prefetch) {
player.setPlayWhenReady(true);
setState(State.Playing);
}
else {
setState(State.Paused);
}
}
}
else if (playbackState == ExoPlayer.STATE_ENDED) {
@ -237,6 +278,8 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@Override
public void onPlayerError(ExoPlaybackException error) {
Preconditions.throwIfNotOnMainThread();
/* if we're transcoding the size of the response will be inexact, so the player
will try to pick up the last few bytes and be left with an HTTP 416. if that happens,
and we're towards the end of the track, just move to the next one */

View File

@ -3,6 +3,8 @@ package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import io.casey.musikcube.remote.websocket.Prefs;
public class PlaybackServiceFactory {
private static StreamingPlaybackService streaming;
private static RemotePlaybackService remote;
@ -11,7 +13,7 @@ public class PlaybackServiceFactory {
public static synchronized PlaybackService instance(final Context context) {
init(context);
if (prefs.getBoolean("streaming_playback", true)) {
if (prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)) {
return streaming;
}
@ -30,7 +32,7 @@ public class PlaybackServiceFactory {
private static void init(final Context context) {
if (streaming == null || remote == null || prefs == null) {
prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
streaming = new StreamingPlaybackService(context);
remote = new RemotePlaybackService(context);
}

View File

@ -1,5 +1,7 @@
package io.casey.musikcube.remote.playback;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;

View File

@ -28,6 +28,7 @@ import io.casey.musikcube.remote.R;
import io.casey.musikcube.remote.ui.model.TrackListSlidingWindow;
import io.casey.musikcube.remote.util.Strings;
import io.casey.musikcube.remote.websocket.Messages;
import io.casey.musikcube.remote.websocket.Prefs;
import io.casey.musikcube.remote.websocket.SocketMessage;
import io.casey.musikcube.remote.websocket.WebSocketService;
import io.reactivex.Observable;
@ -67,15 +68,10 @@ public class StreamingPlaybackService implements PlaybackService {
int currentIndex = -1, nextIndex = -1;
boolean nextPlayerScheduled;
public void stopPlayback() {
public void stopPlaybackAndReset() {
reset(currentPlayer);
reset(nextPlayer);
nextPlayerScheduled = false;
}
public void stopPlaybackAndReset() {
stopPlayback();
this.currentPlayer = this.nextPlayer = null;
nextPlayerScheduled = false; this.currentPlayer = this.nextPlayer = null;
this.currentMetadata = this.nextMetadata = null;
this.currentIndex = this.nextIndex = -1;
}
@ -156,7 +152,7 @@ public class StreamingPlaybackService implements PlaybackService {
public StreamingPlaybackService(final Context context) {
this.wss = WebSocketService.getInstance(context.getApplicationContext());
this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
this.audioManager = (AudioManager) Application.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()));
@ -208,7 +204,7 @@ public class StreamingPlaybackService implements PlaybackService {
@Override
public void playAt(int index) {
if (requestAudioFocus()) {
this.context.stopPlayback();
this.context.stopPlaybackAndReset();
loadQueueAndPlay(this.params, index);
}
}
@ -316,7 +312,7 @@ public class StreamingPlaybackService implements PlaybackService {
@Override
public double getVolume() {
if (prefs.getBoolean("software_volume", false)) {
if (prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME)) {
return PlayerWrapper.getGlobalVolume();
}
@ -409,7 +405,7 @@ public class StreamingPlaybackService implements PlaybackService {
}
private float getVolumeStep() {
if (prefs.getBoolean("software_volume", false)) {
if (prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME)) {
return 0.1f;
}
return 1.0f / getMaxSystemVolume();
@ -420,7 +416,7 @@ public class StreamingPlaybackService implements PlaybackService {
toggleMute();
}
final boolean softwareVolume = prefs.getBoolean("software_volume", false);
final boolean softwareVolume = prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME);
float current = softwareVolume ? PlayerWrapper.getGlobalVolume() : getSystemVolume();
current += delta;
@ -538,11 +534,15 @@ public class StreamingPlaybackService implements PlaybackService {
if (track != null) {
final String externalId = track.optString("external_id", "");
if (Strings.notEmpty(externalId)) {
final String protocol = prefs.getBoolean("ssl_enabled", false) ? "https" : "http";
final String protocol = prefs.getBoolean(
Prefs.Key.SSL_ENABLED, Prefs.Default.SSL_ENABLED) ? "https" : "http";
/* transcoding bitrate, if selected by the user */
String bitrateQueryParam = "";
final int bitrateIndex = prefs.getInt("transcoder_bitrate_index", 0);
final int bitrateIndex = prefs.getInt(
Prefs.Key.TRANSCODER_BITRATE_INDEX,
Prefs.Default.TRANSCODER_BITRATE_INDEX);
if (bitrateIndex > 0) {
final Resources r = Application.getInstance().getResources();
@ -556,8 +556,8 @@ public class StreamingPlaybackService implements PlaybackService {
Locale.ENGLISH,
"%s://%s:%d/audio/external_id/%s%s",
protocol,
prefs.getString("address", "192.168.1.100"),
prefs.getInt("http_port", 7906),
prefs.getString(Prefs.Key.ADDRESS, Prefs.Default.ADDRESS),
prefs.getInt(Prefs.Key.AUDIO_PORT, Prefs.Default.AUDIO_PORT),
URLEncoder.encode(externalId),
bitrateQueryParam);
}
@ -565,25 +565,6 @@ public class StreamingPlaybackService implements PlaybackService {
return null;
}
private void playCurrentTrack() {
this.context.stopPlayback();
final String uri = getUri(this.context.currentMetadata);
if (uri != null) {
this.context.currentPlayer = PlayerWrapper.newInstance();
this.context.currentPlayer.setOnStateChangedListener(onCurrentPlayerStateChanged);
this.context.currentPlayer.play(uri);
setState(PlaybackState.Buffering);
}
}
private void onPlayQueueLoaded() {
if (this.state == PlaybackState.Buffering) {
playCurrentTrack();
}
}
private int resolvePrevIndex(final int currentIndex, final int count) {
if (currentIndex - 1 < 0) {
if (repeatMode == RepeatMode.List) {
@ -741,9 +722,10 @@ public class StreamingPlaybackService implements PlaybackService {
cancelScheduledPausedShutdown();
SystemService.wakeup();
this.context.stopPlayback();
this.context.stopPlaybackAndReset();
final PlaybackContext context = new PlaybackContext();
context.currentIndex = startIndex;
this.context = context;
context.currentIndex = startIndex;
this.params = params;
final SocketMessage countMessage = queryFactory.getRequeryMessage();
@ -763,10 +745,15 @@ public class StreamingPlaybackService implements PlaybackService {
}
})
.doOnComplete(() -> {
if (StreamingPlaybackService.this.params == params) {
StreamingPlaybackService.this.context = context;
if (this.params == params && this.context == context) {
notifyEventListeners();
onPlayQueueLoaded();
final String uri = getUri(this.context.currentMetadata);
if (uri != null) {
this.context.currentPlayer = PlayerWrapper.newInstance();
this.context.currentPlayer.setOnStateChangedListener(onCurrentPlayerStateChanged);
this.context.currentPlayer.play(uri);
}
}
})
.subscribe();

View File

@ -7,7 +7,10 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.Nullable;
@ -18,11 +21,19 @@ import android.support.v7.app.NotificationCompat;
import android.util.Log;
import android.view.KeyEvent;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.Target;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.MainActivity;
import io.casey.musikcube.remote.R;
import io.casey.musikcube.remote.ui.model.AlbumArtModel;
import io.casey.musikcube.remote.util.Debouncer;
import io.casey.musikcube.remote.util.Strings;
import io.casey.musikcube.remote.websocket.Prefs;
/* basically a stub service that exists to keep a connection active to the
StreamingPlaybackService, which keeps music playing. TODO: should also hold
@ -46,12 +57,18 @@ public class SystemService extends Service {
PlaybackStateCompat.ACTION_FAST_FORWARD |
PlaybackStateCompat.ACTION_REWIND;
private Handler handler = new Handler();
private SharedPreferences prefs;
private StreamingPlaybackService playback;
private PowerManager.WakeLock wakeLock;
private PowerManager powerManager;
private MediaSessionCompat mediaSession;
private int headsetHookPressCount = 0;
private AlbumArtModel albumArtModel = AlbumArtModel.empty();
private Bitmap albumArt = null;
private SimpleTarget<Bitmap> albumArtRequest;
public static void wakeup() {
final Context c = Application.getInstance();
c.startService(new Intent(c, SystemService.class).setAction(ACTION_WAKE_UP));
@ -69,6 +86,7 @@ public class SystemService extends Service {
@Override
public void onCreate() {
super.onCreate();
prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
powerManager = (PowerManager) getSystemService(POWER_SERVICE);
registerReceivers();
}
@ -76,6 +94,7 @@ public class SystemService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
recycleAlbumArt();
unregisterReceivers();
}
@ -189,7 +208,7 @@ public class SystemService extends Service {
duration = (int) (playback.getDuration() * 1000);
}
updateMetadata(title, artist, album, duration);
updateMetadata(title, artist, album, null, duration);
updateNotification(title, artist, album, mediaSessionState);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
@ -198,14 +217,73 @@ public class SystemService extends Service {
.build());
}
private void updateMetadata(final String title, final String artist, final String album, int duration) {
private synchronized void recycleAlbumArt() {
if (albumArt != null) {
//albumArt.recycle();
albumArt = null;
}
}
private void downloadAlbumArt(final String title, final String artist, final String album, final int duration) {
recycleAlbumArt();
albumArtModel = new AlbumArtModel(title, artist, album, AlbumArtModel.Size.Mega, (info, url) -> {
if (albumArtModel.is(artist, album)) {
handler.post(() -> {
if (albumArtRequest != null && albumArtRequest.getRequest() != null) {
albumArtRequest.getRequest().clear();
}
albumArtRequest = Glide
.with(getApplicationContext())
.load(url)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new SimpleTarget<Bitmap>(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) {
@Override
public void onResourceReady(final Bitmap bitmap, GlideAnimation glideAnimation) {
albumArtRequest = null;
if (albumArtModel.is(artist, album)) {
albumArt = bitmap;
updateMetadata(title, artist, album, bitmap, duration);
}
}
});
});
}
});
albumArtModel.fetch();
}
private void updateMetadata(final String title, final String artist, final String album, Bitmap image, final int duration) {
boolean albumArtEnabledInSettings = this.prefs.getBoolean(
Prefs.Key.ALBUM_ART_ENABLED, Prefs.Default.ALBUM_ART_ENABLED);
if (albumArtEnabledInSettings) {
if (!"-".equals(artist) && !"-".equals(album) && !albumArtModel.is(artist, album)) {
downloadAlbumArt(title, artist, album, duration);
}
else if (albumArtModel.is(artist, album)) {
if (image == null && Strings.notEmpty(albumArtModel.getUrl())) {
/* lookup may have failed -- try again. if the fetch is already in
progress this will be a no-op */
albumArtModel.fetch();
}
image = albumArt;
}
else {
recycleAlbumArt();
}
}
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
// .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
// BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, image)
.build());
}

View File

@ -26,6 +26,7 @@ import io.casey.musikcube.remote.playback.ExoPlayerWrapper;
import io.casey.musikcube.remote.playback.MediaPlayerWrapper;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.websocket.Prefs;
import io.casey.musikcube.remote.websocket.WebSocketService;
public class SettingsActivity extends AppCompatActivity {
@ -43,7 +44,7 @@ public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
prefs = this.getSharedPreferences("prefs", MODE_PRIVATE);
prefs = this.getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
setContentView(R.layout.activity_settings);
setTitle(R.string.settings_title);
wasStreaming = isStreamingEnabled();
@ -72,10 +73,10 @@ public class SettingsActivity extends AppCompatActivity {
}
private void rebindUi() {
Views.setTextAndMoveCursorToEnd(this.addressText, prefs.getString("address", "192.168.1.100"));
Views.setTextAndMoveCursorToEnd(this.portText, String.format(Locale.ENGLISH, "%d", prefs.getInt("port", 7905)));
Views.setTextAndMoveCursorToEnd(this.httpPortText, String.format(Locale.ENGLISH, "%d", prefs.getInt("http_port", 7906)));
Views.setTextAndMoveCursorToEnd(this.passwordText, prefs.getString("password", ""));
Views.setTextAndMoveCursorToEnd(this.addressText, prefs.getString(Prefs.Key.ADDRESS, Prefs.Default.ADDRESS));
Views.setTextAndMoveCursorToEnd(this.portText, String.format(Locale.ENGLISH, "%d", prefs.getInt(Prefs.Key.MAIN_PORT, Prefs.Default.MAIN_PORT)));
Views.setTextAndMoveCursorToEnd(this.httpPortText, String.format(Locale.ENGLISH, "%d", prefs.getInt(Prefs.Key.AUDIO_PORT, Prefs.Default.AUDIO_PORT)));
Views.setTextAndMoveCursorToEnd(this.passwordText, prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD));
final ArrayAdapter<CharSequence> playbackModes = ArrayAdapter.createFromResource(
this, R.array.streaming_mode_array, android.R.layout.simple_spinner_item);
@ -89,35 +90,38 @@ public class SettingsActivity extends AppCompatActivity {
bitrates.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
bitrateSpinner.setAdapter(bitrates);
bitrateSpinner.setSelection(prefs.getInt("transcoder_bitrate_index", 0));
bitrateSpinner.setSelection(prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, Prefs.Default.TRANSCODER_BITRATE_INDEX));
final ArrayAdapter<CharSequence> cacheSizes = ArrayAdapter.createFromResource(
this, R.array.disk_cache_array, android.R.layout.simple_spinner_item);
cacheSizes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
cacheSpinner.setAdapter(cacheSizes);
cacheSpinner.setSelection(prefs.getInt("disk_cache_size_index", 0));
cacheSpinner.setSelection(prefs.getInt(Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX));
this.albumArtCheckbox.setChecked(this.prefs.getBoolean("album_art_enabled", true));
this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean("message_compression_enabled", true));
this.softwareVolume.setChecked(this.prefs.getBoolean("software_volume", false));
this.certCheckbox.setChecked(this.prefs.getBoolean("cert_validation_disabled", false));
this.albumArtCheckbox.setChecked(this.prefs.getBoolean(Prefs.Key.ALBUM_ART_ENABLED, Prefs.Default.ALBUM_ART_ENABLED));
this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean(Prefs.Key.MESSAGE_COMPRESSION_ENABLED, Prefs.Default.MESSAGE_COMPRESSION_ENABLED));
this.softwareVolume.setChecked(this.prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME));
Views.setCheckWithoutEvent(
this.sslCheckbox,
this.prefs.getBoolean("ssl_enabled", false),
this.prefs.getBoolean(
Prefs.Key.SSL_ENABLED,
Prefs.Default.SSL_ENABLED),
sslCheckChanged);
Views.setCheckWithoutEvent(
this.certCheckbox,
this.prefs.getBoolean("cert_validation_disabled", false),
this.prefs.getBoolean(
Prefs.Key.CERT_VALIDATION_DISABLED,
Prefs.Default.CERT_VALIDATION_DISABLED),
certValidationChanged);
Views.enableUpNavigation(this);
}
private boolean isStreamingEnabled() {
return this.prefs.getBoolean("streaming_playback", false);
return this.prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
}
private boolean isStreamingSelected() {
@ -185,18 +189,18 @@ public class SettingsActivity extends AppCompatActivity {
final String password = passwordText.getText().toString();
prefs.edit()
.putString("address", addr)
.putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0)
.putInt("http_port", (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0)
.putString("password", password)
.putBoolean("album_art_enabled", albumArtCheckbox.isChecked())
.putBoolean("message_compression_enabled", messageCompressionCheckbox.isChecked())
.putBoolean("streaming_playback", isStreamingSelected())
.putBoolean("software_volume", softwareVolume.isChecked())
.putBoolean("ssl_enabled", sslCheckbox.isChecked())
.putBoolean("cert_validation_disabled", certCheckbox.isChecked())
.putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition())
.putInt("disk_cache_size_index", cacheSpinner.getSelectedItemPosition())
.putString(Prefs.Key.ADDRESS, addr)
.putInt(Prefs.Key.MAIN_PORT, (port.length() > 0) ? Integer.valueOf(port) : 0)
.putInt(Prefs.Key.AUDIO_PORT, (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0)
.putString(Prefs.Key.PASSWORD, password)
.putBoolean(Prefs.Key.ALBUM_ART_ENABLED, albumArtCheckbox.isChecked())
.putBoolean(Prefs.Key.MESSAGE_COMPRESSION_ENABLED, messageCompressionCheckbox.isChecked())
.putBoolean(Prefs.Key.STREAMING_PLAYBACK, isStreamingSelected())
.putBoolean(Prefs.Key.SOFTWARE_VOLUME, softwareVolume.isChecked())
.putBoolean(Prefs.Key.SSL_ENABLED, sslCheckbox.isChecked())
.putBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, certCheckbox.isChecked())
.putInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, bitrateSpinner.getSelectedItemPosition())
.putInt(Prefs.Key.DISK_CACHE_SIZE_INDEX, cacheSpinner.getSelectedItemPosition())
.apply();
if (!softwareVolume.isChecked()) {

View File

@ -15,6 +15,7 @@ import com.uacf.taskrunner.Task;
import io.casey.musikcube.remote.playback.PlaybackService;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.websocket.Prefs;
import io.casey.musikcube.remote.websocket.WebSocketService;
public abstract class WebSocketActivityBase extends AppCompatActivity implements Runner.TaskCallbacks {
@ -32,7 +33,7 @@ public abstract class WebSocketActivityBase extends AppCompatActivity implements
this.runnerDelegate.onCreate(savedInstanceState);
this.wss = WebSocketService.getInstance(this);
this.playback = PlaybackServiceFactory.instance(this);
this.prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE);
this.prefs = getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
}
@Override
@ -68,7 +69,8 @@ public abstract class WebSocketActivityBase extends AppCompatActivity implements
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean streaming = prefs.getBoolean("streaming_playback", false);
boolean streaming = prefs.getBoolean(
Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK);
/* if we're not streaming we want the hardware buttons to go out to the system */
if (!streaming) {

View File

@ -9,6 +9,10 @@ import org.json.JSONObject;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@ -59,19 +63,55 @@ public final class AlbumArtModel {
private AlbumArtCallback callback;
private boolean fetching;
private boolean noart;
private long loadTime = 0;
private int id;
private Size desiredSize;
public AlbumArtModel() {
this("", "", "", null);
public enum Size {
Small("small", 0),
Medium("medium", 1),
Large("large", 2),
ExtraLarge("extralarge", 3),
Mega("mega", 4);
final String name;
final int order;
static Size from(final String value) {
for (Size size : values()) {
if (size.name.equals(value)) {
return size;
}
}
return null;
}
Size(String name, int order) {
this.name = name;
this.order = order;
}
}
public AlbumArtModel(String track, String artist, String album, AlbumArtCallback callback) {
public static class Image {
final String url;
final Size size;
public Image(final Size size, final String url) {
this.url = url;
this.size = size;
}
}
public static AlbumArtModel empty() {
return new AlbumArtModel("", "", "", Size.Small, null);
}
public AlbumArtModel(String track, String artist, String album, Size size, AlbumArtCallback callback) {
this.track = track;
this.artist = artist;
this.album = album;
this.callback = callback != null ? callback : (info, url) -> { };
this.id = (artist + album).hashCode();
this.desiredSize = size;
this.id = (artist + album + size.name).hashCode();
synchronized (this) {
this.url = URL_CACHE.get(id);
@ -99,18 +139,15 @@ public final class AlbumArtModel {
return this.url;
}
public synchronized long getLoadTimeMillis() {
return this.loadTime;
}
public int getId() {
return id;
}
public synchronized void fetch() {
public synchronized AlbumArtModel fetch() {
if (this.fetching || this.noart) {
return;
return this;
}
if (!Strings.empty(this.url)) {
callback.onFinished(this, this.url);
}
@ -130,7 +167,6 @@ public final class AlbumArtModel {
throw new RuntimeException(ex);
}
final long start = System.currentTimeMillis();
this.fetching = true;
final Request request = new Request.Builder().url(requestUrl).build();
@ -144,27 +180,53 @@ public final class AlbumArtModel {
@Override
public void onResponse(Call call, Response response) throws IOException {
synchronized (AlbumArtModel.this) {
List<Image> imageList = new ArrayList<>();
try {
final JSONObject json = new JSONObject(response.body().string());
final JSONArray images = json.getJSONObject("album").getJSONArray("image");
for (int i = images.length() - 1; i >= 0; i--) {
final JSONObject image = images.getJSONObject(i);
final String size = image.optString("size", "");
if (size != null && size.length() > 0) {
final String resolvedUrl = image.optString("#text", "");
if (resolvedUrl != null && resolvedUrl.length() > 0) {
synchronized (AlbumArtModel.this) {
URL_CACHE.put(id, resolvedUrl);
}
AlbumArtModel.this.url = resolvedUrl;
loadTime = System.currentTimeMillis() - start;
callback.onFinished(AlbumArtModel.this, resolvedUrl);
return;
final JSONArray imagesJson = json.getJSONObject("album").getJSONArray("image");
for (int i = 0; i < imagesJson.length(); i++) {
final JSONObject imageJson = imagesJson.getJSONObject(i);
final Size size = Size.from(imageJson.optString("size", ""));
if (size != null) {
final String resolvedUrl = imageJson.optString("#text", "");
if (Strings.notEmpty(resolvedUrl)) {
imageList.add(new Image(size, resolvedUrl));
}
}
}
} catch (JSONException ex) {
if (imageList.size() > 0) {
/* find the image with the closest to the requested size.
exact match preferred. */
Size desiredSize = Size.Mega;
Image closest = imageList.get(0);
int lastDelta = Integer.MAX_VALUE;
for (final Image check : imageList) {
if (check.size == desiredSize) {
closest = check;
break;
}
else {
int delta = Math.abs(desiredSize.order - check.size.order);
if (lastDelta > delta) {
closest = check;
lastDelta = delta;
}
}
}
synchronized (AlbumArtModel.this) {
URL_CACHE.put(id, closest.url);
}
fetching = false;
AlbumArtModel.this.url = closest.url;
callback.onFinished(AlbumArtModel.this, closest.url);
return;
}
}
catch (JSONException ex) {
}
noart = true; /* got a response, but it was invalid. we won't try again */
@ -178,6 +240,8 @@ public final class AlbumArtModel {
else {
callback.onFinished(this, null);
}
return this;
}
private static final Pattern[] BAD_PATTERNS = {

View File

@ -0,0 +1,35 @@
package io.casey.musikcube.remote.websocket;
public interface Prefs {
String NAME = "prefs";
interface Key {
String ADDRESS = "address";
String MAIN_PORT = "port";
String AUDIO_PORT = "http_port";
String PASSWORD = "password";
String ALBUM_ART_ENABLED = "album_art_enabled";
String MESSAGE_COMPRESSION_ENABLED = "message_compression_enabled";
String STREAMING_PLAYBACK = "streaming_playback";
String SOFTWARE_VOLUME = "software_volume";
String SSL_ENABLED = "ssl_enabled";
String CERT_VALIDATION_DISABLED = "cert_validation_disabled";
String TRANSCODER_BITRATE_INDEX = "transcoder_bitrate_index";
String DISK_CACHE_SIZE_INDEX = "disk_cache_size_index";
}
interface Default {
String ADDRESS = "192.168.1.100";
int MAIN_PORT = 7905;
int AUDIO_PORT = 7906;
String PASSWORD = "";
boolean ALBUM_ART_ENABLED = true;
boolean MESSAGE_COMPRESSION_ENABLED = true;
boolean STREAMING_PLAYBACK = false;
boolean SOFTWARE_VOLUME = false;
boolean SSL_ENABLED = false;
boolean CERT_VALIDATION_DISABLED = false;
int TRANSCODER_BITRATE_INDEX = 0;
int DISK_CACHE_SIZE_INDEX = 0;
}
}

View File

@ -180,7 +180,7 @@ public class WebSocketService {
private WebSocketService(final Context context) {
this.context = context.getApplicationContext();
this.prefs = this.context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
this.prefs = this.context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
handler.sendEmptyMessageDelayed(MESSAGE_REMOVE_OLD_CALLBACKS, CALLBACK_TIMEOUT_MILLIS);
}
@ -342,8 +342,8 @@ public class WebSocketService {
}
public boolean hasValidConnection() {
final String addr = prefs.getString("address", "");
final int port = prefs.getInt("port", -1);
final String addr = prefs.getString(Prefs.Key.ADDRESS, "");
final int port = prefs.getInt(Prefs.Key.MAIN_PORT, -1);
return (addr.length() > 0 && port >= 0);
}
@ -518,23 +518,24 @@ public class WebSocketService {
try {
final WebSocketFactory factory = new WebSocketFactory();
if (prefs.getBoolean("cert_validation_disabled", false)) {
if (prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(factory);
}
final String protocol = prefs.getBoolean("ssl_enabled", false) ? "wss" : "ws";
final String protocol = prefs.getBoolean(
Prefs.Key.SSL_ENABLED, Prefs.Default.SSL_ENABLED) ? "wss" : "ws";
final String host = String.format(
Locale.ENGLISH,
"%s://%s:%d",
protocol,
prefs.getString("address", "192.168.1.100"),
prefs.getInt("port", 7905));
prefs.getString(Prefs.Key.ADDRESS, Prefs.Default.ADDRESS),
prefs.getInt(Prefs.Key.MAIN_PORT, Prefs.Default.MAIN_PORT));
socket = factory.createSocket(host, CONNECTION_TIMEOUT_MILLIS);
socket.addListener(webSocketAdapter);
if (prefs.getBoolean("message_compression_enabled", true)) {
if (prefs.getBoolean(Prefs.Key.MESSAGE_COMPRESSION_ENABLED, Prefs.Default.MESSAGE_COMPRESSION_ENABLED)) {
socket.addExtension(WebSocketExtension.PERMESSAGE_DEFLATE);
}
@ -544,7 +545,7 @@ public class WebSocketService {
/* authenticate */
final String auth = SocketMessage.Builder
.request(Messages.Request.Authenticate)
.addOption("password", prefs.getString("password", ""))
.addOption("password", prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD))
.build()
.toString();