From c5848ce648110590602d57480f1a7a15693cb2b9 Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Wed, 30 Nov 2016 13:20:21 +0100 Subject: [PATCH] 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 --- .../kore/host/HostConnectionObserver.java | 216 ++++++++++++++---- .../org/xbmc/kore/jsonrpc/HostConnection.java | 41 ++++ .../xbmc/kore/jsonrpc/method/Application.java | 11 +- .../jsonrpc/notification/Application.java | 50 ++++ .../org/xbmc/kore/ui/NowPlayingFragment.java | 124 +++++----- .../main/res/layout/fragment_now_playing.xml | 33 +-- app/src/main/res/values/strings.xml | 1 + 7 files changed, 368 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/jsonrpc/notification/Application.java diff --git a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java index 09332e0..bc021f4 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java +++ b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java @@ -21,8 +21,10 @@ import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.HostConnection; import org.xbmc.kore.jsonrpc.method.JSONRPC; 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.System; +import org.xbmc.kore.jsonrpc.type.ApplicationType; import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.utils.LogUtils; @@ -43,10 +45,20 @@ import java.util.List; */ public class HostConnectionObserver implements HostConnection.PlayerNotificationsObserver, - HostConnection.SystemNotificationsObserver, - HostConnection.InputNotificationsObserver { + HostConnection.SystemNotificationsObserver, + HostConnection.InputNotificationsObserver, + HostConnection.ApplicationNotificationsObserver { 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 */ @@ -123,6 +135,7 @@ public class HostConnectionObserver * The list of observers */ private List playerEventsObservers = new ArrayList(); + private List applicationEventsObservers = new ArrayList<>(); // /** // * Handlers for which observer, on which to notify them @@ -130,7 +143,7 @@ public class HostConnectionObserver // private Map observerHandlerMap = new HashMap(); private Handler checkerHandler = new Handler(); - private Runnable httpCheckerRunnable = new Runnable() { + private Runnable httpPlayerCheckerRunnable = new Runnable() { @Override public void run() { 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() { @Override public void run() { @@ -158,8 +185,8 @@ public class HostConnectionObserver @Override public void onSuccess(String result) { // Ok, we've got a ping, if we were in a error or uninitialized state, update - if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || - (lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { + if ((hostState.lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || + (hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { checkWhatsPlaying(); } checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); @@ -181,14 +208,31 @@ public class HostConnectionObserver } }; - private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; - private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null; - private PlayerType.PropertyValue lastGetPropertiesResult = null; - private ListType.ItemsAll lastGetItemResult = null; - private int lastErrorCode; - private String lastErrorDescription; + public class HostState { + private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null; + private PlayerType.PropertyValue lastGetPropertiesResult = null; + private ListType.ItemsAll lastGetItemResult = null; + 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) { + this.hostState = new HostState(); this.connection = connection; } @@ -216,7 +260,7 @@ public class HostConnectionObserver // Start the ping checker checkerHandler.post(tcpCheckerRunnable); } else { - checkerHandler.post(httpCheckerRunnable); + checkerHandler.post(httpPlayerCheckerRunnable); } } } @@ -229,7 +273,8 @@ public class HostConnectionObserver playerEventsObservers.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."); if (playerEventsObservers.isEmpty()) { @@ -241,9 +286,61 @@ public class HostConnectionObserver connection.unregisterInputNotificationsObserver(this); checkerHandler.removeCallbacks(tcpCheckerRunnable); } 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); checkerHandler.removeCallbacks(tcpCheckerRunnable); } 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() { + @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 */ @@ -344,7 +474,7 @@ public class HostConnectionObserver /** * Calls Player.GetActivePlayers - * On success chains execution to chainCallGetProperties + * On success chains execution to chainCallGetPlayerProperties */ private void chainCallGetActivePlayers() { Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers(); @@ -356,7 +486,7 @@ public class HostConnectionObserver notifyNothingIsPlaying(playerEventsObservers); return; } - chainCallGetProperties(result.get(0)); + chainCallGetPlayerProperties(result.get(0)); } @Override @@ -371,7 +501,7 @@ public class HostConnectionObserver * Calls Player.GetProperties * On success chains execution to chainCallGetItem */ - private void chainCallGetProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) { + private void chainCallGetPlayerProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) { String propertiesToGet[] = new String[] { // Check is something more is needed PlayerType.PropertyName.SPEED, @@ -465,7 +595,7 @@ public class HostConnectionObserver }, 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; /** @@ -478,11 +608,11 @@ public class HostConnectionObserver private void notifyConnectionError(final int errorCode, final String description, List observers) { // Reply if different from last result if (forceReply || - (lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) || - (lastErrorCode != errorCode)) { - lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; - lastErrorCode = errorCode; - lastErrorDescription = description; + (hostState.lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) || + (hostState.lastErrorCode != errorCode)) { + hostState.lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; + hostState.lastErrorCode = errorCode; + hostState.lastErrorDescription = description; forceReply = false; // Copy list to prevent ConcurrentModificationExceptions List allObservers = new ArrayList<>(observers); @@ -519,8 +649,8 @@ public class HostConnectionObserver private void notifyNothingIsPlaying(List observers) { // Reply if forced or different from last result if (forceReply || - (lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) { - lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED; + (hostState.lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) { + hostState.lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED; forceReply = false; // Copy list to prevent ConcurrentModificationExceptions List allObservers = new ArrayList<>(observers); @@ -554,16 +684,16 @@ public class HostConnectionObserver int currentCallResult = (getPropertiesResult.speed == 0) ? PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; if (forceReply || - (lastCallResult != currentCallResult) || - (lastGetPropertiesResult.speed != getPropertiesResult.speed) || - (lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || - (!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) || - (lastGetItemResult.id != getItemResult.id) || - (!lastGetItemResult.label.equals(getItemResult.label))) { - lastCallResult = currentCallResult; - lastGetActivePlayerResult = getActivePlayersResult; - lastGetPropertiesResult = getPropertiesResult; - lastGetItemResult = getItemResult; + (hostState.lastCallResult != currentCallResult) || + (hostState.lastGetPropertiesResult.speed != getPropertiesResult.speed) || + (hostState.lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || + (!hostState.lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) || + (hostState.lastGetItemResult.id != getItemResult.id) || + (!hostState.lastGetItemResult.label.equals(getItemResult.label))) { + hostState.lastCallResult = currentCallResult; + hostState.lastGetActivePlayerResult = getActivePlayersResult; + hostState.lastGetPropertiesResult = getPropertiesResult; + hostState.lastGetItemResult = getItemResult; forceReply = false; // Copy list to prevent ConcurrentModificationExceptions List allObservers = new ArrayList<>(observers); @@ -619,16 +749,16 @@ public class HostConnectionObserver * @param observer Obserser to call with last result */ public void replyWithLastResult(PlayerEventsObserver observer) { - switch (lastCallResult) { + switch (hostState.lastCallResult) { case PlayerEventsObserver.PLAYER_CONNECTION_ERROR: - notifyConnectionError(lastErrorCode, lastErrorDescription, observer); + notifyConnectionError(hostState.lastErrorCode, hostState.lastErrorDescription, observer); break; case PlayerEventsObserver.PLAYER_IS_STOPPED: notifyNothingIsPlaying(observer); break; case PlayerEventsObserver.PLAYER_IS_PAUSED: case PlayerEventsObserver.PLAYER_IS_PLAYING: - notifySomethingIsPlaying(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult, observer); + notifySomethingIsPlaying(hostState.lastGetActivePlayerResult, hostState.lastGetPropertiesResult, hostState.lastGetItemResult, observer); break; case PlayerEventsObserver.PLAYER_NO_RESULT: observer.playerNoResultsYet(); @@ -643,4 +773,8 @@ public class HostConnectionObserver forceReply = true; chainCallGetActivePlayers(); } + + public HostState getHostState() { + return hostState; + } } diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java index c06cb5d..d7729f3 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java @@ -32,6 +32,7 @@ import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; 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.Player; import org.xbmc.kore.jsonrpc.notification.System; @@ -92,6 +93,10 @@ public class HostConnection { public void onInputRequested(Input.OnInputRequested notification); } + public interface ApplicationNotificationsObserver { + public void onVolumeChanged(Application.OnVolumeChanged notification); + } + /** * Host to connect too */ @@ -139,6 +144,12 @@ public class HostConnection { private final HashMap inputNotificationsObservers = new HashMap(); + /** + * The observers that will be notified of application notifications + */ + private final HashMap applicationNotificationsObservers = + new HashMap<>(); + private ExecutorService executorService; private final int connectTimeout; @@ -250,6 +261,23 @@ public class HostConnection { 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 * 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()); diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java b/app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java index 57ec44e..301b71b 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java @@ -57,7 +57,7 @@ public class Application { 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} */ public SetVolume(String volume) { @@ -65,6 +65,15 @@ public class Application { addParameterToRequest("volume", volume); } + /** + * Set the volume + * @param volume volume between 0 and 100 + */ + public SetVolume(int volume) { + super(); + addParameterToRequest("volume", volume); + } + @Override public String getMethodName() { return METHOD_NAME; } diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Application.java b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Application.java new file mode 100644 index 0000000..3575fb8 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Application.java @@ -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; } + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/NowPlayingFragment.java b/app/src/main/java/org/xbmc/kore/ui/NowPlayingFragment.java index cd24a1c..57c48e5 100644 --- a/app/src/main/java/org/xbmc/kore/ui/NowPlayingFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/NowPlayingFragment.java @@ -50,7 +50,6 @@ import org.xbmc.kore.jsonrpc.method.Application; import org.xbmc.kore.jsonrpc.method.GUI; import org.xbmc.kore.jsonrpc.method.Input; 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.ListType; import org.xbmc.kore.jsonrpc.type.PlayerType; @@ -72,7 +71,8 @@ import butterknife.OnClick; */ public class NowPlayingFragment extends Fragment implements HostConnectionObserver.PlayerEventsObserver, - GenericSelectDialog.GenericSelectDialogListener { + HostConnectionObserver.ApplicationEventsObserver, + GenericSelectDialog.GenericSelectDialogListener { 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.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.shuffle) ImageButton shuffleButton; @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.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.rating) TextView mediaRating; @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); 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 // Full dim on 4 * iconSize dp Resources resources = getActivity().getResources(); @@ -222,16 +218,7 @@ public class NowPlayingFragment extends Fragment public void onResume() { super.onResume(); hostConnectionObserver.registerPlayerObserver(this, true); - Application.GetProperties action = new Application.GetProperties(Application.GetProperties.MUTED); - action.execute(hostManager.getConnection(), new ApiCallback() { - @Override - public void onSuccess(ApplicationType.PropertyValue result) { - setVolumeMuteButton(result.muted); - } - - @Override - public void onError(int errorCode, String description) { } - }, callbackHandler); + hostConnectionObserver.registerApplicationObserver(this, true); } @Override @@ -239,6 +226,7 @@ public class NowPlayingFragment extends Fragment super.onPause(); stopNowPlayingInfo(); hostConnectionObserver.unregisterPlayerObserver(this); + hostConnectionObserver.unregisterApplicationObserver(this); } /** @@ -262,16 +250,6 @@ public class NowPlayingFragment extends Fragment public void onError(int errorCode, String description) { } }; - private void setupVolumeRepeatButton(View button, final ApiMethod 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 */ @@ -317,11 +295,17 @@ public class NowPlayingFragment extends Fragment */ @OnClick(R.id.volume_mute) 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(); action.execute(hostManager.getConnection(), new ApiCallback() { @Override public void onSuccess(Boolean result) { - setVolumeMuteButton(result); + //We depend on the listener to correct the mute button state } @Override @@ -386,7 +370,7 @@ public class NowPlayingFragment extends Fragment case R.id.audiostreams: // Setup audiostream select dialog 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); @@ -394,21 +378,21 @@ public class NowPlayingFragment extends Fragment for (int i = 0; i < availableAudioStreams.size(); i++) { PlayerType.AudioStream current = availableAudioStreams.get(i); 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) { selectedItem = i + ADDED_AUDIO_OPTIONS; } } 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); } return true; case R.id.subtitles: // Setup subtitles select dialog 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[1] = getString(R.string.subtitle_sync); @@ -418,7 +402,7 @@ public class NowPlayingFragment extends Fragment for (int i = 0; i < availableSubtitles.size(); i++) { PlayerType.Subtitle current = availableSubtitles.get(i); 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) { selectedItem = i + ADDED_SUBTITLE_OPTIONS; } @@ -426,7 +410,7 @@ public class NowPlayingFragment extends Fragment } 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); return true; } @@ -519,8 +503,8 @@ public class NowPlayingFragment extends Fragment public void onError(int errorCode, String description) { if (!isAdded()) return; Toast.makeText(getActivity(), - String.format(getString(R.string.error_executing_subtitles), description), - Toast.LENGTH_SHORT).show(); + String.format(getString(R.string.error_executing_subtitles), description), + Toast.LENGTH_SHORT).show(); } }, callbackHandler); } @@ -614,6 +598,11 @@ public class NowPlayingFragment extends Fragment playerNoResultsYet(); } + @Override + public void applicationOnVolumeChanged(int volume, boolean muted) { + setVolumeState(muted, volume); + } + // Ignore this public void inputOnInputRequested(String title, String type, String value) {} public void observerOnStopObserving() {} @@ -679,7 +668,7 @@ public class NowPlayingFragment extends Fragment title = getItemResult.title; underTitle = Utils.listStringConcat(getItemResult.artist, ", ") - + " | " + getItemResult.album; + + " | " + getItemResult.album; art = getItemResult.fanart; poster = getItemResult.thumbnail; @@ -783,6 +772,8 @@ public class NowPlayingFragment extends Fragment } styledAttributes.recycle(); + volumeSeekBar.setOnSeekBarChangeListener(volumeSeekbarChangeListener); + Resources resources = getActivity().getResources(); DisplayMetrics displayMetrics = new 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 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) { ViewGroup.LayoutParams layoutParams = mediaPoster.getLayoutParams(); layoutParams.height = layoutParams.width; @@ -805,8 +796,8 @@ public class NowPlayingFragment extends Fragment } UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, - poster, title, - mediaPoster, posterWidth, posterHeight); + poster, title, + mediaPoster, posterWidth, posterHeight); UIUtils.loadImageIntoImageview(hostManager, art, mediaArt, displayMetrics.widthPixels, artHeight); // 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 // if (getPropertiesResult.type.equals(PlayerType.PropertyValue.TYPE_VIDEO)) { if ((getPropertiesResult.audiostreams != null) && - (getPropertiesResult.audiostreams.size() > 0)) { + (getPropertiesResult.audiostreams.size() > 0)) { overflowButton.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) { mediaTotalTime = totalTime.hours * 3600 + - totalTime.minutes * 60 + - totalTime.seconds; + totalTime.minutes * 60 + + totalTime.seconds; mediaSeekbar.setMax(mediaTotalTime); mediaDuration.setText(UIUtils.formatTime(totalTime)); mediaCurrentTime = time.hours * 3600 + - time.minutes * 60 + - time.seconds; + time.minutes * 60 + + time.seconds; mediaSeekbar.setProgress(mediaCurrentTime); 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 - * @param isMuted Whether the player is muted + * Sets UI volume state + * @param muted whether volume is muted + * @param volume */ - private void setVolumeMuteButton(Boolean isMuted) { + private void setVolumeState(Boolean muted, int volume) { if (!isAdded()) return; - if (isMuted) { + + if (muted) { + volumeSeekBar.setProgress(0); + volumeTextView.setText(R.string.muted); + Resources.Theme theme = getActivity().getTheme(); TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { R.attr.colorAccent}); volumeMuteButton.setColorFilter( styledAttributes.getColor(0, - getActivity().getResources().getColor(R.color.accent_default))); + getActivity().getResources().getColor(R.color.accent_default))); styledAttributes.recycle(); + } else { + volumeSeekBar.setProgress(volume); + volumeTextView.setText(String.valueOf(volume)); 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 */ diff --git a/app/src/main/res/layout/fragment_now_playing.xml b/app/src/main/res/layout/fragment_now_playing.xml index 1d14f3f..8dbfd4b 100644 --- a/app/src/main/res/layout/fragment_now_playing.xml +++ b/app/src/main/res/layout/fragment_now_playing.xml @@ -179,14 +179,7 @@ android:orientation="horizontal" style="@style/ButtonBar" android:background="?attr/contentBackgroundColor"> - + - + android:layout_weight="2" + android:orientation="vertical"> + + + + By album By artist By artist and year + muted