Implemented showing volume level on NowPlayingFragment (#312)

Replaced up/down volume buttons with a seekbar that displays the current
volume level and can be used to change the volume level
This commit is contained in:
Martijn Brekhof 2016-11-30 13:20:21 +01:00 committed by Synced Synapse
parent d2b5449e98
commit c5848ce648
7 changed files with 368 additions and 108 deletions

View File

@ -21,8 +21,10 @@ import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.HostConnection; import org.xbmc.kore.jsonrpc.HostConnection;
import org.xbmc.kore.jsonrpc.method.JSONRPC; import org.xbmc.kore.jsonrpc.method.JSONRPC;
import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.notification.Application;
import org.xbmc.kore.jsonrpc.notification.Input; import org.xbmc.kore.jsonrpc.notification.Input;
import org.xbmc.kore.jsonrpc.notification.System; import org.xbmc.kore.jsonrpc.notification.System;
import org.xbmc.kore.jsonrpc.type.ApplicationType;
import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
@ -43,10 +45,20 @@ import java.util.List;
*/ */
public class HostConnectionObserver public class HostConnectionObserver
implements HostConnection.PlayerNotificationsObserver, implements HostConnection.PlayerNotificationsObserver,
HostConnection.SystemNotificationsObserver, HostConnection.SystemNotificationsObserver,
HostConnection.InputNotificationsObserver { HostConnection.InputNotificationsObserver,
HostConnection.ApplicationNotificationsObserver {
public static final String TAG = LogUtils.makeLogTag(HostConnectionObserver.class); public static final String TAG = LogUtils.makeLogTag(HostConnectionObserver.class);
public interface ApplicationEventsObserver {
/**
* Notifies the observer that volume has changed
* @param volume
* @param muted
*/
public void applicationOnVolumeChanged(int volume, boolean muted);
}
/** /**
* Interface that an observer has to implement to receive player events * Interface that an observer has to implement to receive player events
*/ */
@ -123,6 +135,7 @@ public class HostConnectionObserver
* The list of observers * The list of observers
*/ */
private List<PlayerEventsObserver> playerEventsObservers = new ArrayList<PlayerEventsObserver>(); private List<PlayerEventsObserver> playerEventsObservers = new ArrayList<PlayerEventsObserver>();
private List<ApplicationEventsObserver> applicationEventsObservers = new ArrayList<>();
// /** // /**
// * Handlers for which observer, on which to notify them // * Handlers for which observer, on which to notify them
@ -130,7 +143,7 @@ public class HostConnectionObserver
// private Map<PlayerEventsObserver, Handler> observerHandlerMap = new HashMap<PlayerEventsObserver, Handler>(); // private Map<PlayerEventsObserver, Handler> observerHandlerMap = new HashMap<PlayerEventsObserver, Handler>();
private Handler checkerHandler = new Handler(); private Handler checkerHandler = new Handler();
private Runnable httpCheckerRunnable = new Runnable() { private Runnable httpPlayerCheckerRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
final int HTTP_NOTIFICATION_CHECK_INTERVAL = 3000; final int HTTP_NOTIFICATION_CHECK_INTERVAL = 3000;
@ -145,6 +158,20 @@ public class HostConnectionObserver
} }
}; };
private Runnable httpApplicationCheckerRunnable = new Runnable() {
@Override
public void run() {
final int HTTP_NOTIFICATION_CHECK_INTERVAL = 3000;
// If no one is listening to this, just exit
if (applicationEventsObservers.isEmpty()) return;
getApplicationProperties();
// Keep checking
checkerHandler.postDelayed(this, HTTP_NOTIFICATION_CHECK_INTERVAL);
}
};
private Runnable tcpCheckerRunnable = new Runnable() { private Runnable tcpCheckerRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
@ -158,8 +185,8 @@ public class HostConnectionObserver
@Override @Override
public void onSuccess(String result) { public void onSuccess(String result) {
// Ok, we've got a ping, if we were in a error or uninitialized state, update // Ok, we've got a ping, if we were in a error or uninitialized state, update
if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || if ((hostState.lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) ||
(lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { (hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) {
checkWhatsPlaying(); checkWhatsPlaying();
} }
checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL);
@ -181,14 +208,31 @@ public class HostConnectionObserver
} }
}; };
private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; public class HostState {
private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null; private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT;
private PlayerType.PropertyValue lastGetPropertiesResult = null; private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null;
private ListType.ItemsAll lastGetItemResult = null; private PlayerType.PropertyValue lastGetPropertiesResult = null;
private int lastErrorCode; private ListType.ItemsAll lastGetItemResult = null;
private String lastErrorDescription; private boolean volumeMuted = false;
private int volumeLevel = -1; // -1 indicates no volumeLevel known
private int lastErrorCode;
private String lastErrorDescription;
public int getVolumeLevel() {
return volumeLevel;
}
public boolean isVolumeMuted() {
return volumeMuted;
}
}
public HostState hostState;
private HostConnectionObserver() {}
public HostConnectionObserver(HostConnection connection) { public HostConnectionObserver(HostConnection connection) {
this.hostState = new HostState();
this.connection = connection; this.connection = connection;
} }
@ -216,7 +260,7 @@ public class HostConnectionObserver
// Start the ping checker // Start the ping checker
checkerHandler.post(tcpCheckerRunnable); checkerHandler.post(tcpCheckerRunnable);
} else { } else {
checkerHandler.post(httpCheckerRunnable); checkerHandler.post(httpPlayerCheckerRunnable);
} }
} }
} }
@ -229,7 +273,8 @@ public class HostConnectionObserver
playerEventsObservers.remove(observer); playerEventsObservers.remove(observer);
// observerHandlerMap.remove(observer); // observerHandlerMap.remove(observer);
LogUtils.LOGD(TAG, "Unregistering observer. Still got " + playerEventsObservers.size() + LogUtils.LOGD(TAG, "Unregistering player observer " + observer.getClass().getSimpleName() +
". Still got " + playerEventsObservers.size() +
" observers."); " observers.");
if (playerEventsObservers.isEmpty()) { if (playerEventsObservers.isEmpty()) {
@ -241,9 +286,61 @@ public class HostConnectionObserver
connection.unregisterInputNotificationsObserver(this); connection.unregisterInputNotificationsObserver(this);
checkerHandler.removeCallbacks(tcpCheckerRunnable); checkerHandler.removeCallbacks(tcpCheckerRunnable);
} else { } else {
checkerHandler.removeCallbacks(httpCheckerRunnable); checkerHandler.removeCallbacks(httpPlayerCheckerRunnable);
}
hostState.lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT;
}
}
/**
* Registers a new observer that will be notified about application events
* @param observer Observer
* @param replyImmediately
*/
public void registerApplicationObserver(ApplicationEventsObserver observer, boolean replyImmediately) {
if (this.connection == null)
return;
applicationEventsObservers.add(observer);
if (replyImmediately) {
if( hostState.volumeLevel == -1 ) {
getApplicationProperties();
} else {
observer.applicationOnVolumeChanged(hostState.volumeLevel, hostState.volumeMuted);
}
}
if (applicationEventsObservers.size() == 1) {
// If this is the first observer, start checking through HTTP or register us
// as a connection observer, which we will pass to the "real" observer
if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) {
connection.registerApplicationNotificationsObserver(this, checkerHandler);
} else {
checkerHandler.post(httpApplicationCheckerRunnable);
}
}
}
/**
* Unregisters a previously registered observer
* @param observer Observer to unregister
*/
public void unregisterApplicationObserver(PlayerEventsObserver observer) {
applicationEventsObservers.remove(observer);
LogUtils.LOGD(TAG, "Unregistering application observer " + observer.getClass().getSimpleName() +
". Still got " + applicationEventsObservers.size() +
" observers.");
if (applicationEventsObservers.isEmpty()) {
// No more observers, so unregister us from the host connection, or stop
// the http checker thread
if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) {
connection.unregisterApplicationotificationsObserver(this);
} else {
checkerHandler.removeCallbacks(httpApplicationCheckerRunnable);
} }
lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT;
} }
} }
@ -262,9 +359,9 @@ public class HostConnectionObserver
connection.unregisterInputNotificationsObserver(this); connection.unregisterInputNotificationsObserver(this);
checkerHandler.removeCallbacks(tcpCheckerRunnable); checkerHandler.removeCallbacks(tcpCheckerRunnable);
} else { } else {
checkerHandler.removeCallbacks(httpCheckerRunnable); checkerHandler.removeCallbacks(httpPlayerCheckerRunnable);
} }
lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; hostState.lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT;
} }
/** /**
@ -332,6 +429,39 @@ public class HostConnectionObserver
} }
} }
@Override
public void onVolumeChanged(Application.OnVolumeChanged notification) {
hostState.volumeMuted = notification.muted;
hostState.volumeLevel = notification.volume;
for (ApplicationEventsObserver observer : applicationEventsObservers) {
observer.applicationOnVolumeChanged(notification.volume, notification.muted);
}
}
private void getApplicationProperties() {
org.xbmc.kore.jsonrpc.method.Application.GetProperties getProperties =
new org.xbmc.kore.jsonrpc.method.Application.GetProperties(org.xbmc.kore.jsonrpc.method.Application.GetProperties.VOLUME,
org.xbmc.kore.jsonrpc.method.Application.GetProperties.MUTED);
getProperties.execute(connection, new ApiCallback<ApplicationType.PropertyValue>() {
@Override
public void onSuccess(ApplicationType.PropertyValue result) {
hostState.volumeMuted = result.muted;
hostState.volumeLevel = result.volume;
for (ApplicationEventsObserver observer : applicationEventsObservers) {
observer.applicationOnVolumeChanged(result.volume, result.muted);
}
}
@Override
public void onError(int errorCode, String description) {
LogUtils.LOGD(TAG, "Could not get application properties");
notifyConnectionError(errorCode, description, playerEventsObservers);
}
}, checkerHandler);
}
/** /**
* Checks whats playing and notifies observers * Checks whats playing and notifies observers
*/ */
@ -344,7 +474,7 @@ public class HostConnectionObserver
/** /**
* Calls Player.GetActivePlayers * Calls Player.GetActivePlayers
* On success chains execution to chainCallGetProperties * On success chains execution to chainCallGetPlayerProperties
*/ */
private void chainCallGetActivePlayers() { private void chainCallGetActivePlayers() {
Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers(); Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers();
@ -356,7 +486,7 @@ public class HostConnectionObserver
notifyNothingIsPlaying(playerEventsObservers); notifyNothingIsPlaying(playerEventsObservers);
return; return;
} }
chainCallGetProperties(result.get(0)); chainCallGetPlayerProperties(result.get(0));
} }
@Override @Override
@ -371,7 +501,7 @@ public class HostConnectionObserver
* Calls Player.GetProperties * Calls Player.GetProperties
* On success chains execution to chainCallGetItem * On success chains execution to chainCallGetItem
*/ */
private void chainCallGetProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) { private void chainCallGetPlayerProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) {
String propertiesToGet[] = new String[] { String propertiesToGet[] = new String[] {
// Check is something more is needed // Check is something more is needed
PlayerType.PropertyName.SPEED, PlayerType.PropertyName.SPEED,
@ -465,7 +595,7 @@ public class HostConnectionObserver
}, checkerHandler); }, checkerHandler);
} }
// Whether to foorce a reply or if the results are equal to the last one, don't reply // Whether to force a reply or if the results are equal to the last one, don't reply
private boolean forceReply = false; private boolean forceReply = false;
/** /**
@ -478,11 +608,11 @@ public class HostConnectionObserver
private void notifyConnectionError(final int errorCode, final String description, List<PlayerEventsObserver> observers) { private void notifyConnectionError(final int errorCode, final String description, List<PlayerEventsObserver> observers) {
// Reply if different from last result // Reply if different from last result
if (forceReply || if (forceReply ||
(lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) || (hostState.lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) ||
(lastErrorCode != errorCode)) { (hostState.lastErrorCode != errorCode)) {
lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; hostState.lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR;
lastErrorCode = errorCode; hostState.lastErrorCode = errorCode;
lastErrorDescription = description; hostState.lastErrorDescription = description;
forceReply = false; forceReply = false;
// Copy list to prevent ConcurrentModificationExceptions // Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers); List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
@ -519,8 +649,8 @@ public class HostConnectionObserver
private void notifyNothingIsPlaying(List<PlayerEventsObserver> observers) { private void notifyNothingIsPlaying(List<PlayerEventsObserver> observers) {
// Reply if forced or different from last result // Reply if forced or different from last result
if (forceReply || if (forceReply ||
(lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) { (hostState.lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) {
lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED; hostState.lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED;
forceReply = false; forceReply = false;
// Copy list to prevent ConcurrentModificationExceptions // Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers); List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
@ -554,16 +684,16 @@ public class HostConnectionObserver
int currentCallResult = (getPropertiesResult.speed == 0) ? int currentCallResult = (getPropertiesResult.speed == 0) ?
PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING;
if (forceReply || if (forceReply ||
(lastCallResult != currentCallResult) || (hostState.lastCallResult != currentCallResult) ||
(lastGetPropertiesResult.speed != getPropertiesResult.speed) || (hostState.lastGetPropertiesResult.speed != getPropertiesResult.speed) ||
(lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || (hostState.lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) ||
(!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) || (!hostState.lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) ||
(lastGetItemResult.id != getItemResult.id) || (hostState.lastGetItemResult.id != getItemResult.id) ||
(!lastGetItemResult.label.equals(getItemResult.label))) { (!hostState.lastGetItemResult.label.equals(getItemResult.label))) {
lastCallResult = currentCallResult; hostState.lastCallResult = currentCallResult;
lastGetActivePlayerResult = getActivePlayersResult; hostState.lastGetActivePlayerResult = getActivePlayersResult;
lastGetPropertiesResult = getPropertiesResult; hostState.lastGetPropertiesResult = getPropertiesResult;
lastGetItemResult = getItemResult; hostState.lastGetItemResult = getItemResult;
forceReply = false; forceReply = false;
// Copy list to prevent ConcurrentModificationExceptions // Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers); List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
@ -619,16 +749,16 @@ public class HostConnectionObserver
* @param observer Obserser to call with last result * @param observer Obserser to call with last result
*/ */
public void replyWithLastResult(PlayerEventsObserver observer) { public void replyWithLastResult(PlayerEventsObserver observer) {
switch (lastCallResult) { switch (hostState.lastCallResult) {
case PlayerEventsObserver.PLAYER_CONNECTION_ERROR: case PlayerEventsObserver.PLAYER_CONNECTION_ERROR:
notifyConnectionError(lastErrorCode, lastErrorDescription, observer); notifyConnectionError(hostState.lastErrorCode, hostState.lastErrorDescription, observer);
break; break;
case PlayerEventsObserver.PLAYER_IS_STOPPED: case PlayerEventsObserver.PLAYER_IS_STOPPED:
notifyNothingIsPlaying(observer); notifyNothingIsPlaying(observer);
break; break;
case PlayerEventsObserver.PLAYER_IS_PAUSED: case PlayerEventsObserver.PLAYER_IS_PAUSED:
case PlayerEventsObserver.PLAYER_IS_PLAYING: case PlayerEventsObserver.PLAYER_IS_PLAYING:
notifySomethingIsPlaying(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult, observer); notifySomethingIsPlaying(hostState.lastGetActivePlayerResult, hostState.lastGetPropertiesResult, hostState.lastGetItemResult, observer);
break; break;
case PlayerEventsObserver.PLAYER_NO_RESULT: case PlayerEventsObserver.PLAYER_NO_RESULT:
observer.playerNoResultsYet(); observer.playerNoResultsYet();
@ -643,4 +773,8 @@ public class HostConnectionObserver
forceReply = true; forceReply = true;
chainCallGetActivePlayers(); chainCallGetActivePlayers();
} }
public HostState getHostState() {
return hostState;
}
} }

View File

@ -32,6 +32,7 @@ import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response; import com.squareup.okhttp.Response;
import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.jsonrpc.notification.Application;
import org.xbmc.kore.jsonrpc.notification.Input; import org.xbmc.kore.jsonrpc.notification.Input;
import org.xbmc.kore.jsonrpc.notification.Player; import org.xbmc.kore.jsonrpc.notification.Player;
import org.xbmc.kore.jsonrpc.notification.System; import org.xbmc.kore.jsonrpc.notification.System;
@ -92,6 +93,10 @@ public class HostConnection {
public void onInputRequested(Input.OnInputRequested notification); public void onInputRequested(Input.OnInputRequested notification);
} }
public interface ApplicationNotificationsObserver {
public void onVolumeChanged(Application.OnVolumeChanged notification);
}
/** /**
* Host to connect too * Host to connect too
*/ */
@ -139,6 +144,12 @@ public class HostConnection {
private final HashMap<InputNotificationsObserver, Handler> inputNotificationsObservers = private final HashMap<InputNotificationsObserver, Handler> inputNotificationsObservers =
new HashMap<InputNotificationsObserver, Handler>(); new HashMap<InputNotificationsObserver, Handler>();
/**
* The observers that will be notified of application notifications
*/
private final HashMap<ApplicationNotificationsObserver, Handler> applicationNotificationsObservers =
new HashMap<>();
private ExecutorService executorService; private ExecutorService executorService;
private final int connectTimeout; private final int connectTimeout;
@ -250,6 +261,23 @@ public class HostConnection {
inputNotificationsObservers.remove(observer); inputNotificationsObservers.remove(observer);
} }
/**
* Registers an observer for input notifications
* @param observer The {@link InputNotificationsObserver}
*/
public void registerApplicationNotificationsObserver(ApplicationNotificationsObserver observer,
Handler handler) {
applicationNotificationsObservers.put(observer, handler);
}
/**
* Unregisters and observer from the input notifications
* @param observer The {@link InputNotificationsObserver}
*/
public void unregisterApplicationotificationsObserver(ApplicationNotificationsObserver observer) {
applicationNotificationsObservers.remove(observer);
}
/** /**
* Calls the a method on the server * Calls the a method on the server
* This call is always asynchronous. The results will be posted, through the * This call is always asynchronous. The results will be posted, through the
@ -814,6 +842,19 @@ public class HostConnection {
} }
}); });
} }
} else if (notificationName.equals(Application.OnVolumeChanged.NOTIFICATION_NAME)) {
final Application.OnVolumeChanged apiNotification =
new Application.OnVolumeChanged(params);
for (final ApplicationNotificationsObserver observer :
applicationNotificationsObservers.keySet()) {
Handler handler = inputNotificationsObservers.get(observer);
handler.post(new Runnable() {
@Override
public void run() {
observer.onVolumeChanged(apiNotification);
}
});
}
} }
LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue()); LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue());

View File

@ -57,7 +57,7 @@ public class Application {
public final static String METHOD_NAME = "Application.SetVolume"; public final static String METHOD_NAME = "Application.SetVolume";
/** /**
* Set the current volume * Increment or decrement the volume
* @param volume String enum in {@link org.xbmc.kore.jsonrpc.type.GlobalType.IncrementDecrement} * @param volume String enum in {@link org.xbmc.kore.jsonrpc.type.GlobalType.IncrementDecrement}
*/ */
public SetVolume(String volume) { public SetVolume(String volume) {
@ -65,6 +65,15 @@ public class Application {
addParameterToRequest("volume", volume); addParameterToRequest("volume", volume);
} }
/**
* Set the volume
* @param volume volume between 0 and 100
*/
public SetVolume(int volume) {
super();
addParameterToRequest("volume", volume);
}
@Override @Override
public String getMethodName() { return METHOD_NAME; } public String getMethodName() { return METHOD_NAME; }

View File

@ -0,0 +1,50 @@
/*
* Copyright 2015 Synced Synapse. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xbmc.kore.jsonrpc.notification;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.jsonrpc.ApiNotification;
import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.utils.JsonUtils;
/**
* All Player.* notifications
*/
public class Application {
/**
* Player.OnSpeedChanged notification
* Speed of the playback of a media item has been changed. If there is no ID available extra information will be provided.
* be provided.
*/
public static class OnVolumeChanged extends ApiNotification {
public static final String NOTIFICATION_NAME = "Application.OnVolumeChanged";
public final int volume;
public final boolean muted;
public OnVolumeChanged(ObjectNode node) {
super(node);
ObjectNode dataNode = (ObjectNode)node.get("data");
volume = JsonUtils.intFromJsonNode(dataNode, "volume");
muted = JsonUtils.booleanFromJsonNode(dataNode, "muted");
}
public String getNotificationName() { return NOTIFICATION_NAME; }
}
}

View File

@ -50,7 +50,6 @@ import org.xbmc.kore.jsonrpc.method.Application;
import org.xbmc.kore.jsonrpc.method.GUI; import org.xbmc.kore.jsonrpc.method.GUI;
import org.xbmc.kore.jsonrpc.method.Input; import org.xbmc.kore.jsonrpc.method.Input;
import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.type.ApplicationType;
import org.xbmc.kore.jsonrpc.type.GlobalType; import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.jsonrpc.type.PlayerType;
@ -72,7 +71,8 @@ import butterknife.OnClick;
*/ */
public class NowPlayingFragment extends Fragment public class NowPlayingFragment extends Fragment
implements HostConnectionObserver.PlayerEventsObserver, implements HostConnectionObserver.PlayerEventsObserver,
GenericSelectDialog.GenericSelectDialogListener { HostConnectionObserver.ApplicationEventsObserver,
GenericSelectDialog.GenericSelectDialogListener {
private static final String TAG = LogUtils.makeLogTag(NowPlayingFragment.class); private static final String TAG = LogUtils.makeLogTag(NowPlayingFragment.class);
/** /**
@ -132,8 +132,6 @@ public class NowPlayingFragment extends Fragment
@InjectView(R.id.rewind) ImageButton rewindButton; @InjectView(R.id.rewind) ImageButton rewindButton;
@InjectView(R.id.fast_forward) ImageButton fastForwardButton; @InjectView(R.id.fast_forward) ImageButton fastForwardButton;
@InjectView(R.id.volume_down) ImageButton volumeDownButton;
@InjectView(R.id.volume_up) ImageButton volumeUpButton;
@InjectView(R.id.volume_mute) ImageButton volumeMuteButton; @InjectView(R.id.volume_mute) ImageButton volumeMuteButton;
@InjectView(R.id.shuffle) ImageButton shuffleButton; @InjectView(R.id.shuffle) ImageButton shuffleButton;
@InjectView(R.id.repeat) ImageButton repeatButton; @InjectView(R.id.repeat) ImageButton repeatButton;
@ -154,6 +152,9 @@ public class NowPlayingFragment extends Fragment
@InjectView(R.id.media_progress) TextView mediaProgress; @InjectView(R.id.media_progress) TextView mediaProgress;
@InjectView(R.id.seek_bar) SeekBar mediaSeekbar; @InjectView(R.id.seek_bar) SeekBar mediaSeekbar;
@InjectView(R.id.volume_bar) SeekBar volumeSeekBar;
@InjectView(R.id.volume_text) TextView volumeTextView;
@InjectView(R.id.media_details) RelativeLayout mediaDetailsPanel; @InjectView(R.id.media_details) RelativeLayout mediaDetailsPanel;
@InjectView(R.id.rating) TextView mediaRating; @InjectView(R.id.rating) TextView mediaRating;
@InjectView(R.id.max_rating) TextView mediaMaxRating; @InjectView(R.id.max_rating) TextView mediaMaxRating;
@ -187,11 +188,6 @@ public class NowPlayingFragment extends Fragment
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_now_playing, container, false); ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_now_playing, container, false);
ButterKnife.inject(this, root); ButterKnife.inject(this, root);
setupVolumeRepeatButton(volumeDownButton,
new Application.SetVolume(GlobalType.IncrementDecrement.DECREMENT));
setupVolumeRepeatButton(volumeUpButton,
new Application.SetVolume(GlobalType.IncrementDecrement.INCREMENT));
// Setup dim the fanart when scroll changes // Setup dim the fanart when scroll changes
// Full dim on 4 * iconSize dp // Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources(); Resources resources = getActivity().getResources();
@ -222,16 +218,7 @@ public class NowPlayingFragment extends Fragment
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
hostConnectionObserver.registerPlayerObserver(this, true); hostConnectionObserver.registerPlayerObserver(this, true);
Application.GetProperties action = new Application.GetProperties(Application.GetProperties.MUTED); hostConnectionObserver.registerApplicationObserver(this, true);
action.execute(hostManager.getConnection(), new ApiCallback<ApplicationType.PropertyValue>() {
@Override
public void onSuccess(ApplicationType.PropertyValue result) {
setVolumeMuteButton(result.muted);
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
} }
@Override @Override
@ -239,6 +226,7 @@ public class NowPlayingFragment extends Fragment
super.onPause(); super.onPause();
stopNowPlayingInfo(); stopNowPlayingInfo();
hostConnectionObserver.unregisterPlayerObserver(this); hostConnectionObserver.unregisterPlayerObserver(this);
hostConnectionObserver.unregisterApplicationObserver(this);
} }
/** /**
@ -262,16 +250,6 @@ public class NowPlayingFragment extends Fragment
public void onError(int errorCode, String description) { } public void onError(int errorCode, String description) { }
}; };
private void setupVolumeRepeatButton(View button, final ApiMethod<Integer> action) {
button.setOnTouchListener(new RepeatListener(UIUtils.initialButtonRepeatInterval, UIUtils.buttonRepeatInterval,
new View.OnClickListener() {
@Override
public void onClick(View v) {
action.execute(hostManager.getConnection(), defaultIntActionCallback, callbackHandler);
}
}));
}
/** /**
* Callbacks for bottom button bar * Callbacks for bottom button bar
*/ */
@ -317,11 +295,17 @@ public class NowPlayingFragment extends Fragment
*/ */
@OnClick(R.id.volume_mute) @OnClick(R.id.volume_mute)
public void onVolumeMuteClicked(View v) { public void onVolumeMuteClicked(View v) {
// We boldly set the mute button to the desired state before actually setting
// the mute state on the host. We do this to make it clear to the user that the button
// was pressed.
HostConnectionObserver.HostState hostState = hostConnectionObserver.getHostState();
setVolumeState(!hostState.isVolumeMuted(), hostState.getVolumeLevel());
Application.SetMute action = new Application.SetMute(); Application.SetMute action = new Application.SetMute();
action.execute(hostManager.getConnection(), new ApiCallback<Boolean>() { action.execute(hostManager.getConnection(), new ApiCallback<Boolean>() {
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
setVolumeMuteButton(result); //We depend on the listener to correct the mute button state
} }
@Override @Override
@ -386,7 +370,7 @@ public class NowPlayingFragment extends Fragment
case R.id.audiostreams: case R.id.audiostreams:
// Setup audiostream select dialog // Setup audiostream select dialog
String[] audiostreams = new String[(availableAudioStreams != null) ? String[] audiostreams = new String[(availableAudioStreams != null) ?
availableAudioStreams.size() + ADDED_AUDIO_OPTIONS : ADDED_AUDIO_OPTIONS]; availableAudioStreams.size() + ADDED_AUDIO_OPTIONS : ADDED_AUDIO_OPTIONS];
audiostreams[0] = getString(R.string.audio_sync); audiostreams[0] = getString(R.string.audio_sync);
@ -394,21 +378,21 @@ public class NowPlayingFragment extends Fragment
for (int i = 0; i < availableAudioStreams.size(); i++) { for (int i = 0; i < availableAudioStreams.size(); i++) {
PlayerType.AudioStream current = availableAudioStreams.get(i); PlayerType.AudioStream current = availableAudioStreams.get(i);
audiostreams[i + ADDED_AUDIO_OPTIONS] = TextUtils.isEmpty(current.language) ? audiostreams[i + ADDED_AUDIO_OPTIONS] = TextUtils.isEmpty(current.language) ?
current.name : current.language + " | " + current.name; current.name : current.language + " | " + current.name;
if (current.index == currentAudiostreamIndex) { if (current.index == currentAudiostreamIndex) {
selectedItem = i + ADDED_AUDIO_OPTIONS; selectedItem = i + ADDED_AUDIO_OPTIONS;
} }
} }
GenericSelectDialog dialog = GenericSelectDialog.newInstance(NowPlayingFragment.this, GenericSelectDialog dialog = GenericSelectDialog.newInstance(NowPlayingFragment.this,
SELECT_AUDIOSTREAM, getString(R.string.audiostreams), audiostreams, selectedItem); SELECT_AUDIOSTREAM, getString(R.string.audiostreams), audiostreams, selectedItem);
dialog.show(NowPlayingFragment.this.getFragmentManager(), null); dialog.show(NowPlayingFragment.this.getFragmentManager(), null);
} }
return true; return true;
case R.id.subtitles: case R.id.subtitles:
// Setup subtitles select dialog // Setup subtitles select dialog
String[] subtitles = new String[(availableSubtitles != null) ? String[] subtitles = new String[(availableSubtitles != null) ?
availableSubtitles.size() + ADDED_SUBTITLE_OPTIONS : ADDED_SUBTITLE_OPTIONS]; availableSubtitles.size() + ADDED_SUBTITLE_OPTIONS : ADDED_SUBTITLE_OPTIONS];
subtitles[0] = getString(R.string.download_subtitle); subtitles[0] = getString(R.string.download_subtitle);
subtitles[1] = getString(R.string.subtitle_sync); subtitles[1] = getString(R.string.subtitle_sync);
@ -418,7 +402,7 @@ public class NowPlayingFragment extends Fragment
for (int i = 0; i < availableSubtitles.size(); i++) { for (int i = 0; i < availableSubtitles.size(); i++) {
PlayerType.Subtitle current = availableSubtitles.get(i); PlayerType.Subtitle current = availableSubtitles.get(i);
subtitles[i + ADDED_SUBTITLE_OPTIONS] = TextUtils.isEmpty(current.language) ? subtitles[i + ADDED_SUBTITLE_OPTIONS] = TextUtils.isEmpty(current.language) ?
current.name : current.language + " | " + current.name; current.name : current.language + " | " + current.name;
if (current.index == currentSubtitleIndex) { if (current.index == currentSubtitleIndex) {
selectedItem = i + ADDED_SUBTITLE_OPTIONS; selectedItem = i + ADDED_SUBTITLE_OPTIONS;
} }
@ -426,7 +410,7 @@ public class NowPlayingFragment extends Fragment
} }
GenericSelectDialog dialog = GenericSelectDialog.newInstance(NowPlayingFragment.this, GenericSelectDialog dialog = GenericSelectDialog.newInstance(NowPlayingFragment.this,
SELECT_SUBTITLES, getString(R.string.subtitles), subtitles, selectedItem); SELECT_SUBTITLES, getString(R.string.subtitles), subtitles, selectedItem);
dialog.show(NowPlayingFragment.this.getFragmentManager(), null); dialog.show(NowPlayingFragment.this.getFragmentManager(), null);
return true; return true;
} }
@ -519,8 +503,8 @@ public class NowPlayingFragment extends Fragment
public void onError(int errorCode, String description) { public void onError(int errorCode, String description) {
if (!isAdded()) return; if (!isAdded()) return;
Toast.makeText(getActivity(), Toast.makeText(getActivity(),
String.format(getString(R.string.error_executing_subtitles), description), String.format(getString(R.string.error_executing_subtitles), description),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
}, callbackHandler); }, callbackHandler);
} }
@ -614,6 +598,11 @@ public class NowPlayingFragment extends Fragment
playerNoResultsYet(); playerNoResultsYet();
} }
@Override
public void applicationOnVolumeChanged(int volume, boolean muted) {
setVolumeState(muted, volume);
}
// Ignore this // Ignore this
public void inputOnInputRequested(String title, String type, String value) {} public void inputOnInputRequested(String title, String type, String value) {}
public void observerOnStopObserving() {} public void observerOnStopObserving() {}
@ -679,7 +668,7 @@ public class NowPlayingFragment extends Fragment
title = getItemResult.title; title = getItemResult.title;
underTitle = Utils.listStringConcat(getItemResult.artist, ", ") underTitle = Utils.listStringConcat(getItemResult.artist, ", ")
+ " | " + getItemResult.album; + " | " + getItemResult.album;
art = getItemResult.fanart; art = getItemResult.fanart;
poster = getItemResult.thumbnail; poster = getItemResult.thumbnail;
@ -783,6 +772,8 @@ public class NowPlayingFragment extends Fragment
} }
styledAttributes.recycle(); styledAttributes.recycle();
volumeSeekBar.setOnSeekBarChangeListener(volumeSeekbarChangeListener);
Resources resources = getActivity().getResources(); Resources resources = getActivity().getResources();
DisplayMetrics displayMetrics = new DisplayMetrics(); DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
@ -796,7 +787,7 @@ public class NowPlayingFragment extends Fragment
// If not video, change aspect ration of poster to a square // If not video, change aspect ration of poster to a square
boolean isVideo = (getItemResult.type.equals(ListType.ItemsAll.TYPE_MOVIE)) || boolean isVideo = (getItemResult.type.equals(ListType.ItemsAll.TYPE_MOVIE)) ||
(getItemResult.type.equals(ListType.ItemsAll.TYPE_EPISODE)); (getItemResult.type.equals(ListType.ItemsAll.TYPE_EPISODE));
if (!isVideo) { if (!isVideo) {
ViewGroup.LayoutParams layoutParams = mediaPoster.getLayoutParams(); ViewGroup.LayoutParams layoutParams = mediaPoster.getLayoutParams();
layoutParams.height = layoutParams.width; layoutParams.height = layoutParams.width;
@ -805,8 +796,8 @@ public class NowPlayingFragment extends Fragment
} }
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
poster, title, poster, title,
mediaPoster, posterWidth, posterHeight); mediaPoster, posterWidth, posterHeight);
UIUtils.loadImageIntoImageview(hostManager, art, mediaArt, displayMetrics.widthPixels, artHeight); UIUtils.loadImageIntoImageview(hostManager, art, mediaArt, displayMetrics.widthPixels, artHeight);
// Reset padding // Reset padding
@ -838,7 +829,7 @@ public class NowPlayingFragment extends Fragment
// TODO: change this check to the commeted out one when jsonrpc returns the correct type // TODO: change this check to the commeted out one when jsonrpc returns the correct type
// if (getPropertiesResult.type.equals(PlayerType.PropertyValue.TYPE_VIDEO)) { // if (getPropertiesResult.type.equals(PlayerType.PropertyValue.TYPE_VIDEO)) {
if ((getPropertiesResult.audiostreams != null) && if ((getPropertiesResult.audiostreams != null) &&
(getPropertiesResult.audiostreams.size() > 0)) { (getPropertiesResult.audiostreams.size() > 0)) {
overflowButton.setVisibility(View.VISIBLE); overflowButton.setVisibility(View.VISIBLE);
videoCastList.setVisibility(View.VISIBLE); videoCastList.setVisibility(View.VISIBLE);
@ -936,14 +927,14 @@ public class NowPlayingFragment extends Fragment
*/ */
private void setDurationInfo(String type, GlobalType.Time time, GlobalType.Time totalTime, int speed) { private void setDurationInfo(String type, GlobalType.Time time, GlobalType.Time totalTime, int speed) {
mediaTotalTime = totalTime.hours * 3600 + mediaTotalTime = totalTime.hours * 3600 +
totalTime.minutes * 60 + totalTime.minutes * 60 +
totalTime.seconds; totalTime.seconds;
mediaSeekbar.setMax(mediaTotalTime); mediaSeekbar.setMax(mediaTotalTime);
mediaDuration.setText(UIUtils.formatTime(totalTime)); mediaDuration.setText(UIUtils.formatTime(totalTime));
mediaCurrentTime = time.hours * 3600 + mediaCurrentTime = time.hours * 3600 +
time.minutes * 60 + time.minutes * 60 +
time.seconds; time.seconds;
mediaSeekbar.setProgress(mediaCurrentTime); mediaSeekbar.setProgress(mediaCurrentTime);
mediaProgress.setText(UIUtils.formatTime(time)); mediaProgress.setText(UIUtils.formatTime(time));
@ -955,24 +946,53 @@ public class NowPlayingFragment extends Fragment
} }
/** /**
* Sets the color of the mute volume button according to the player's status * Sets UI volume state
* @param isMuted Whether the player is muted * @param muted whether volume is muted
* @param volume
*/ */
private void setVolumeMuteButton(Boolean isMuted) { private void setVolumeState(Boolean muted, int volume) {
if (!isAdded()) return; if (!isAdded()) return;
if (isMuted) {
if (muted) {
volumeSeekBar.setProgress(0);
volumeTextView.setText(R.string.muted);
Resources.Theme theme = getActivity().getTheme(); Resources.Theme theme = getActivity().getTheme();
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
R.attr.colorAccent}); R.attr.colorAccent});
volumeMuteButton.setColorFilter( volumeMuteButton.setColorFilter(
styledAttributes.getColor(0, styledAttributes.getColor(0,
getActivity().getResources().getColor(R.color.accent_default))); getActivity().getResources().getColor(R.color.accent_default)));
styledAttributes.recycle(); styledAttributes.recycle();
} else { } else {
volumeSeekBar.setProgress(volume);
volumeTextView.setText(String.valueOf(volume));
volumeMuteButton.clearColorFilter(); volumeMuteButton.clearColorFilter();
} }
} }
private SeekBar.OnSeekBarChangeListener volumeSeekbarChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
volumeTextView.setText(String.valueOf(progress));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
new Application.SetVolume(seekBar.getProgress())
.execute(hostManager.getConnection(), defaultIntActionCallback, callbackHandler);
}
};
/** /**
* Seekbar change listener. Sends seek commands to XBMC based on the seekbar position * Seekbar change listener. Sends seek commands to XBMC based on the seekbar position
*/ */

View File

@ -179,14 +179,7 @@
android:orientation="horizontal" android:orientation="horizontal"
style="@style/ButtonBar" style="@style/ButtonBar"
android:background="?attr/contentBackgroundColor"> android:background="?attr/contentBackgroundColor">
<ImageButton
android:id="@+id/volume_down"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconVolumeDown"
android:contentDescription="@string/volume_down"/>
<ImageButton <ImageButton
android:id="@+id/volume_mute" android:id="@+id/volume_mute"
android:layout_width="0dp" android:layout_width="0dp"
@ -195,14 +188,26 @@
style="@style/Widget.Button.Borderless" style="@style/Widget.Button.Borderless"
android:src="?attr/iconVolumeMute" android:src="?attr/iconVolumeMute"
android:contentDescription="@string/volume_mute"/> android:contentDescription="@string/volume_mute"/>
<ImageButton <LinearLayout
android:id="@+id/volume_up"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" android:layout_height="match_parent"
style="@style/Widget.Button.Borderless" android:layout_weight="2"
android:src="?attr/iconVolumeUp" android:orientation="vertical">
android:contentDescription="@string/volume_up"/> <TextView
android:id="@+id/volume_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
style="@style/TextAppearance.Media.SmallDetails"
android:textSize="10sp"/>
<SeekBar
android:id="@+id/volume_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Button.Borderless"
android:layout_gravity="bottom"/>
</LinearLayout>
<ImageButton <ImageButton
android:id="@+id/repeat" android:id="@+id/repeat"
android:layout_width="0dp" android:layout_width="0dp"

View File

@ -393,5 +393,6 @@
<string name="by_album">By album</string> <string name="by_album">By album</string>
<string name="by_artist">By artist</string> <string name="by_artist">By artist</string>
<string name="by_artist_and_year">By artist and year</string> <string name="by_artist_and_year">By artist and year</string>
<string name="muted">muted</string>
</resources> </resources>