Implemented a slideup panel with media controls and info (#320)

* The slideup panel is only displayed when something is
         playing. It starts collapsed showing the media poster
         and title of what is currently playing.

       * Media controls implemented are volume, progress, shuffle,
         repeat and play/pause for all items. Next and previous are only
         available when a music item is playing.

       * In collapsed mode the panel will display the mute button only
         if Kodi is muted. The mute button in expanded mode is always
         visible.

       * Panel is enabled by default. Users can disable the panel
         in Settings

       * Implemented listening to Player.OnPropertyChanged notifications
         to update shuffle and repeat button states.
This commit is contained in:
Martijn Brekhof 2017-07-13 20:10:49 +02:00 committed by Synced Synapse
parent 7186874471
commit cb430aa20d
24 changed files with 1054 additions and 85 deletions

View File

@ -42,6 +42,7 @@ Credits
- [PagerSlidingTabStrip](https://github.com/astuetz/PagerSlidingTabStrip)
- [FloatingActionButton](https://github.com/makovkastar/FloatingActionButton)
- [ExpandableTextView](https://github.com/Blogcat/Android-ExpandableTextView)
- [AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel)
Links
-----

View File

@ -125,6 +125,7 @@ dependencies {
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'com.melnykov:floatingactionbutton:1.3.0'
compile 'at.blogc:expandabletextview:1.0.3'
compile 'com.sothree.slidinguppanel:library:3.3.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'

View File

@ -77,6 +77,10 @@ public class Settings {
public static final String KEY_PREF_SHOW_NOTIFICATION = "pref_show_notification";
public static final boolean DEFAULT_PREF_SHOW_NOTIFICATION = false;
// Show now playing panel
public static final String KEY_PREF_SHOW_NOW_PLAYING_PANEL = "pref_show_nowplayingpanel";
public static final boolean DEFAULT_PREF_SHOW_NOW_PLAYING_PANEL = true;
// Pause during calls
public static final String KEY_PREF_PAUSE_DURING_CALLS = "pref_pause_during_calls";
public static final boolean DEFAULT_PREF_PAUSE_DURING_CALLS = false;

View File

@ -22,6 +22,7 @@ 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.Player.NotificationsData;
import org.xbmc.kore.jsonrpc.notification.Input;
import org.xbmc.kore.jsonrpc.notification.System;
import org.xbmc.kore.jsonrpc.type.ApplicationType;
@ -73,6 +74,8 @@ public class HostConnectionObserver
PLAYER_IS_PAUSED = 3,
PLAYER_IS_STOPPED = 4;
public void playerOnPropertyChanged(NotificationsData notificationsData);
/**
* Notifies that something is playing
* @param getActivePlayerResult Active player obtained by a call to {@link org.xbmc.kore.jsonrpc.method.Player.GetActivePlayers}
@ -178,15 +181,17 @@ public class HostConnectionObserver
final int PING_AFTER_ERROR_CHECK_INTERVAL = 2000,
PING_AFTER_SUCCESS_CHECK_INTERVAL = 10000;
// If no one is listening to this, just exit
if (playerEventsObservers.isEmpty()) return;
if (playerEventsObservers.isEmpty() && applicationEventsObservers.isEmpty()) return;
JSONRPC.Ping ping = new JSONRPC.Ping();
ping.execute(connection, new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
// Ok, we've got a ping, if we were in a error or uninitialized state, update
if ((hostState.lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) ||
(hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) {
// Ok, we've got a ping, if there are playerEventsObservers and
// we were in a error or uninitialized state, update
if ((! playerEventsObservers.isEmpty()) &&
((hostState.lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) ||
(hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR))) {
checkWhatsPlaying();
}
checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL);
@ -244,9 +249,7 @@ public class HostConnectionObserver
if (this.connection == null)
return;
// Save this observer and a new handle to notify him
playerEventsObservers.add(observer);
// observerHandlerMap.put(observer, new Handler());
if (replyImmediately) replyWithLastResult(observer);
@ -316,6 +319,7 @@ public class HostConnectionObserver
// as a connection observer, which we will pass to the "real" observer
if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) {
connection.registerApplicationNotificationsObserver(this, checkerHandler);
checkerHandler.post(tcpCheckerRunnable);
} else {
checkerHandler.post(httpApplicationCheckerRunnable);
}
@ -337,7 +341,7 @@ public class HostConnectionObserver
// 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);
connection.unregisterApplicationNotificationsObserver(this);
} else {
checkerHandler.removeCallbacks(httpApplicationCheckerRunnable);
}
@ -364,6 +368,14 @@ public class HostConnectionObserver
hostState.lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT;
}
@Override
public void onPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.OnPropertyChanged notification) {
List<PlayerEventsObserver> allObservers = new ArrayList<>(playerEventsObservers);
for (final PlayerEventsObserver observer : allObservers) {
observer.playerOnPropertyChanged(notification.data);
}
}
/**
* The {@link HostConnection.PlayerNotificationsObserver} interface methods
*/
@ -746,7 +758,7 @@ public class HostConnectionObserver
/**
* Replies to the observer with the last result we got.
* If we have no result, nothing will be called on the observer interface.
* @param observer Obserser to call with last result
* @param observer Observer to call with last result
*/
public void replyWithLastResult(PlayerEventsObserver observer) {
switch (hostState.lastCallResult) {

View File

@ -42,7 +42,6 @@ import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.Socket;
@ -70,6 +69,7 @@ public class HostConnection {
* Interface that an observer must implement to be notified of player notifications
*/
public interface PlayerNotificationsObserver {
public void onPropertyChanged(Player.OnPropertyChanged notification);
public void onPlay(Player.OnPlay notification);
public void onPause(Player.OnPause notification);
public void onSpeedChanged(Player.OnSpeedChanged notification);
@ -274,7 +274,7 @@ public class HostConnection {
* Unregisters and observer from the input notifications
* @param observer The {@link InputNotificationsObserver}
*/
public void unregisterApplicationotificationsObserver(ApplicationNotificationsObserver observer) {
public void unregisterApplicationNotificationsObserver(ApplicationNotificationsObserver observer) {
applicationNotificationsObservers.remove(observer);
}
@ -661,6 +661,18 @@ public class HostConnection {
}
});
}
} else if (notificationName.equals(Player.OnPropertyChanged.NOTIFICATION_NAME)) {
final Player.OnPropertyChanged apiNotification = new Player.OnPropertyChanged(params);
for (final PlayerNotificationsObserver observer :
playerNotificationsObservers.keySet()) {
Handler handler = playerNotificationsObservers.get(observer);
handler.post(new Runnable() {
@Override
public void run() {
observer.onPropertyChanged(apiNotification);
}
});
}
} else if (notificationName.equals(System.OnQuit.NOTIFICATION_NAME)) {
final System.OnQuit apiNotification = new System.OnQuit(params);
for (final SystemNotificationsObserver observer :

View File

@ -16,6 +16,7 @@
package org.xbmc.kore.jsonrpc.notification;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.xbmc.kore.jsonrpc.ApiNotification;
import org.xbmc.kore.jsonrpc.type.GlobalType;
@ -26,6 +27,23 @@ import org.xbmc.kore.utils.JsonUtils;
*/
public class Player {
/**
* Player.OnPropertyChanged notification
* Player properties have changed. Such as repeat type and shuffle mode
*/
public static class OnPropertyChanged extends ApiNotification {
public static final String NOTIFICATION_NAME = "Player.OnPropertyChanged";
public final NotificationsData data;
public OnPropertyChanged(ObjectNode node) {
super(node);
data = new NotificationsData(node.get(NotificationsData.DATA_NODE));
}
public String getNotificationName() { return NOTIFICATION_NAME; }
}
/**
* Player.OnPause notification
* Playback of a media item has been paused. If there is no ID available extra information will be provided.
@ -179,15 +197,44 @@ public class Player {
}
}
/**
* Notification data for player properties
*/
public static class NotificationsProperty {
public static final String PROPERTY_NODE = "property";
public final Boolean shuffled;
public final String repeatMode;
public NotificationsProperty(JsonNode node) {
JsonNode shuffledNode = node.get("shuffled");
if (shuffledNode != null)
shuffled = shuffledNode.asBoolean();
else
shuffled = null;
repeatMode = JsonUtils.stringFromJsonNode(node, "repeat");
}
}
public static class NotificationsData {
public static final String DATA_NODE = "data";
public final NotificationsPlayer player;
public final NotificationsItem item;
public final NotificationsProperty property;
public NotificationsData(JsonNode node) {
item = new NotificationsItem((ObjectNode)node.get(NotificationsItem.ITEM_NODE));
player = new NotificationsPlayer((ObjectNode)node.get(NotificationsPlayer.PLAYER_NODE));
JsonNode jsonNode = node.get(NotificationsItem.ITEM_NODE);
item = (jsonNode != null) ? new NotificationsItem(jsonNode) : null;
jsonNode = node.get(NotificationsPlayer.PLAYER_NODE);
player = (jsonNode != null)
? new NotificationsPlayer(jsonNode)
: null;
jsonNode = node.get(NotificationsProperty.PROPERTY_NODE);
property = (jsonNode != null) ? new NotificationsProperty(jsonNode) : null;
}
}

View File

@ -26,6 +26,7 @@ import android.support.v4.content.ContextCompat;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostConnectionObserver;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.notification.Player;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.utils.LogUtils;
@ -150,6 +151,11 @@ public class ConnectionObserversManagerService extends Service
}
}
@Override
public void playerOnPropertyChanged(Player.NotificationsData notificationsData) {
}
/**
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/

View File

@ -17,10 +17,8 @@ package org.xbmc.kore.service;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
@ -36,6 +34,7 @@ import com.squareup.picasso.Target;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostConnectionObserver;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.notification.Player;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
@ -68,6 +67,11 @@ public class NotificationObserver
mRemoteStartPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
public void playerOnPropertyChanged(Player.NotificationsData notificationsData) {
}
/**
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/

View File

@ -74,6 +74,11 @@ public class PauseCallObserver extends PhoneStateListener
}
}
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
}
@Override
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,

View File

@ -17,43 +17,88 @@ package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.transition.TransitionInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostConnectionObserver;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.ApiMethod;
import org.xbmc.kore.jsonrpc.method.Application;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
import org.xbmc.kore.ui.widgets.MediaProgressIndicator;
import org.xbmc.kore.ui.widgets.NowPlayingPanel;
import org.xbmc.kore.ui.widgets.VolumeLevelIndicator;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
public abstract class BaseMediaActivity extends AppCompatActivity {
import butterknife.ButterKnife;
import butterknife.InjectView;
public abstract class BaseMediaActivity extends BaseActivity
implements HostConnectionObserver.ApplicationEventsObserver,
HostConnectionObserver.PlayerEventsObserver,
NowPlayingPanel.OnPanelButtonsClickListener,
MediaProgressIndicator.OnProgressChangeListener {
private static final String TAG = LogUtils.makeLogTag(BaseMediaActivity.class);
private static final String NAVICON_ISARROW = "navstate";
private static final String ACTIONBAR_TITLE = "actionbartitle";
@InjectView(R.id.now_playing_panel) NowPlayingPanel nowPlayingPanel;
private NavigationDrawerFragment navigationDrawerFragment;
private SharedElementTransition sharedElementTransition = new SharedElementTransition();
private boolean drawerIndicatorIsArrow;
private int currentActivePlayerId = -1;
private HostManager hostManager;
private HostConnectionObserver hostConnectionObserver;
private boolean showNowPlayingPanel;
protected abstract String getActionBarTitle();
protected abstract Fragment createFragment();
/**
* Default callback for methods that don't return anything
*/
private ApiCallback<String> defaultStringActionCallback = ApiMethod.getDefaultActionCallback();
private Handler callbackHandler = new Handler();
private ApiCallback<Integer> defaultIntActionCallback = ApiMethod.getDefaultActionCallback();
private Runnable hidePanelRunnable = new Runnable() {
@Override
public void run() {
nowPlayingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
}
};
@Override
@TargetApi(21)
protected void onCreate(Bundle savedInstanceState) {
@ -61,13 +106,10 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
if (Utils.isLollipopOrLater()) {
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
setTheme(UIUtils.getThemeResourceId(
prefs.getString(Settings.KEY_PREF_THEME, Settings.DEFAULT_PREF_THEME)));
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_generic_media);
ButterKnife.inject(this);
// Set up the drawer.
navigationDrawerFragment = (NavigationDrawerFragment)getSupportFragmentManager()
@ -113,6 +155,8 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
if (Utils.isLollipopOrLater()) {
sharedElementTransition.setupExitTransition(this, fragment);
}
hostManager = HostManager.getInstance(this);
}
@Override
@ -129,6 +173,38 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
}
}
@Override
protected void onResume() {
super.onResume();
showNowPlayingPanel = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(Settings.KEY_PREF_SHOW_NOW_PLAYING_PANEL,
Settings.DEFAULT_PREF_SHOW_NOW_PLAYING_PANEL);
if(showNowPlayingPanel) {
setupNowPlayingPanel();
} else {
//Hide it in case we were displaying the panel and user disabled showing
//the panel in Settings
nowPlayingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
}
}
@Override
public void onPause() {
super.onPause();
if(!showNowPlayingPanel)
return;
hostConnectionObserver = hostManager.getHostConnectionObserver();
if (hostConnectionObserver == null)
return;
hostConnectionObserver.unregisterApplicationObserver(this);
hostConnectionObserver.unregisterPlayerObserver(this);
}
public boolean getDrawerIndicatorIsArrow() {
return drawerIndicatorIsArrow;
}
@ -187,4 +263,272 @@ public abstract class BaseMediaActivity extends AppCompatActivity {
.addToBackStack(null)
.commit();
}
@Override
public void applicationOnVolumeChanged(int volume, boolean muted) {
nowPlayingPanel.setVolume(volume, muted);
}
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
if (notificationsData.property.shuffled != null)
nowPlayingPanel.setShuffled(notificationsData.property.shuffled);
if (notificationsData.property.repeatMode != null )
nowPlayingPanel.setRepeatMode(notificationsData.property.repeatMode);
}
@Override
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
currentActivePlayerId = getActivePlayerResult.playerid;
updateNowPlayingPanel(getPropertiesResult, getItemResult);
}
@Override
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, PlayerType.PropertyValue getPropertiesResult, ListType.ItemsAll getItemResult) {
currentActivePlayerId = getActivePlayerResult.playerid;
updateNowPlayingPanel(getPropertiesResult, getItemResult);
}
@Override
public void playerOnStop() {
//We delay hiding the panel to prevent hiding the panel when playing
// the next item in a playlist
callbackHandler.removeCallbacks(hidePanelRunnable);
callbackHandler.postDelayed(hidePanelRunnable, 1000);
}
@Override
public void playerOnConnectionError(int errorCode, String description) {
}
@Override
public void playerNoResultsYet() {
}
@Override
public void observerOnStopObserving() {
nowPlayingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
}
@Override
public void systemOnQuit() {
nowPlayingPanel.setPanelState(SlidingUpPanelLayout.PanelState.HIDDEN);
}
@Override
public void inputOnInputRequested(String title, String type, String value) {
}
@Override
public void onProgressChanged(int progress) {
PlayerType.PositionTime positionTime = new PlayerType.PositionTime(progress);
Player.Seek seekAction = new Player.Seek(currentActivePlayerId, positionTime);
seekAction.execute(HostManager.getInstance(this).getConnection(), new ApiCallback<PlayerType.SeekReturnType>() {
@Override
public void onSuccess(PlayerType.SeekReturnType result) {
// Ignore
}
@Override
public void onError(int errorCode, String description) {
LogUtils.LOGE(TAG, "Got an error calling Player.Seek. Error code: " + errorCode + ", description: " + description);
}
}, new Handler());
}
@Override
public void onPlayClicked() {
Player.PlayPause action = new Player.PlayPause(currentActivePlayerId);
action.execute(hostManager.getConnection(), defaultIntActionCallback, callbackHandler);
}
@Override
public void onPreviousClicked() {
Player.GoTo action = new Player.GoTo(currentActivePlayerId, Player.GoTo.PREVIOUS);
action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler);
}
@Override
public void onNextClicked() {
Player.GoTo action = new Player.GoTo(currentActivePlayerId, Player.GoTo.NEXT);
action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler);
}
@Override
public void onVolumeMuteClicked() {
Application.SetMute action = new Application.SetMute();
action.execute(hostManager.getConnection(), new ApiCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
//We depend on the listener to correct the mute button state
}
@Override
public void onError(int errorCode, String description) { }
}, new Handler());
}
@Override
public void onShuffleClicked() {
Player.SetShuffle action = new Player.SetShuffle(currentActivePlayerId);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
//We depend on the listener to correct the mute button state
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
}
@Override
public void onRepeatClicked() {
Player.SetRepeat action = new Player.SetRepeat(currentActivePlayerId, PlayerType.Repeat.CYCLE);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
//We depend on the listener to correct the mute button state
}
@Override
public void onError(int errorCode, String description) { }
}, callbackHandler);
}
@Override
public void onVolumeMutedIndicatorClicked() {
Application.SetMute action = new Application.SetMute();
action.execute(hostManager.getConnection(), new ApiCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
//We depend on the listener to correct the mute button state
}
@Override
public void onError(int errorCode, String description) { }
}, new Handler());
}
private void setupNowPlayingPanel() {
nowPlayingPanel.setOnVolumeChangeListener(new VolumeLevelIndicator.OnVolumeChangeListener() {
@Override
public void onVolumeChanged(int volume) {
new Application.SetVolume(volume)
.execute(hostManager.getConnection(), defaultIntActionCallback, new Handler());
}
});
nowPlayingPanel.setOnPanelButtonsClickListener(this);
nowPlayingPanel.setOnProgressChangeListener(this);
hostConnectionObserver = hostManager.getHostConnectionObserver();
if (hostConnectionObserver == null)
return;
hostConnectionObserver.registerApplicationObserver(this, true);
hostConnectionObserver.registerPlayerObserver(this, true);
hostConnectionObserver.forceRefreshResults();
}
private void updateNowPlayingPanel(PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
String title;
String poster;
String details = null;
callbackHandler.removeCallbacks(hidePanelRunnable);
// Only set state to collapsed if panel is currently hidden. This prevents collapsing
// the panel when the user expanded the panel and started playing the item from a paused
// state
if (nowPlayingPanel.getPanelState() == SlidingUpPanelLayout.PanelState.HIDDEN) {
nowPlayingPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
}
nowPlayingPanel.setMediaProgress(getPropertiesResult.time, getPropertiesResult.totaltime);
nowPlayingPanel.setPlayButton(getPropertiesResult.speed > 0);
nowPlayingPanel.setShuffled(getPropertiesResult.shuffled);
nowPlayingPanel.setRepeatMode(getPropertiesResult.repeat);
nowPlayingPanel.setSpeed(getPropertiesResult.speed);
switch (getItemResult.type) {
case ListType.ItemsAll.TYPE_MOVIE:
title = getItemResult.title;
details = getItemResult.tagline;
poster = TextUtils.isEmpty(getItemResult.thumbnail) ? getItemResult.fanart
: getItemResult.thumbnail;
break;
case ListType.ItemsAll.TYPE_EPISODE:
title = getItemResult.title;
String seasonEpisode = String.format(getString(R.string.season_episode),
getItemResult.season, getItemResult.episode);
details = String.format("%s | %s", getItemResult.showtitle, seasonEpisode);
poster = TextUtils.isEmpty(getItemResult.art.poster) ? getItemResult.art.fanart
: getItemResult.art.poster;
break;
case ListType.ItemsAll.TYPE_SONG:
title = getItemResult.title;
details = getItemResult.displayartist + " | " + getItemResult.album;
poster = TextUtils.isEmpty(getItemResult.thumbnail) ? getItemResult.fanart
: getItemResult.thumbnail;
break;
case ListType.ItemsAll.TYPE_MUSIC_VIDEO:
title = getItemResult.title;
details = Utils.listStringConcat(getItemResult.artist, ", ") + " | " + getItemResult.album;
poster = TextUtils.isEmpty(getItemResult.thumbnail) ? getItemResult.fanart
: getItemResult.thumbnail;
break;
case ListType.ItemsAll.TYPE_CHANNEL:
title = getItemResult.label;
details = getItemResult.title;
poster = TextUtils.isEmpty(getItemResult.thumbnail) ? getItemResult.fanart
: getItemResult.thumbnail;
break;
default:
title = getItemResult.label;
poster = TextUtils.isEmpty(getItemResult.thumbnail) ? getItemResult.fanart
: getItemResult.thumbnail;
break;
}
if (title.contentEquals(nowPlayingPanel.getTitle()))
return; // Still showing same item as previous call
nowPlayingPanel.setTitle(title);
if (details != null) {
nowPlayingPanel.setDetails(details);
}
if ((getItemResult.type.contentEquals(ListType.ItemsAll.TYPE_MUSIC_VIDEO)) ||
(getItemResult.type.contentEquals(ListType.ItemsAll.TYPE_SONG))) {
nowPlayingPanel.setNextPrevVisibility(View.VISIBLE);
} else {
nowPlayingPanel.setNextPrevVisibility(View.GONE);
}
Resources resources = getResources();
int posterWidth = resources.getDimensionPixelOffset(R.dimen.notification_art_slim_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.notification_art_slim_height);
// 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));
nowPlayingPanel.setSquarePoster(!isVideo);
UIUtils.loadImageWithCharacterAvatar(this, hostManager, poster, title,
nowPlayingPanel.getPoster(),
(isVideo) ? posterWidth : posterHeight, posterHeight);
}
}

View File

@ -84,7 +84,6 @@ public class NowPlayingFragment extends Fragment
*/
public interface NowPlayingListener {
public void SwitchToRemotePanel();
public void onShuffleClicked();
}
/**
@ -126,19 +125,17 @@ public class NowPlayingFragment extends Fragment
private int currentSubtitleIndex = -1;
private int currentAudiostreamIndex = -1;
private ApiCallback<Integer> defaultIntActionCallback = ApiMethod.getDefaultActionCallback();
private ApiCallback<Boolean> defaultBooleanActionCallback = ApiMethod.getDefaultActionCallback();
/**
* Injectable views
*/
@InjectView(R.id.play) ImageButton playButton;
@InjectView(R.id.stop) ImageButton stopButton;
@InjectView(R.id.previous) ImageButton previousButton;
@InjectView(R.id.next) ImageButton nextButton;
@InjectView(R.id.rewind) ImageButton rewindButton;
@InjectView(R.id.fast_forward) ImageButton fastForwardButton;
@InjectView(R.id.repeat) RepeatModeButton repeatButton;
@InjectView(R.id.volume_mute) HighlightButton volumeMuteButton;
@InjectView(R.id.shuffle) HighlightButton shuffleButton;
@InjectView(R.id.repeat) RepeatModeButton repeatButton;
@InjectView(R.id.overflow) ImageButton overflowButton;
@InjectView(R.id.info_panel) RelativeLayout infoPanel;
@ -156,7 +153,6 @@ public class NowPlayingFragment extends Fragment
@InjectView(R.id.volume_level_indicator) VolumeLevelIndicator volumeLevelIndicator;
@InjectView(R.id.media_details) RelativeLayout mediaDetailsPanel;
@InjectView(R.id.rating) TextView mediaRating;
@InjectView(R.id.max_rating) TextView mediaMaxRating;
@InjectView(R.id.year) TextView mediaYear;
@ -189,6 +185,16 @@ public class NowPlayingFragment extends Fragment
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_now_playing, container, false);
ButterKnife.inject(this, root);
volumeLevelIndicator.setOnVolumeChangeListener(new VolumeLevelIndicator.OnVolumeChangeListener() {
@Override
public void onVolumeChanged(int volume) {
new Application.SetVolume(volume)
.execute(hostManager.getConnection(), defaultIntActionCallback, new Handler());
}
});
mediaProgressIndicator.setOnProgressChangeListener(this);
// Setup dim the fanart when scroll changes
// Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
@ -242,8 +248,6 @@ public class NowPlayingFragment extends Fragment
* Default callback for methods that don't return anything
*/
private ApiCallback<String> defaultStringActionCallback = ApiMethod.getDefaultActionCallback();
private ApiCallback<Integer> defaultIntActionCallback = ApiMethod.getDefaultActionCallback();
private ApiCallback<Boolean> defaultBooleanActionCallback = ApiMethod.getDefaultActionCallback();
/**
* Callback for methods that change the play speed
@ -252,7 +256,7 @@ public class NowPlayingFragment extends Fragment
@Override
public void onSuccess(Integer result) {
if (!isAdded()) return;
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, result);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, result == 1);
}
@Override
@ -272,7 +276,7 @@ public class NowPlayingFragment extends Fragment
public void onStopClicked(View v) {
Player.Stop action = new Player.Stop(currentActivePlayerId);
action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, 0);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, false);
}
@OnClick(R.id.fast_forward)
@ -299,17 +303,8 @@ public class NowPlayingFragment extends Fragment
action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler);
}
/**
* Calllbacks for media button toolbar
*/
@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();
UIUtils.highlightImageView(getActivity(), volumeMuteButton, !hostState.isVolumeMuted());
Application.SetMute action = new Application.SetMute();
action.execute(hostManager.getConnection(), defaultBooleanActionCallback, new Handler());
}
@ -323,7 +318,6 @@ public class NowPlayingFragment extends Fragment
if (!isAdded()) return;
// Force a refresh
hostConnectionObserver.forceRefreshResults();
nowPlayingListener.onShuffleClicked();
}
@Override
@ -539,20 +533,12 @@ public class NowPlayingFragment extends Fragment
}
@Override
public void onProgressChanged(int progress) {
PlayerType.PositionTime positionTime = new PlayerType.PositionTime(progress);
Player.Seek seekAction = new Player.Seek(currentActivePlayerId, positionTime);
seekAction.execute(hostManager.getConnection(), new ApiCallback<PlayerType.SeekReturnType>() {
@Override
public void onSuccess(PlayerType.SeekReturnType result) {
// Ignore
}
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
if (notificationsData.property.shuffled != null)
shuffleButton.setHighlight(notificationsData.property.shuffled);
@Override
public void onError(int errorCode, String description) {
LogUtils.LOGD(TAG, "Got an error calling Player.Seek. Error code: " + errorCode + ", description: " + description);
}
}, callbackHandler);
if (notificationsData.property.repeatMode != null)
UIUtils.setRepeatButton(repeatButton, notificationsData.property.repeatMode);
}
/**
@ -561,19 +547,19 @@ public class NowPlayingFragment extends Fragment
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
setNowPlayingInfo(getPropertiesResult, getItemResult);
setNowPlayingInfo(getActivePlayerResult, getPropertiesResult, getItemResult);
currentActivePlayerId = getActivePlayerResult.playerid;
// Switch icon
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed == 1);
}
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
setNowPlayingInfo(getPropertiesResult, getItemResult);
setNowPlayingInfo(getActivePlayerResult, getPropertiesResult, getItemResult);
currentActivePlayerId = getActivePlayerResult.playerid;
// Switch icon
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed == 1);
}
public void playerOnStop() {
@ -619,18 +605,38 @@ public class NowPlayingFragment extends Fragment
@Override
public void applicationOnVolumeChanged(int volume, boolean muted) {
volumeLevelIndicator.setVolume(muted, volume);
UIUtils.highlightImageView(getActivity(), volumeMuteButton, muted);
volumeMuteButton.setHighlight(muted);
}
// Ignore this
public void inputOnInputRequested(String title, String type, String value) {}
public void observerOnStopObserving() {}
@Override
public void onProgressChanged(int progress) {
PlayerType.PositionTime positionTime = new PlayerType.PositionTime(progress);
Player.Seek seekAction = new Player.Seek(currentActivePlayerId, positionTime);
seekAction.execute(HostManager.getInstance(getContext()).getConnection(), new ApiCallback<PlayerType.SeekReturnType>() {
@Override
public void onSuccess(PlayerType.SeekReturnType result) {
// Ignore
}
@Override
public void onError(int errorCode, String description) {
LogUtils.LOGD("MediaSeekBar", "Got an error calling Player.Seek. Error code: " + errorCode + ", description: " + description);
}
}, new Handler());
}
/**
* Sets whats playing information
* @param getItemResult Return from method {@link org.xbmc.kore.jsonrpc.method.Player.GetItem}
*/
private void setNowPlayingInfo(PlayerType.PropertyValue getPropertiesResult,
private void setNowPlayingInfo(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
final ListType.ItemsAll getItemResult) {
final String title, underTitle, art, poster, genreSeason, year,
descriptionPlot, votes, maxRating;
@ -776,7 +782,6 @@ public class NowPlayingFragment extends Fragment
}
UIUtils.setRepeatButton(repeatButton, getPropertiesResult.repeat);
shuffleButton.setHighlight(getPropertiesResult.shuffled);
Resources resources = getActivity().getResources();

View File

@ -194,6 +194,12 @@ public class PlaylistFragment extends Fragment
private PlayerType.PropertyValue lastGetPropertiesResult;
private List<ListType.ItemsAll> lastGetPlaylistItemsResult = null;
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
if (notificationsData.property.shuffled != null)
setupPlaylistInfo(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult);
}
/**
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/

View File

@ -610,6 +610,12 @@ public class RemoteActivity extends BaseActivity
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/
private String lastImageUrl = null;
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
}
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
@ -690,11 +696,6 @@ public class RemoteActivity extends BaseActivity
viewPager.setCurrentItem(1);
}
@Override
public void onShuffleClicked() {
refreshPlaylist();
}
private void refreshPlaylist() {
String tag = "android:switcher:" + viewPager.getId() + ":" + PLAYLIST_FRAGMENT_ID;
PlaylistFragment playlistFragment = (PlaylistFragment)getSupportFragmentManager()

View File

@ -552,13 +552,18 @@ public class RemoteFragment extends Fragment
@Override
public void onSuccess(Integer result) {
if (!isAdded()) return;
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, result);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, result == 1);
}
@Override
public void onError(int errorCode, String description) { }
};
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
}
/**
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/
@ -569,7 +574,7 @@ public class RemoteFragment extends Fragment
currentActivePlayerId = getActivePlayerResult.playerid;
currentNowPlayingItemType = getItemResult.type;
// Switch icon
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed == 1);
}
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
@ -579,7 +584,7 @@ public class RemoteFragment extends Fragment
currentActivePlayerId = getActivePlayerResult.playerid;
currentNowPlayingItemType = getItemResult.type;
// Switch icon
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed);
UIUtils.setPlayPauseButtonIcon(getActivity(), playButton, getPropertiesResult.speed == 1);
}
public void playerOnStop() {

View File

@ -18,6 +18,8 @@ package org.xbmc.kore.ui.widgets;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
@ -25,6 +27,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
public class MediaProgressIndicator extends LinearLayout {
@ -94,6 +97,24 @@ public class MediaProgressIndicator extends LinearLayout {
});
}
@Override
protected Parcelable onSaveInstanceState() {
SavedState savedState = new SavedState(super.onSaveInstanceState());
savedState.progress = progress;
savedState.maxProgress = maxProgress;
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
progress = savedState.progress;
maxProgress = savedState.maxProgress;
setProgress(progress);
setMaxProgress(maxProgress);
}
private Runnable seekBarUpdater = new Runnable() {
@Override
public void run() {
@ -119,6 +140,10 @@ public class MediaProgressIndicator extends LinearLayout {
progressTextView.setText(UIUtils.formatTime(progress));
}
public int getProgress() {
return progress;
}
public void setMaxProgress(int max) {
maxProgress = max;
seekBar.setMax(max);
@ -140,4 +165,37 @@ public class MediaProgressIndicator extends LinearLayout {
if (speed > 0)
seekBar.postDelayed(seekBarUpdater, SEEK_BAR_UPDATE_INTERVAL);
}
private static class SavedState extends BaseSavedState {
int progress;
int maxProgress;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
progress = in.readInt();
maxProgress = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(progress);
out.writeInt(maxProgress);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

View File

@ -0,0 +1,271 @@
/*
* Copyright 2017 Martijn Brekhof. 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.ui.widgets;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.type.GlobalType;
import org.xbmc.kore.utils.UIUtils;
public class NowPlayingPanel extends SlidingUpPanelLayout {
public interface OnPanelButtonsClickListener {
void onPlayClicked();
void onPreviousClicked();
void onNextClicked();
void onVolumeMuteClicked();
void onShuffleClicked();
void onRepeatClicked();
void onVolumeMutedIndicatorClicked();
}
private OnPanelButtonsClickListener onPanelButtonsClickListener;
private TextView title;
private TextView details;
private ImageView poster;
private ImageButton previousButton;
private ImageButton nextButton;
private ImageButton playButton;
private MediaProgressIndicator mediaProgressIndicator;