From a1931cb6061008aaecace55967503ab45a10eb37 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 24 Feb 2026 03:59:17 +0100 Subject: [PATCH] Add Now Playing feature to GroupItem and Stream control functionality - Implemented UI elements for displaying current track information including title, artist, and album art in GroupItem. - Added functionality to control playback (previous, play/pause, next) from the GroupItem. - Updated GroupListFragment to manage visibility of Now Playing UI based on stream status. - Introduced StreamProperties class to handle metadata and control capabilities for streams. - Enhanced RemoteControl to handle stream control commands and property updates. - Updated layout files to accommodate new UI elements for playback controls. --- .../java/de/badaix/snapcast/GroupItem.java | 117 ++++++- .../de/badaix/snapcast/GroupListFragment.java | 12 + .../java/de/badaix/snapcast/MainActivity.java | 23 ++ .../snapcast/control/RemoteControl.java | 25 ++ .../snapcast/control/json/ServerStatus.java | 2 + .../badaix/snapcast/control/json/Stream.java | 18 +- .../control/json/StreamProperties.java | 318 ++++++++++++++++++ .../src/main/res/drawable/ic_pause_24px.xml | 10 + .../main/res/drawable/ic_play_arrow_24px.xml | 10 + .../main/res/drawable/ic_skip_next_24px.xml | 10 + .../res/drawable/ic_skip_previous_24px.xml | 10 + Snapcast/src/main/res/layout/group_item.xml | 146 +++++--- 12 files changed, 647 insertions(+), 54 deletions(-) create mode 100644 Snapcast/src/main/java/de/badaix/snapcast/control/json/StreamProperties.java create mode 100644 Snapcast/src/main/res/drawable/ic_pause_24px.xml create mode 100644 Snapcast/src/main/res/drawable/ic_play_arrow_24px.xml create mode 100644 Snapcast/src/main/res/drawable/ic_skip_next_24px.xml create mode 100644 Snapcast/src/main/res/drawable/ic_skip_previous_24px.xml diff --git a/Snapcast/src/main/java/de/badaix/snapcast/GroupItem.java b/Snapcast/src/main/java/de/badaix/snapcast/GroupItem.java index 3c8a1048..88b17f94 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/GroupItem.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/GroupItem.java @@ -21,11 +21,14 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; @@ -37,6 +40,7 @@ import de.badaix.snapcast.control.json.Group; import de.badaix.snapcast.control.json.ServerStatus; import de.badaix.snapcast.control.json.Stream; +import de.badaix.snapcast.control.json.StreamProperties; import de.badaix.snapcast.control.json.Volume; /** @@ -48,7 +52,6 @@ public class GroupItem extends LinearLayout implements SeekBar.OnSeekBarChangeLi private static final String TAG = "GroupItem"; - // private TextView title; private final SeekBar volumeSeekBar; private final ImageButton ibMute; private final ImageButton ibSettings; @@ -58,7 +61,15 @@ public class GroupItem extends LinearLayout implements SeekBar.OnSeekBarChangeLi private TextView tvStreamName = null; private GroupItemListener listener = null; private final LinearLayout llVolume; + private final LinearLayout llNowPlaying; + private final ImageView ivAlbumArt; + private final TextView tvTrackTitle; + private final TextView tvTrackArtist; + private final ImageButton ibPrevious; + private final ImageButton ibPlayPause; + private final ImageButton ibNext; private boolean hideOffline = false; + private boolean showNowPlaying = true; private Vector clientItems = null; private Vector clientVolumes = null; private int groupVolume = 0; @@ -68,7 +79,6 @@ public GroupItem(Context context, ServerStatus server, Group group) { LayoutInflater vi = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); vi.inflate(R.layout.group_item, this); -// title = (TextView) findViewById(R.id.title); volumeSeekBar = findViewById(R.id.volumeSeekBar); ibMute = findViewById(R.id.ibMute); ibMute.setImageResource(R.drawable.volume_up_24px); @@ -86,6 +96,18 @@ public void onClick(View view) { streamChoiceDialog(); } }); + + llNowPlaying = findViewById(R.id.llNowPlaying); + ivAlbumArt = findViewById(R.id.ivAlbumArt); + tvTrackTitle = findViewById(R.id.tvTrackTitle); + tvTrackArtist = findViewById(R.id.tvTrackArtist); + ibPrevious = findViewById(R.id.ibPrevious); + ibPlayPause = findViewById(R.id.ibPlayPause); + ibNext = findViewById(R.id.ibNext); + ibPrevious.setOnClickListener(this); + ibPlayPause.setOnClickListener(this); + ibNext.setOnClickListener(this); + volumeSeekBar.setOnSeekBarChangeListener(this); volumeSeekBar.setOnTouchListener(this); volumeSeekBar.setOnFocusChangeListener(this); @@ -124,18 +146,60 @@ private void update() { if ((tvStreamName == null) || (stream == null)) return; tvStreamName.setText(stream.getName()); -/* String codec = stream.getUri().getQuery().get("codec"); - if (codec.contains(":")) - codec = codec.split(":")[0]; - tvStreamState.setText(stream.getUri().getQuery().get("sampleformat") + " - " + codec + " - " + stream.getStatus().toString()); - - title.setEnabled(group.isConnected()); - volumeSeekBar.setProgress(group.getConfig().getVolume().getPercent()); - if (client.getConfig().getVolume().isMuted()) - ibMute.setImageResource(R.drawable.ic_mute_icon); - else - ibMute.setImageResource(R.drawable.ic_speaker_icon); -*/ + updateNowPlaying(stream); + } + + private void updateNowPlaying(Stream stream) { + StreamProperties props = stream.getProperties(); + if (!showNowPlaying || props == null || (!props.hasMetadata() && !props.canControl())) { + llNowPlaying.setVisibility(GONE); + return; + } + + llNowPlaying.setVisibility(VISIBLE); + + if (props.hasMetadata()) { + String title = props.getTitle(); + String artist = props.getArtistString(); + tvTrackTitle.setText(TextUtils.isEmpty(title) ? "" : title); + tvTrackTitle.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE); + tvTrackArtist.setText(TextUtils.isEmpty(artist) ? props.getAlbum() : artist); + tvTrackArtist.setVisibility(TextUtils.isEmpty(artist) && TextUtils.isEmpty(props.getAlbum()) ? GONE : VISIBLE); + + Bitmap art = props.getArtBitmap(); + if (art != null) { + ivAlbumArt.setImageBitmap(art); + ivAlbumArt.setVisibility(VISIBLE); + } else { + ivAlbumArt.setVisibility(GONE); + } + } else { + tvTrackTitle.setVisibility(GONE); + tvTrackArtist.setVisibility(GONE); + ivAlbumArt.setVisibility(GONE); + } + + if (props.canControl()) { + ibPrevious.setVisibility(VISIBLE); + ibPlayPause.setVisibility(VISIBLE); + ibNext.setVisibility(VISIBLE); + ibPrevious.setEnabled(props.canGoPrevious()); + ibPrevious.setAlpha(props.canGoPrevious() ? 1.0f : 0.3f); + ibNext.setEnabled(props.canGoNext()); + ibNext.setAlpha(props.canGoNext() ? 1.0f : 0.3f); + + if (props.isPlaying()) { + ibPlayPause.setImageResource(R.drawable.ic_pause_24px); + } else { + ibPlayPause.setImageResource(R.drawable.ic_play_arrow_24px); + } + ibPlayPause.setEnabled(props.canPause() || props.canPlay()); + ibPlayPause.setAlpha((props.canPause() || props.canPlay()) ? 1.0f : 0.3f); + } else { + ibPrevious.setVisibility(GONE); + ibPlayPause.setVisibility(GONE); + ibNext.setVisibility(GONE); + } } private void updateVolume() { @@ -167,6 +231,13 @@ public void setHideOffline(boolean hideOffline) { update(); } + public void setShowNowPlaying(boolean show) { + if (this.showNowPlaying == show) + return; + this.showNowPlaying = show; + update(); + } + @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (!fromUser) @@ -231,6 +302,22 @@ public void onClick(View v) { listener.onMute(this, group.isMuted()); } else if (v == ibSettings) { listener.onPropertiesClicked(this); + } else if (v == ibPrevious || v == ibPlayPause || v == ibNext) { + if (listener == null) + return; + String streamId = group.getStreamId(); + if (v == ibPrevious) { + listener.onStreamControl(streamId, "previous"); + } else if (v == ibNext) { + listener.onStreamControl(streamId, "next"); + } else { + Stream stream = server.getStream(streamId); + if (stream != null && stream.getProperties() != null && stream.getProperties().isPlaying()) { + listener.onStreamControl(streamId, "pause"); + } else { + listener.onStreamControl(streamId, "play"); + } + } } } @@ -307,6 +394,8 @@ public interface GroupItemListener { void onPropertiesClicked(GroupItem group); void onGroupStreamChanged(Group group, String streamId); + + void onStreamControl(String streamId, String command); } } diff --git a/Snapcast/src/main/java/de/badaix/snapcast/GroupListFragment.java b/Snapcast/src/main/java/de/badaix/snapcast/GroupListFragment.java index 6a944163..977374be 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/GroupListFragment.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/GroupListFragment.java @@ -142,6 +142,18 @@ View getView(int position, @Nullable View convertView, } groupItem.setHideOffline(hideOffline); groupItem.setListener(listener); + + boolean isFirstForStream = true; + String streamId = group.getStreamId(); + for (int i = 0; i < position; i++) { + Group prev = getItem(i); + if (prev != null && streamId.equals(prev.getStreamId())) { + isFirstForStream = false; + break; + } + } + groupItem.setShowNowPlaying(isFirstForStream); + return groupItem; } diff --git a/Snapcast/src/main/java/de/badaix/snapcast/MainActivity.java b/Snapcast/src/main/java/de/badaix/snapcast/MainActivity.java index 68a8ed0d..27d6efbb 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/MainActivity.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/MainActivity.java @@ -61,6 +61,7 @@ import de.badaix.snapcast.control.json.Group; import de.badaix.snapcast.control.json.ServerStatus; import de.badaix.snapcast.control.json.Stream; +import de.badaix.snapcast.control.json.StreamProperties; import de.badaix.snapcast.control.json.Volume; import de.badaix.snapcast.utils.NsdHelper; import de.badaix.snapcast.utils.Settings; @@ -758,4 +759,26 @@ public void onUpdate(String streamId, Stream stream) { serverStatus.updateStream(stream); groupListFragment.updateServer(serverStatus); } + + @Override + public void onStreamPropertiesChanged(String streamId, StreamProperties properties) { + Stream stream = serverStatus.getStream(streamId); + if (stream == null) { + remoteControl.getServerStatus(); + return; + } + StreamProperties existing = stream.getProperties(); + if (existing != null) { + existing.merge(properties); + } else { + stream.setProperties(properties); + } + groupListFragment.updateServer(serverStatus); + } + + @Override + public void onStreamControl(String streamId, String command) { + if (remoteControl != null) + remoteControl.controlStream(streamId, command); + } } diff --git a/Snapcast/src/main/java/de/badaix/snapcast/control/RemoteControl.java b/Snapcast/src/main/java/de/badaix/snapcast/control/RemoteControl.java index 87e4b998..b709e91b 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/control/RemoteControl.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/control/RemoteControl.java @@ -31,6 +31,7 @@ import de.badaix.snapcast.control.json.Group; import de.badaix.snapcast.control.json.ServerStatus; import de.badaix.snapcast.control.json.Stream; +import de.badaix.snapcast.control.json.StreamProperties; import de.badaix.snapcast.control.json.Volume; /** @@ -159,6 +160,8 @@ private void processJson(JSONObject json) { listener.onUpdate(new ServerStatus(response.result.getJSONObject("server"))); } else if (request.method.equals("Server.DeleteClient")) { listener.onUpdate(new ServerStatus(response.result.getJSONObject("server"))); + } else if (request.method.equals("Stream.Control")) { + // response is just "ok", nothing to propagate } } else { /// Notification @@ -183,6 +186,12 @@ private void processJson(JSONObject json) { listener.onStreamChanged(rpcEvent, notification.params.getString("id"), notification.params.getString("stream_id")); } else if (notification.method.equals("Stream.OnUpdate")) { listener.onUpdate(notification.params.getString("id"), new Stream(notification.params.getJSONObject("stream"))); + } else if (notification.method.equals("Stream.OnProperties")) { + String streamId = notification.params.getString("id"); + JSONObject propsJson = notification.params.has("properties") + ? notification.params.getJSONObject("properties") + : notification.params; + listener.onStreamPropertiesChanged(streamId, new StreamProperties(propsJson)); } else if (notification.method.equals("Group.OnUpdate")) { listener.onUpdate(new Group(notification.params.getJSONObject("group"))); } else if (notification.method.equals("Server.OnUpdate")) { @@ -347,6 +356,20 @@ public void delete(Client client) { } } + public void controlStream(String streamId, String command) { + try { + JSONObject params = new JSONObject(); + params.put("id", streamId); + params.put("command", command); + params.put("params", new JSONObject()); + RPCRequest request = jsonRequest("Stream.Control", params); + if (isConnected()) + tcpClient.sendMessage(request.toString()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + public enum ClientEvent { connected("Client.OnConnect"), disconnected("Client.OnDisconnect"), @@ -399,6 +422,8 @@ public interface GroupListener { public interface StreamListener { void onUpdate(String streamId, Stream stream); + + void onStreamPropertiesChanged(String streamId, StreamProperties properties); } public interface ServerListener { diff --git a/Snapcast/src/main/java/de/badaix/snapcast/control/json/ServerStatus.java b/Snapcast/src/main/java/de/badaix/snapcast/control/json/ServerStatus.java index 2a91f265..2876ad33 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/control/json/ServerStatus.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/control/json/ServerStatus.java @@ -144,6 +144,8 @@ public boolean updateStream(Stream stream) { if (stream.getId().equals(s.getId())) { if (s.equals(stream)) return false; + if (stream.getProperties() == null && s.getProperties() != null) + stream.setProperties(s.getProperties()); streams.set(i, stream); return true; } diff --git a/Snapcast/src/main/java/de/badaix/snapcast/control/json/Stream.java b/Snapcast/src/main/java/de/badaix/snapcast/control/json/Stream.java index d08546e6..e2f2529c 100644 --- a/Snapcast/src/main/java/de/badaix/snapcast/control/json/Stream.java +++ b/Snapcast/src/main/java/de/badaix/snapcast/control/json/Stream.java @@ -30,6 +30,7 @@ public class Stream implements JsonSerialisable { private StreamUri uri; private String id; private Status status; + private StreamProperties properties; public Stream(JSONObject json) { fromJson(json); @@ -47,6 +48,9 @@ public void fromJson(JSONObject json) { id = json.getString("id"); status = Status.unknown; } + if (json.has("properties")) { + properties = new StreamProperties(json.getJSONObject("properties")); + } } catch (JSONException e) { e.printStackTrace(); } @@ -59,6 +63,8 @@ public JSONObject toJson() { json.put("uri", uri.toJson()); json.put("id", id); json.put("status", status); + if (properties != null) + json.put("properties", properties.toJson()); } catch (JSONException e) { e.printStackTrace(); } @@ -74,7 +80,8 @@ public boolean equals(Object o) { if (!Objects.equals(uri, stream.uri)) return false; if (!Objects.equals(id, stream.id)) return false; - return Objects.equals(status, stream.status); + if (!Objects.equals(status, stream.status)) return false; + return Objects.equals(properties, stream.properties); } @Override @@ -82,6 +89,7 @@ public int hashCode() { int result = uri != null ? uri.hashCode() : 0; result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (status != null ? status.hashCode() : 0); + result = 31 * result + (properties != null ? properties.hashCode() : 0); return result; } @@ -113,6 +121,14 @@ public String getName() { return uri.getName(); } + public StreamProperties getProperties() { + return properties; + } + + public void setProperties(StreamProperties properties) { + this.properties = properties; + } + @Override public String toString() { return toJson().toString(); diff --git a/Snapcast/src/main/java/de/badaix/snapcast/control/json/StreamProperties.java b/Snapcast/src/main/java/de/badaix/snapcast/control/json/StreamProperties.java new file mode 100644 index 00000000..6a412853 --- /dev/null +++ b/Snapcast/src/main/java/de/badaix/snapcast/control/json/StreamProperties.java @@ -0,0 +1,318 @@ +package de.badaix.snapcast.control.json; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.text.TextUtils; +import android.util.Base64; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Objects; + +public class StreamProperties implements JsonSerialisable { + private boolean canControl = false; + private boolean canGoNext = false; + private boolean canGoPrevious = false; + private boolean canPlay = false; + private boolean canPause = false; + private boolean canSeek = false; + + private String playbackStatus = "unknown"; + private double position = 0; + private int volume = -1; + private boolean shuffle = false; + private boolean mute = false; + private String loopStatus = "none"; + private double rate = 1.0; + + private String title = ""; + private ArrayList artist = new ArrayList<>(); + private String album = ""; + private ArrayList albumArtist = new ArrayList<>(); + private String artUrl = ""; + private String artDataBase64 = ""; + private String artDataExtension = ""; + private double duration = 0; + private String url = ""; + private int trackNumber = -1; + private int discNumber = -1; + private ArrayList genre = new ArrayList<>(); + + private Bitmap cachedArtBitmap = null; + private boolean artBitmapDecoded = false; + + private JSONObject rawJson = null; + + public StreamProperties() { + } + + public StreamProperties(JSONObject json) { + fromJson(json); + } + + @Override + public void fromJson(JSONObject json) { + rawJson = json; + + canControl = json.optBoolean("canControl", false); + canGoNext = json.optBoolean("canGoNext", false); + canGoPrevious = json.optBoolean("canGoPrevious", false); + canPlay = json.optBoolean("canPlay", false); + canPause = json.optBoolean("canPause", false); + canSeek = json.optBoolean("canSeek", false); + + playbackStatus = json.optString("playbackStatus", "unknown"); + position = json.optDouble("position", 0); + volume = json.optInt("volume", -1); + shuffle = json.optBoolean("shuffle", false); + mute = json.optBoolean("mute", false); + loopStatus = json.optString("loopStatus", "none"); + rate = json.optDouble("rate", 1.0); + + JSONObject metadata = json.optJSONObject("metadata"); + if (metadata != null) { + parseMetadata(metadata); + } + } + + private void parseMetadata(JSONObject metadata) { + title = metadata.optString("title", ""); + album = metadata.optString("album", ""); + artUrl = metadata.optString("artUrl", ""); + url = metadata.optString("url", ""); + duration = metadata.optDouble("duration", 0); + trackNumber = metadata.optInt("trackNumber", -1); + discNumber = metadata.optInt("discNumber", -1); + + artist = parseStringArray(metadata, "artist"); + albumArtist = parseStringArray(metadata, "albumArtist"); + genre = parseStringArray(metadata, "genre"); + + JSONObject artData = metadata.optJSONObject("artData"); + if (artData != null) { + artDataBase64 = artData.optString("data", ""); + artDataExtension = artData.optString("extension", ""); + cachedArtBitmap = null; + artBitmapDecoded = false; + } + } + + private ArrayList parseStringArray(JSONObject json, String key) { + ArrayList result = new ArrayList<>(); + try { + if (json.has(key)) { + Object value = json.get(key); + if (value instanceof JSONArray) { + JSONArray arr = (JSONArray) value; + for (int i = 0; i < arr.length(); i++) { + result.add(arr.getString(i)); + } + } else if (value instanceof String) { + result.add((String) value); + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + return result; + } + + /** + * Merge incoming properties into this instance. + * Only fields present in the incoming JSON are overwritten, + * preserving previously known values (e.g. metadata survives + * a capabilities-only update). + */ + public void merge(StreamProperties other) { + if (other.rawJson == null) + return; + + JSONObject json = other.rawJson; + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = keys.next(); + switch (key) { + case "canControl": canControl = other.canControl; break; + case "canGoNext": canGoNext = other.canGoNext; break; + case "canGoPrevious": canGoPrevious = other.canGoPrevious; break; + case "canPlay": canPlay = other.canPlay; break; + case "canPause": canPause = other.canPause; break; + case "canSeek": canSeek = other.canSeek; break; + case "playbackStatus": playbackStatus = other.playbackStatus; break; + case "position": position = other.position; break; + case "volume": volume = other.volume; break; + case "shuffle": shuffle = other.shuffle; break; + case "mute": mute = other.mute; break; + case "loopStatus": loopStatus = other.loopStatus; break; + case "rate": rate = other.rate; break; + case "metadata": + title = other.title; + artist = other.artist; + album = other.album; + albumArtist = other.albumArtist; + artUrl = other.artUrl; + artDataBase64 = other.artDataBase64; + artDataExtension = other.artDataExtension; + duration = other.duration; + url = other.url; + trackNumber = other.trackNumber; + discNumber = other.discNumber; + genre = other.genre; + cachedArtBitmap = null; + artBitmapDecoded = false; + break; + } + } + } + + @Override + public JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + json.put("canControl", canControl); + json.put("canGoNext", canGoNext); + json.put("canGoPrevious", canGoPrevious); + json.put("canPlay", canPlay); + json.put("canPause", canPause); + json.put("canSeek", canSeek); + json.put("playbackStatus", playbackStatus); + json.put("position", position); + json.put("shuffle", shuffle); + json.put("mute", mute); + json.put("loopStatus", loopStatus); + json.put("rate", rate); + if (volume >= 0) + json.put("volume", volume); + + JSONObject metadata = new JSONObject(); + if (!TextUtils.isEmpty(title)) metadata.put("title", title); + if (!TextUtils.isEmpty(album)) metadata.put("album", album); + if (!TextUtils.isEmpty(artUrl)) metadata.put("artUrl", artUrl); + if (!TextUtils.isEmpty(url)) metadata.put("url", url); + if (duration > 0) metadata.put("duration", duration); + if (trackNumber >= 0) metadata.put("trackNumber", trackNumber); + if (discNumber >= 0) metadata.put("discNumber", discNumber); + if (!artist.isEmpty()) { + JSONArray arr = new JSONArray(); + for (String a : artist) arr.put(a); + metadata.put("artist", arr); + } + if (!albumArtist.isEmpty()) { + JSONArray arr = new JSONArray(); + for (String a : albumArtist) arr.put(a); + metadata.put("albumArtist", arr); + } + if (!genre.isEmpty()) { + JSONArray arr = new JSONArray(); + for (String g : genre) arr.put(g); + metadata.put("genre", arr); + } + if (!TextUtils.isEmpty(artDataBase64)) { + JSONObject artData = new JSONObject(); + artData.put("data", artDataBase64); + artData.put("extension", artDataExtension); + metadata.put("artData", artData); + } + if (metadata.length() > 0) + json.put("metadata", metadata); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + + public Bitmap getArtBitmap() { + if (artBitmapDecoded) + return cachedArtBitmap; + artBitmapDecoded = true; + if (TextUtils.isEmpty(artDataBase64)) { + cachedArtBitmap = null; + return null; + } + try { + byte[] decoded = Base64.decode(artDataBase64, Base64.DEFAULT); + cachedArtBitmap = BitmapFactory.decodeByteArray(decoded, 0, decoded.length); + } catch (Exception e) { + e.printStackTrace(); + cachedArtBitmap = null; + } + return cachedArtBitmap; + } + + public boolean hasMetadata() { + return !TextUtils.isEmpty(title) || !artist.isEmpty() || !TextUtils.isEmpty(album); + } + + public String getArtistString() { + return TextUtils.join(", ", artist); + } + + // Capability getters + public boolean canControl() { return canControl; } + public boolean canGoNext() { return canGoNext; } + public boolean canGoPrevious() { return canGoPrevious; } + public boolean canPlay() { return canPlay; } + public boolean canPause() { return canPause; } + public boolean canSeek() { return canSeek; } + + // State getters + public String getPlaybackStatus() { return playbackStatus; } + public boolean isPlaying() { return "playing".equalsIgnoreCase(playbackStatus); } + public boolean isPaused() { return "paused".equalsIgnoreCase(playbackStatus); } + public double getPosition() { return position; } + public int getVolume() { return volume; } + public boolean isShuffle() { return shuffle; } + public boolean isMute() { return mute; } + public String getLoopStatus() { return loopStatus; } + public double getRate() { return rate; } + + // Metadata getters + public String getTitle() { return title; } + public ArrayList getArtist() { return artist; } + public String getAlbum() { return album; } + public ArrayList getAlbumArtist() { return albumArtist; } + public String getArtUrl() { return artUrl; } + public double getDuration() { return duration; } + public String getUrl() { return url; } + public int getTrackNumber() { return trackNumber; } + public int getDiscNumber() { return discNumber; } + public ArrayList getGenre() { return genre; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StreamProperties that = (StreamProperties) o; + return canControl == that.canControl && + canGoNext == that.canGoNext && + canGoPrevious == that.canGoPrevious && + canPlay == that.canPlay && + canPause == that.canPause && + canSeek == that.canSeek && + Objects.equals(playbackStatus, that.playbackStatus) && + Objects.equals(title, that.title) && + Objects.equals(artist, that.artist) && + Objects.equals(album, that.album) && + Objects.equals(artUrl, that.artUrl) && + Objects.equals(artDataBase64, that.artDataBase64); + } + + @Override + public int hashCode() { + int result = (canControl ? 1 : 0); + result = 31 * result + (playbackStatus != null ? playbackStatus.hashCode() : 0); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (artist != null ? artist.hashCode() : 0); + result = 31 * result + (album != null ? album.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return toJson().toString(); + } +} diff --git a/Snapcast/src/main/res/drawable/ic_pause_24px.xml b/Snapcast/src/main/res/drawable/ic_pause_24px.xml new file mode 100644 index 00000000..13d6d2ec --- /dev/null +++ b/Snapcast/src/main/res/drawable/ic_pause_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/Snapcast/src/main/res/drawable/ic_play_arrow_24px.xml b/Snapcast/src/main/res/drawable/ic_play_arrow_24px.xml new file mode 100644 index 00000000..13c137a9 --- /dev/null +++ b/Snapcast/src/main/res/drawable/ic_play_arrow_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/Snapcast/src/main/res/drawable/ic_skip_next_24px.xml b/Snapcast/src/main/res/drawable/ic_skip_next_24px.xml new file mode 100644 index 00000000..6edde851 --- /dev/null +++ b/Snapcast/src/main/res/drawable/ic_skip_next_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/Snapcast/src/main/res/drawable/ic_skip_previous_24px.xml b/Snapcast/src/main/res/drawable/ic_skip_previous_24px.xml new file mode 100644 index 00000000..1805b7d1 --- /dev/null +++ b/Snapcast/src/main/res/drawable/ic_skip_previous_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/Snapcast/src/main/res/layout/group_item.xml b/Snapcast/src/main/res/layout/group_item.xml index 7d43da2b..e73df82f 100644 --- a/Snapcast/src/main/res/layout/group_item.xml +++ b/Snapcast/src/main/res/layout/group_item.xml @@ -20,9 +20,7 @@ xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> - - - + + @@ -48,53 +47,122 @@ android:background="@null" android:src="@drawable/settings_24px" /> - + android:layout_toStartOf="@+id/ibSettings" + android:singleLine="true" + android:fadingEdge="horizontal" + android:paddingBottom="1dp" + android:paddingLeft="5dp" + android:paddingTop="2dp" + android:text="Stream name" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + + + + + + + + - - - - - - - - + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceSmall" /> - + + + + + + + + + + + + + + + + + -