diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/MainActivity.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/MainActivity.java index c4ba4349d..1ac1a3c57 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/MainActivity.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/MainActivity.java @@ -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() { @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 diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java index ec1ec4a9f..0b6e8e4ac 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java @@ -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 */ diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlaybackServiceFactory.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlaybackServiceFactory.java index 3f669af51..704b89bc1 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlaybackServiceFactory.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlaybackServiceFactory.java @@ -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); } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlayerWrapper.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlayerWrapper.java index ae9ba0d08..ae0ab642b 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlayerWrapper.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/PlayerWrapper.java @@ -1,5 +1,7 @@ package io.casey.musikcube.remote.playback; +import android.util.Log; + import java.util.HashSet; import java.util.Set; diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java index 8b5263ede..917f0c224 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java @@ -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(); diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/SystemService.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/SystemService.java index ca4f1fa6d..31bfd56cd 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/SystemService.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/SystemService.java @@ -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 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(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()); } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java index 98dc52ded..a053cd0f2 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java @@ -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 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 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()) { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/WebSocketActivityBase.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/WebSocketActivityBase.java index 50411f9cd..2154ed1df 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/WebSocketActivityBase.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/WebSocketActivityBase.java @@ -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) { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/model/AlbumArtModel.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/model/AlbumArtModel.java index dd6a7947a..9ee8959dd 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/model/AlbumArtModel.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/model/AlbumArtModel.java @@ -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 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 = { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/Prefs.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/Prefs.java new file mode 100644 index 000000000..a21bd294e --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/Prefs.java @@ -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; + } +} diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/WebSocketService.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/WebSocketService.java index cf437e932..86b79767d 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/WebSocketService.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/websocket/WebSocketService.java @@ -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();