From cb430aa20d13c56a04a800275bd92161ce41cbf8 Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Thu, 13 Jul 2017 20:10:49 +0200 Subject: [PATCH] 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. --- README.md | 1 + app/build.gradle | 1 + app/src/main/java/org/xbmc/kore/Settings.java | 4 + .../kore/host/HostConnectionObserver.java | 28 +- .../org/xbmc/kore/jsonrpc/HostConnection.java | 16 +- .../kore/jsonrpc/notification/Player.java | 51 ++- .../ConnectionObserversManagerService.java | 6 + .../kore/service/NotificationObserver.java | 8 +- .../xbmc/kore/service/PauseCallObserver.java | 5 + .../org/xbmc/kore/ui/BaseMediaActivity.java | 358 +++++++++++++++++- .../sections/remote/NowPlayingFragment.java | 89 +++-- .../ui/sections/remote/PlaylistFragment.java | 6 + .../ui/sections/remote/RemoteActivity.java | 11 +- .../ui/sections/remote/RemoteFragment.java | 11 +- .../ui/widgets/MediaProgressIndicator.java | 58 +++ .../xbmc/kore/ui/widgets/NowPlayingPanel.java | 271 +++++++++++++ .../java/org/xbmc/kore/utils/LogUtils.java | 4 +- .../java/org/xbmc/kore/utils/UIUtils.java | 29 +- .../res/layout/activity_generic_media.xml | 13 +- .../main/res/layout/fragment_now_playing.xml | 4 +- app/src/main/res/layout/now_playing_panel.xml | 154 ++++++++ app/src/main/res/values/dimens.xml | 3 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 6 + 24 files changed, 1054 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/widgets/NowPlayingPanel.java create mode 100644 app/src/main/res/layout/now_playing_panel.xml diff --git a/README.md b/README.md index 08f86cb..a70913f 100644 --- a/README.md +++ b/README.md @@ -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 ----- diff --git a/app/build.gradle b/app/build.gradle index e143bc0..a090730 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/xbmc/kore/Settings.java b/app/src/main/java/org/xbmc/kore/Settings.java index 5285b48..6f9628b 100644 --- a/app/src/main/java/org/xbmc/kore/Settings.java +++ b/app/src/main/java/org/xbmc/kore/Settings.java @@ -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; 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 74b813a..1646ab8 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java +++ b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java @@ -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() { @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 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) { 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 4a47a35..0d86fe5 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java @@ -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 : diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Player.java b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Player.java index a72eaf1..4a8e8ce 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Player.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Player.java @@ -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; } } diff --git a/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java b/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java index bc07c64..72a5d70 100644 --- a/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java +++ b/app/src/main/java/org/xbmc/kore/service/ConnectionObserversManagerService.java @@ -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 */ diff --git a/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java b/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java index 3f64887..34612be 100644 --- a/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java +++ b/app/src/main/java/org/xbmc/kore/service/NotificationObserver.java @@ -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 */ diff --git a/app/src/main/java/org/xbmc/kore/service/PauseCallObserver.java b/app/src/main/java/org/xbmc/kore/service/PauseCallObserver.java index 12a4c93..c50d871 100644 --- a/app/src/main/java/org/xbmc/kore/service/PauseCallObserver.java +++ b/app/src/main/java/org/xbmc/kore/service/PauseCallObserver.java @@ -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, diff --git a/app/src/main/java/org/xbmc/kore/ui/BaseMediaActivity.java b/app/src/main/java/org/xbmc/kore/ui/BaseMediaActivity.java index 290423b..8cb5467 100644 --- a/app/src/main/java/org/xbmc/kore/ui/BaseMediaActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/BaseMediaActivity.java @@ -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 defaultStringActionCallback = ApiMethod.getDefaultActionCallback(); + private Handler callbackHandler = new Handler(); + private ApiCallback 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() { + @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() { + @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() { + @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() { + @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() { + @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); + } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java index 03ff630..ab7ae28 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/NowPlayingFragment.java @@ -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 defaultIntActionCallback = ApiMethod.getDefaultActionCallback(); + private ApiCallback 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 defaultStringActionCallback = ApiMethod.getDefaultActionCallback(); - private ApiCallback defaultIntActionCallback = ApiMethod.getDefaultActionCallback(); - private ApiCallback 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() { - @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() { + @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(); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/PlaylistFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/PlaylistFragment.java index 6556679..bc10ec2 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/PlaylistFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/PlaylistFragment.java @@ -194,6 +194,12 @@ public class PlaylistFragment extends Fragment private PlayerType.PropertyValue lastGetPropertiesResult; private List 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 */ diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java index c1ab574..df4b582 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java @@ -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() diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java index eaca364..a5f6c1d 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java @@ -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() { diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/MediaProgressIndicator.java b/app/src/main/java/org/xbmc/kore/ui/widgets/MediaProgressIndicator.java index 0cc5dbc..df66a41 100644 --- a/app/src/main/java/org/xbmc/kore/ui/widgets/MediaProgressIndicator.java +++ b/app/src/main/java/org/xbmc/kore/ui/widgets/MediaProgressIndicator.java @@ -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 CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } } \ No newline at end of file diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/NowPlayingPanel.java b/app/src/main/java/org/xbmc/kore/ui/widgets/NowPlayingPanel.java new file mode 100644 index 0000000..794d040 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/widgets/NowPlayingPanel.java @@ -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; + private VolumeLevelIndicator volumeLevelIndicator; + private HighlightButton volumeMuteButton; + private HighlightButton volumeMutedIndicatorButton; + private RepeatModeButton repeatModeButton; + private HighlightButton shuffleButton; + + public NowPlayingPanel(Context context) { + super(context); + initializeView(context); + } + public NowPlayingPanel(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + initializeView(context); + } + + public NowPlayingPanel(Context context, AttributeSet attributeSet, int defStyle) { + super(context, attributeSet, defStyle); + initializeView(context); + } + + private void initializeView(Context context) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.now_playing_panel, this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + title = (TextView) findViewById(R.id.npp_title); + details = (TextView) findViewById(R.id.npp_details); + poster = (ImageView) findViewById(R.id.npp_poster); + previousButton = (ImageButton) findViewById(R.id.npp_previous); + nextButton = (ImageButton) findViewById(R.id.npp_next); + playButton = (ImageButton) findViewById(R.id.npp_play); + mediaProgressIndicator = (MediaProgressIndicator) findViewById(R.id.npp_progress_indicator); + volumeLevelIndicator = (VolumeLevelIndicator) findViewById(R.id.npp_volume_level_indicator); + volumeMuteButton = (HighlightButton) findViewById(R.id.npp_volume_mute); + repeatModeButton = (RepeatModeButton) findViewById(R.id.npp_repeat); + shuffleButton = (HighlightButton) findViewById(R.id.npp_shuffle); + volumeMutedIndicatorButton = (HighlightButton) findViewById(R.id.npp_volume_muted_indicator); + + setupButtonClickListeners(); + } + + public void setOnPanelButtonsClickListener(OnPanelButtonsClickListener listener) { + onPanelButtonsClickListener = listener; + } + + public void setOnVolumeChangeListener(VolumeLevelIndicator.OnVolumeChangeListener listener) { + volumeLevelIndicator.setOnVolumeChangeListener(listener); + } + + public void setOnProgressChangeListener(MediaProgressIndicator.OnProgressChangeListener listener) { + mediaProgressIndicator.setOnProgressChangeListener(listener); + } + + public void setVolume(int volume, boolean muted) { + volumeLevelIndicator.setVolume(muted, volume); + + if (muted) { + volumeMutedIndicatorButton.setVisibility(View.VISIBLE); + } else { + volumeMutedIndicatorButton.setVisibility(View.GONE); + } + + volumeMutedIndicatorButton.setHighlight(muted); + volumeMuteButton.setHighlight(muted); + } + + public void setRepeatMode(String repeatMode) { + UIUtils.setRepeatButton(repeatModeButton, repeatMode); + } + + public void setShuffled(boolean shuffled) { + shuffleButton.setHighlight(shuffled); + } + + /** + * Sets the state of the play button + * @param play true if playing, false if paused + */ + public void setPlayButton(boolean play) { + UIUtils.setPlayPauseButtonIcon(getContext(), playButton, play); + } + + public void setMediaProgress(GlobalType.Time time, GlobalType.Time totalTime) { + mediaProgressIndicator.setMaxProgress(totalTime.ToSeconds()); + mediaProgressIndicator.setProgress(time.ToSeconds()); + } + + /** + * Returns the progression indicator used for media progression + * @return + */ + public MediaProgressIndicator getMediaProgress() { + return mediaProgressIndicator; + } + + /** + * + * @param speed + */ + public void setSpeed(int speed) { + mediaProgressIndicator.setSpeed(speed); + } + + public CharSequence getTitle() { + return title.getText(); + } + + public void setTitle(String title) { + this.title.setText(title); + } + + public void setDetails(String details) { + this.details.setText(details); + } + + public void setNextPrevVisibility(int visibility) { + nextButton.setVisibility(visibility); + previousButton.setVisibility(visibility); + } + + public void setSquarePoster(boolean square) { + if (square) { + ViewGroup.LayoutParams layoutParams = poster.getLayoutParams(); + layoutParams.width = layoutParams.height; + poster.setLayoutParams(layoutParams); + } + } + + public ImageView getPoster() { + return poster; + } + + private void setupButtonClickListeners() { + playButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + previousButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + nextButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + volumeMuteButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + shuffleButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + repeatModeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + + volumeMutedIndicatorButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleButtonClickEvent(v); + } + }); + } + + private void handleButtonClickEvent(View view) { + if (onPanelButtonsClickListener == null) + return; + + switch (view.getId()) { + case R.id.npp_previous: + onPanelButtonsClickListener.onPreviousClicked(); + break; + case R.id.npp_next: + onPanelButtonsClickListener.onNextClicked(); + break; + case R.id.npp_play: + onPanelButtonsClickListener.onPlayClicked(); + break; + case R.id.npp_volume_mute: + onPanelButtonsClickListener.onVolumeMuteClicked(); + break; + case R.id.npp_repeat: + onPanelButtonsClickListener.onRepeatClicked(); + break; + case R.id.npp_shuffle: + onPanelButtonsClickListener.onShuffleClicked(); + break; + case R.id.npp_volume_muted_indicator: + onPanelButtonsClickListener.onVolumeMutedIndicatorClicked(); + break; + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/utils/LogUtils.java b/app/src/main/java/org/xbmc/kore/utils/LogUtils.java index b7876e9..43df518 100644 --- a/app/src/main/java/org/xbmc/kore/utils/LogUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/LogUtils.java @@ -33,8 +33,8 @@ public class LogUtils { // TODO: Remove this later private static final List doNotLogTags = Arrays.asList( - HostConnection.TAG, - HostConnectionObserver.TAG +// HostConnection.TAG, +// HostConnectionObserver.TAG ); public static String makeLogTag(String str) { diff --git a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java index 4ed30d9..0d83026 100644 --- a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java @@ -93,6 +93,21 @@ public class UIUtils { } } + /** + * Converts the time format from {@link #formatTime(int, int, int)} to seconds + * @param time + * @return + */ + public static int timeToSeconds(String time) { + String[] items = time.split(":"); + if (items.length > 2) { + return (Integer.parseInt(items[0]) * 3600) + (Integer.parseInt(items[1]) * 60) + + (Integer.parseInt(items[2])); + } else { + return (Integer.parseInt(items[0]) * 60) + (Integer.parseInt(items[1])); + } + } + /** * Formats a file size, ISO prefixes */ @@ -198,9 +213,9 @@ public class UIUtils { char charAvatar = TextUtils.isEmpty(str) ? ' ' : str.charAt(0); int avatarColorsIdx = TextUtils.isEmpty(str) ? 0 : - Math.max(Character.getNumericValue(str.charAt(0)) + - Character.getNumericValue(str.charAt(str.length() - 1)) + - str.length(), 0) % characterAvatarColors.length(); + Math.max(Character.getNumericValue(str.charAt(0)) + + Character.getNumericValue(str.charAt(str.length() - 1)) + + str.length(), 0) % characterAvatarColors.length(); int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000); // avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length()); return new CharacterDrawable(charAvatar, color); @@ -210,12 +225,12 @@ public class UIUtils { static int iconPauseResId = R.drawable.ic_pause_white_24dp, iconPlayResId = R.drawable.ic_play_arrow_white_24dp; /** - * Sets play/pause button icon on a ImageView, based on speed + * Sets play/pause button icon on a ImageView * @param context Activity * @param view ImageView/ImageButton - * @param speed Current player speed + * @param play true if playing, false if paused */ - public static void setPlayPauseButtonIcon(Context context, ImageView view, int speed) { + public static void setPlayPauseButtonIcon(Context context, ImageView view, boolean play) { if (!playPauseIconsLoaded) { TypedArray styledAttributes = context.obtainStyledAttributes(new int[]{R.attr.iconPause, R.attr.iconPlay}); @@ -225,7 +240,7 @@ public class UIUtils { playPauseIconsLoaded = true; } - view.setImageResource((speed == 1) ? iconPauseResId: iconPlayResId); + view.setImageResource(play ? iconPauseResId : iconPlayResId ); } /** diff --git a/app/src/main/res/layout/activity_generic_media.xml b/app/src/main/res/layout/activity_generic_media.xml index fc14e6d..81cfbd4 100644 --- a/app/src/main/res/layout/activity_generic_media.xml +++ b/app/src/main/res/layout/activity_generic_media.xml @@ -28,10 +28,17 @@ - + android:layout_height="match_parent" + android:gravity="bottom" + sothree:umanoPanelHeight="@dimen/notification_art_slim_height" + sothree:umanoShadowHeight="4dp" + sothree:umanoFadeColor="#00000000" + sothree:umanoInitialState="hidden"/> + + android:orientation="vertical" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8baa248..9286e08 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -128,7 +128,8 @@ 128dp 64dp 64dp - 44dp + 31dp + 44dp 128dp 128dp 88dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee0b869..800f68d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,6 +346,8 @@ Show over lockscreen Stay awake Show notification while playing + Show now playing panel while playing + Displays an expandable panel at the bottom of the screen when media is playing or paused Pause during phone call Use device volume keys Vibrate on touch diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 75977af..7a04942 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -64,6 +64,12 @@ android:title="@string/show_notification" android:defaultValue="false"/> + +