mirror of
https://github.com/clangen/musikcube.git
synced 2025-03-29 19:20:28 +00:00
Android client updates that support displaying time (including syncing
and extrapolating from server timestamps)
This commit is contained in:
parent
d7cbec1058
commit
629f8cb4a7
@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@ -27,6 +28,7 @@ import io.casey.musikcube.remote.ui.fragment.InvalidPasswordDialogFragment;
|
||||
import io.casey.musikcube.remote.ui.util.Views;
|
||||
import io.casey.musikcube.remote.ui.view.LongPressTextView;
|
||||
import io.casey.musikcube.remote.ui.view.MainMetadataView;
|
||||
import io.casey.musikcube.remote.util.Duration;
|
||||
import io.casey.musikcube.remote.websocket.Messages;
|
||||
import io.casey.musikcube.remote.websocket.Prefs;
|
||||
import io.casey.musikcube.remote.websocket.SocketMessage;
|
||||
@ -37,11 +39,12 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
|
||||
private WebSocketService wss = null;
|
||||
|
||||
private Handler handler = new Handler();
|
||||
private SharedPreferences prefs;
|
||||
private PlaybackService playback;
|
||||
|
||||
private MainMetadataView metadataView;
|
||||
private TextView playPause;
|
||||
private TextView playPause, currentTime, totalTime;
|
||||
private View connectedNotPlaying, disconnectedButton;
|
||||
private CheckBox shuffleCb, muteCb, repeatCb;
|
||||
private View disconnectedOverlay;
|
||||
@ -79,6 +82,7 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
super.onPause();
|
||||
metadataView.onPause();
|
||||
unbindCheckboxEventListeners();
|
||||
handler.removeCallbacks(updateTimeRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -88,6 +92,7 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
metadataView.onResume();
|
||||
bindCheckBoxEventListeners();
|
||||
rebindUi();
|
||||
scheduleUpdateTime(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -162,6 +167,8 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
this.disconnectedButton = findViewById(R.id.disconnected);
|
||||
this.disconnectedOverlay = findViewById(R.id.disconnected_overlay);
|
||||
this.playPause = (TextView) findViewById(R.id.button_play_pause);
|
||||
this.currentTime = (TextView) findViewById(R.id.current_time);
|
||||
this.totalTime = (TextView) findViewById(R.id.total_time);
|
||||
|
||||
findViewById(R.id.button_prev).setOnClickListener((View view) -> playback.prev());
|
||||
|
||||
@ -272,6 +279,17 @@ public class MainActivity extends WebSocketActivityBase {
|
||||
startActivity(PlayQueueActivity.getStartIntent(MainActivity.this, playback.getQueuePosition()));
|
||||
}
|
||||
|
||||
private void scheduleUpdateTime(boolean immediate) {
|
||||
handler.removeCallbacks(updateTimeRunnable);
|
||||
handler.postDelayed(updateTimeRunnable, immediate ? 0 : 1000);
|
||||
}
|
||||
|
||||
private Runnable updateTimeRunnable = () -> {
|
||||
currentTime.setText(Duration.format(playback.getCurrentTime()));
|
||||
totalTime.setText(Duration.format(playback.getDuration()));
|
||||
scheduleUpdateTime(false);
|
||||
};
|
||||
|
||||
private CheckBox.OnCheckedChangeListener muteListener =
|
||||
(CompoundButton compoundButton, boolean b) -> {
|
||||
if (b != playback.isMuted()) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.casey.musikcube.remote.playback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
@ -13,7 +14,8 @@ import io.casey.musikcube.remote.websocket.SocketMessage;
|
||||
import io.casey.musikcube.remote.websocket.WebSocketService;
|
||||
|
||||
public class RemotePlaybackService implements PlaybackService {
|
||||
private WebSocketService wss;
|
||||
private static final double NANOSECONDS_PER_SECOND = 1000000000.0;
|
||||
private static final long SYNC_TIME_INTERVAL_MS = 5000L;
|
||||
|
||||
private interface Key {
|
||||
String STATE = "state";
|
||||
@ -28,6 +30,71 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
String PLAYING_TRACK = "playing_track";
|
||||
}
|
||||
|
||||
/**
|
||||
* an annoying little class that maintains and updates state that estimates
|
||||
* the currently playing time. remember, here we're a remote control, so we
|
||||
* don't know the exact position of the play head! we update every 5 seconds
|
||||
* and estimate.
|
||||
*/
|
||||
private static class EstimatedPosition {
|
||||
double lastTime = 0.0, pauseTime = 0.0;
|
||||
long trackId = -1;
|
||||
long queryTime = 0;
|
||||
|
||||
double get(final JSONObject track) {
|
||||
if (track != null && track.optLong(Metadata.Track.ID, -1L) == trackId && trackId != -1) {
|
||||
if (pauseTime != 0) {
|
||||
return pauseTime;
|
||||
}
|
||||
else {
|
||||
return estimatedTime();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void update(final SocketMessage message) {
|
||||
queryTime = System.nanoTime();
|
||||
lastTime = message.getDoubleOption(Messages.Key.PLAYING_CURRENT_TIME, 0);
|
||||
trackId = message.getLongOption(Messages.Key.ID, -1);
|
||||
}
|
||||
|
||||
void pause() {
|
||||
pauseTime = estimatedTime();
|
||||
}
|
||||
|
||||
void resume() {
|
||||
lastTime = pauseTime;
|
||||
queryTime = System.nanoTime();
|
||||
pauseTime = 0.0;
|
||||
}
|
||||
|
||||
void update(final double time, final long id) {
|
||||
queryTime = System.nanoTime();
|
||||
lastTime = time;
|
||||
trackId = id;
|
||||
|
||||
if (pauseTime != 0) {
|
||||
pauseTime = time; /* ehh... */
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
lastTime = pauseTime = 0.0;
|
||||
queryTime = System.nanoTime();
|
||||
trackId = -1;
|
||||
}
|
||||
|
||||
double estimatedTime() {
|
||||
final long diff = System.nanoTime() - queryTime;
|
||||
final double seconds = (double) diff / NANOSECONDS_PER_SECOND;
|
||||
return lastTime + seconds;
|
||||
}
|
||||
}
|
||||
|
||||
private Handler handler = new Handler();
|
||||
private WebSocketService wss;
|
||||
private EstimatedPosition currentTime = new EstimatedPosition();
|
||||
private PlaybackState playbackState = PlaybackState.Unknown;
|
||||
private Set<EventListener> listeners = new HashSet<>();
|
||||
private RepeatMode repeatMode;
|
||||
@ -37,7 +104,6 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
private int queueCount;
|
||||
private int queuePosition;
|
||||
private double duration;
|
||||
private double currentTime;
|
||||
private JSONObject track = new JSONObject();
|
||||
|
||||
public RemotePlaybackService(final Context context) {
|
||||
@ -170,6 +236,7 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
|
||||
if (listeners.size() == 1) {
|
||||
wss.addClient(client);
|
||||
scheduleTimeSyncMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,6 +248,7 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
|
||||
if (listeners.size() == 0) {
|
||||
wss.removeClient(client);
|
||||
handler.removeCallbacks(syncTimeRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -226,7 +294,7 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
|
||||
@Override
|
||||
public double getCurrentTime() {
|
||||
return currentTime;
|
||||
return currentTime.get(track);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -256,11 +324,11 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
shuffled = muted = false;
|
||||
volume = 0.0f;
|
||||
queueCount = queuePosition = 0;
|
||||
duration = currentTime = 0.0f;
|
||||
track = new JSONObject();
|
||||
currentTime.reset();
|
||||
}
|
||||
|
||||
private boolean canHandle(SocketMessage socketMessage) {
|
||||
private boolean isPlaybackOverviewMessage(SocketMessage socketMessage) {
|
||||
if (socketMessage == null) {
|
||||
return false;
|
||||
}
|
||||
@ -269,10 +337,10 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
|
||||
return
|
||||
name.equals(Messages.Broadcast.PlaybackOverviewChanged.toString()) ||
|
||||
name.equals(Messages.Request.GetPlaybackOverview.toString());
|
||||
name.equals(Messages.Request.GetPlaybackOverview.toString());
|
||||
}
|
||||
|
||||
private boolean update(SocketMessage message) {
|
||||
private boolean updatePlaybackOverview(SocketMessage message) {
|
||||
if (message == null) {
|
||||
reset();
|
||||
return false;
|
||||
@ -287,6 +355,17 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
}
|
||||
|
||||
playbackState = PlaybackState.from(message.getStringOption(Key.STATE));
|
||||
|
||||
switch (playbackState) {
|
||||
case Paused:
|
||||
currentTime.pause();
|
||||
break;
|
||||
case Playing:
|
||||
currentTime.resume();
|
||||
scheduleTimeSyncMessage();
|
||||
break;
|
||||
}
|
||||
|
||||
repeatMode = RepeatMode.from(message.getStringOption(Key.REPEAT_MODE));
|
||||
shuffled = message.getBooleanOption(Key.SHUFFLED);
|
||||
muted = message.getBooleanOption(Key.MUTED);
|
||||
@ -294,9 +373,14 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
queueCount = message.getIntOption(Key.PLAY_QUEUE_COUNT);
|
||||
queuePosition = message.getIntOption(Key.PLAY_QUEUE_POSITION);
|
||||
duration = message.getDoubleOption(Key.PLAYING_DURATION);
|
||||
currentTime = message.getDoubleOption(Key.PLAYING_CURRENT_TIME);
|
||||
track = message.getJsonObjectOption(Key.PLAYING_TRACK, new JSONObject());
|
||||
|
||||
if (track != null) {
|
||||
currentTime.update(
|
||||
message.getDoubleOption(Key.PLAYING_CURRENT_TIME, -1),
|
||||
track.optLong(Metadata.Track.ID, -1));
|
||||
}
|
||||
|
||||
notifyStateUpdated();
|
||||
|
||||
return true;
|
||||
@ -308,6 +392,21 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTimeSyncMessage() {
|
||||
handler.removeCallbacks(syncTimeRunnable);
|
||||
|
||||
if (getPlaybackState() == PlaybackState.Playing) {
|
||||
handler.postDelayed(syncTimeRunnable, SYNC_TIME_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable syncTimeRunnable = () -> {
|
||||
if (this.wss.hasClient(this.client)) {
|
||||
this.wss.send(SocketMessage.Builder
|
||||
.request(Messages.Request.GetCurrentTime).build());
|
||||
}
|
||||
};
|
||||
|
||||
private final TrackListSlidingWindow.QueryFactory queryFactory
|
||||
= new TrackListSlidingWindow.QueryFactory() {
|
||||
@Override
|
||||
@ -343,8 +442,12 @@ public class RemotePlaybackService implements PlaybackService {
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(SocketMessage message) {
|
||||
if (canHandle(message)) {
|
||||
update(message);
|
||||
if (isPlaybackOverviewMessage(message)) {
|
||||
updatePlaybackOverview(message);
|
||||
}
|
||||
else if (Messages.Request.GetCurrentTime.is(message.getName())) {
|
||||
currentTime.update(message);
|
||||
scheduleTimeSyncMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
package io.casey.musikcube.remote.util;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Duration {
|
||||
private Duration() {
|
||||
|
||||
}
|
||||
|
||||
public static String format(double seconds) {
|
||||
final int mins = ((int) seconds / 60);
|
||||
final int secs = (int) seconds - (mins * 60);
|
||||
return String.format(Locale.getDefault(), "%d:%02d", mins, secs);
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ public class Messages {
|
||||
public enum Request {
|
||||
Authenticate("authenticate"),
|
||||
Ping("ping"),
|
||||
GetCurrentTime("get_current_time"),
|
||||
GetPlaybackOverview("get_playback_overview"),
|
||||
PauseOrResume("pause_or_resume"),
|
||||
Stop("stop"),
|
||||
@ -45,6 +46,10 @@ public class Messages {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean is(final String name) {
|
||||
return rawValue.equals(name);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Broadcast {
|
||||
@ -92,6 +97,7 @@ public class Messages {
|
||||
String VALUE = "value";
|
||||
String FILTER = "filter";
|
||||
String RELATIVE = "relative";
|
||||
String PLAYING_CURRENT_TIME = "playing_current_time";
|
||||
}
|
||||
|
||||
public interface Value {
|
||||
|
@ -129,6 +129,21 @@ public class SocketMessage {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public long getLongOption(final String key) {
|
||||
return getLongOption(key, 0);
|
||||
}
|
||||
|
||||
public long getLongOption(final String key, final long defaultValue) {
|
||||
if (options.has(key)) {
|
||||
try {
|
||||
return options.getLong(key);
|
||||
}
|
||||
catch (JSONException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public double getDoubleOption(final String key) {
|
||||
return getDoubleOption(key, 0.0);
|
||||
|
@ -213,6 +213,11 @@ public class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasClient(Client client) {
|
||||
Preconditions.throwIfNotOnMainThread();
|
||||
return this.clients.contains(client);
|
||||
}
|
||||
|
||||
public void reconnect() {
|
||||
Preconditions.throwIfNotOnMainThread();
|
||||
autoReconnect = true;
|
||||
|
@ -6,68 +6,53 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:clipToPadding="false"
|
||||
tools:context="io.casey.musikcube.remote.MainActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@+id/play_controls">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/connected_not_playing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="@color/theme_green"
|
||||
android:text="@string/button_connected"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:text="@string/transport_not_playing"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disconnected"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:padding="8dp"
|
||||
android:clickable="true"
|
||||
android:background="@drawable/not_playing_button"
|
||||
android:text="@string/status_disconnected"/>
|
||||
|
||||
<io.casey.musikcube.remote.ui.view.MainMetadataView
|
||||
android:id="@+id/main_metadata_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:background="@color/theme_background"
|
||||
android:id="@+id/play_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true">
|
||||
android:layout_alignParentBottom="true"
|
||||
android:elevation="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:background="@drawable/playback_button"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:gravity="left"
|
||||
android:text="0:00"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right"
|
||||
android:gravity="right"
|
||||
android:text="0:00"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp" />
|
||||
@ -274,5 +259,53 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@+id/play_controls">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/connected_not_playing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="@color/theme_green"
|
||||
android:text="@string/button_connected"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:text="@string/transport_not_playing"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disconnected"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:padding="8dp"
|
||||
android:clickable="true"
|
||||
android:background="@drawable/not_playing_button"
|
||||
android:text="@string/status_disconnected"/>
|
||||
|
||||
<io.casey.musikcube.remote.ui.view.MainMetadataView
|
||||
android:id="@+id/main_metadata_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user