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 194de80..9c67d30 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java +++ b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java @@ -18,6 +18,7 @@ package org.xbmc.kore.host; import android.os.Handler; import android.os.Looper; +import org.xbmc.kore.host.actions.GetPlaylist; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.HostConnection; import org.xbmc.kore.jsonrpc.method.JSONRPC; @@ -25,6 +26,7 @@ import org.xbmc.kore.jsonrpc.method.Player; import org.xbmc.kore.jsonrpc.notification.Application; import org.xbmc.kore.jsonrpc.notification.Input; import org.xbmc.kore.jsonrpc.notification.Player.NotificationsData; +import org.xbmc.kore.jsonrpc.notification.Playlist; import org.xbmc.kore.jsonrpc.notification.System; import org.xbmc.kore.jsonrpc.type.ApplicationType; import org.xbmc.kore.jsonrpc.type.ListType; @@ -49,9 +51,26 @@ public class HostConnectionObserver implements HostConnection.PlayerNotificationsObserver, HostConnection.SystemNotificationsObserver, HostConnection.InputNotificationsObserver, - HostConnection.ApplicationNotificationsObserver { + HostConnection.ApplicationNotificationsObserver, + HostConnection.PlaylistNotificationsObserver { public static final String TAG = LogUtils.makeLogTag(HostConnectionObserver.class); + public interface PlaylistEventsObserver { + /** + * @param playlistId of playlist that has been cleared + */ + void playlistOnClear(int playlistId); + + void playlistChanged(int playlistId); + + /** + * @param playlists the available playlists on the server + */ + void playlistsAvailable(ArrayList playlists); + + void playlistOnError(int errorCode, String description); + } + /** * Interface that an observer has to implement to receive playlist events */ @@ -87,8 +106,8 @@ public class HostConnectionObserver * @param getItemResult Currently playing item, obtained by a call to {@link org.xbmc.kore.jsonrpc.method.Player.GetItem} */ void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult, - PlayerType.PropertyValue getPropertiesResult, - ListType.ItemsAll getItemResult); + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); /** * Notifies that something is paused @@ -97,8 +116,8 @@ public class HostConnectionObserver * @param getItemResult Currently paused item, obtained by a call to {@link org.xbmc.kore.jsonrpc.method.Player.GetItem} */ void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, - PlayerType.PropertyValue getPropertiesResult, - ListType.ItemsAll getItemResult); + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); /** * Notifies that media is stopped/nothing is playing @@ -143,15 +162,19 @@ public class HostConnectionObserver */ private List playerEventsObservers = new ArrayList<>(); private List applicationEventsObservers = new ArrayList<>(); + private List playlistEventsObservers = new ArrayList<>(); // Associate the Handler with the UI thread + int checkPlaylistCounter = 0; private Handler checkerHandler = new Handler(Looper.getMainLooper()); private Runnable httpCheckerRunnable = new Runnable() { @Override public void run() { final int HTTP_NOTIFICATION_CHECK_INTERVAL = 2000; // If no one is listening to this, just exit - if (playerEventsObservers.isEmpty() && applicationEventsObservers.isEmpty()) + if (playerEventsObservers.isEmpty() + && applicationEventsObservers.isEmpty() + && playlistEventsObservers.isEmpty()) return; if (!playerEventsObservers.isEmpty()) @@ -160,6 +183,12 @@ public class HostConnectionObserver if (!applicationEventsObservers.isEmpty()) getApplicationProperties(); + if (!playlistEventsObservers.isEmpty() && checkPlaylistCounter > 1) { + checkPlaylist(); + checkPlaylistCounter = 0; + } + checkPlaylistCounter++; + checkerHandler.postDelayed(this, HTTP_NOTIFICATION_CHECK_INTERVAL); } }; @@ -168,7 +197,8 @@ public class HostConnectionObserver @Override public void run() { // If no one is listening to this, just exit - if (playerEventsObservers.isEmpty() && applicationEventsObservers.isEmpty()) + if (playerEventsObservers.isEmpty() && applicationEventsObservers.isEmpty() && + playlistEventsObservers.isEmpty()) return; final int PING_AFTER_ERROR_CHECK_INTERVAL = 2000, @@ -181,11 +211,16 @@ public class HostConnectionObserver // we were in a error or uninitialized state, update if ((!playerEventsObservers.isEmpty()) && ((hostState.lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || - (hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR))) { + (hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR))) { LogUtils.LOGD(TAG, "Checking what's playing because we don't have info about it"); checkWhatsPlaying(); } + if ((!playlistEventsObservers.isEmpty()) && + (hostState.lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { + checkPlaylist(); + } + checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); } @@ -323,16 +358,44 @@ public class HostConnectionObserver } } - private void startCheckerHandler() { - // Check if checkerHandler is already running, to prevent multiple runnables to be posted - // when multiple observers are registered. - if (checkerHandler.hasMessages(0)) + /** + * Registers a new observer that will be notified about playlist events + * @param observer Observer + * @param replyImmediately Whether to immediately issue a request if there are playlists available + */ + public void registerPlaylistObserver(PlaylistEventsObserver observer, boolean replyImmediately) { + if (this.connection == null) return; - if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { - checkerHandler.post(tcpCheckerRunnable); - } else { - checkerHandler.post(httpCheckerRunnable); + if ( ! playlistEventsObservers.contains(observer) ) { + playlistEventsObservers.add(observer); + } + + if (playlistEventsObservers.size() == 1) { + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.registerPlaylistNotificationsObserver(this, checkerHandler); + } + + startCheckerHandler(); + } + + if (replyImmediately) + checkPlaylist(); + } + + public void unregisterPlaylistObserver(PlayerEventsObserver observer) { + playlistEventsObservers.remove(observer); + + LogUtils.LOGD(TAG, "Unregistering playlist observer " + observer.getClass().getSimpleName() + + ". Still got " + playlistEventsObservers.size() + + " observers."); + + if (playlistEventsObservers.isEmpty()) { + // No more observers, so unregister us from the host connection, or stop + // the http checker thread + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.unregisterPlaylistNotificationsObserver(this); + } } } @@ -350,6 +413,7 @@ public class HostConnectionObserver connection.unregisterSystemNotificationsObserver(this); connection.unregisterInputNotificationsObserver(this); connection.unregisterApplicationNotificationsObserver(this); + connection.unregisterPlaylistNotificationsObserver(this); checkerHandler.removeCallbacks(tcpCheckerRunnable); } hostState.lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; @@ -370,7 +434,9 @@ public class HostConnectionObserver public void onPlay(org.xbmc.kore.jsonrpc.notification.Player.OnPlay notification) { // Ignore this if Kodi is Leia or higher, as we'll be properly notified via OnAVStart // See https://github.com/xbmc/Kore/issues/602 and https://github.com/xbmc/xbmc/pull/13726 - if (connection.getHostInfo().isLeiaOrLater()) { + // Note: OnPlay is still required for picture items. + if (connection.getHostInfo().isLeiaOrLater() && + ! notification.data.item.type.contentEquals("picture") ) { LogUtils.LOGD(TAG, "OnPlay notification ignored. Will wait for OnAVStart."); return; } @@ -454,6 +520,40 @@ public class HostConnectionObserver } } + @Override + public void onPlaylistCleared(Playlist.OnClear notification) { + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistOnClear(notification.playlistId); + } + } + + @Override + public void onPlaylistItemAdded(Playlist.OnAdd notification) { + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistChanged(notification.playlistId); + } + } + + @Override + public void onPlaylistItemRemoved(Playlist.OnRemove notification) { + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistChanged(notification.playlistId); + } + } + + private void startCheckerHandler() { + // Check if checkerHandler is already running, to prevent multiple runnables to be posted + // when multiple observers are registered. + if (checkerHandler.hasMessages(0)) + return; + + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + checkerHandler.post(tcpCheckerRunnable); + } else { + checkerHandler.post(httpCheckerRunnable); + } + } + private void getApplicationProperties() { org.xbmc.kore.jsonrpc.method.Application.GetProperties getProperties = new org.xbmc.kore.jsonrpc.method.Application.GetProperties(org.xbmc.kore.jsonrpc.method.Application.GetProperties.VOLUME, @@ -477,6 +577,62 @@ public class HostConnectionObserver }, checkerHandler); } + private ArrayList prevGetPlaylistResults = new ArrayList<>(); + private boolean isCheckingPlaylist = false; + private void checkPlaylist() { + if (isCheckingPlaylist) + return; + + isCheckingPlaylist = true; + + connection.execute(new GetPlaylist(connection), new ApiCallback>() { + @Override + public void onSuccess(ArrayList result) { + isCheckingPlaylist = false; + + if (result.isEmpty()) { + callPlaylistsOnClear(prevGetPlaylistResults); + return; + } + + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistsAvailable(result); + } + + // Handle onClear for HTTP only connections + for (GetPlaylist.GetPlaylistResult getPlaylistResult : result) { + for (int i = 0; i < prevGetPlaylistResults.size(); i++) { + if (getPlaylistResult.id == prevGetPlaylistResults.get(i).id) { + prevGetPlaylistResults.remove(i); + break; + } + } + } + + callPlaylistsOnClear(prevGetPlaylistResults); + + prevGetPlaylistResults = result; + } + + @Override + public void onError(int errorCode, String description) { + isCheckingPlaylist = false; + + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistOnError(errorCode, description); + } + } + }, new Handler()); + } + + private void callPlaylistsOnClear(ArrayList clearedPlaylists) { + for (GetPlaylist.GetPlaylistResult getPlaylistResult : clearedPlaylists) { + for (PlaylistEventsObserver observer : playlistEventsObservers) { + observer.playlistOnClear(getPlaylistResult.id); + } + } + } + /** * Indicator set when we are calling Kodi to check what's playing, so that we don't call it * while there are still pending calls @@ -544,7 +700,7 @@ public class HostConnectionObserver PlayerType.PropertyName.AUDIOSTREAMS, PlayerType.PropertyName.SUBTITLES, PlayerType.PropertyName.PLAYLISTID, - }; + }; Player.GetProperties getProperties = new Player.GetProperties(getActivePlayersResult.playerid, propertiesToGet); getProperties.execute(connection, new ApiCallback() { @@ -606,7 +762,7 @@ public class HostConnectionObserver ListType.FieldsAll.WRITER, ListType.FieldsAll.YEAR, ListType.FieldsAll.DESCRIPTION, - }; + }; // propertiesToGet = ListType.FieldsAll.allValues; Player.GetItem getItem = new Player.GetItem(getActivePlayersResult.playerid, propertiesToGet); getItem.execute(connection, new ApiCallback() { @@ -693,15 +849,15 @@ public class HostConnectionObserver private boolean getPropertiesResultChanged(PlayerType.PropertyValue getPropertiesResult) { return (hostState.lastGetPropertiesResult == null) || - (hostState.lastGetPropertiesResult.speed != getPropertiesResult.speed) || - (hostState.lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || - (!hostState.lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)); + (hostState.lastGetPropertiesResult.speed != getPropertiesResult.speed) || + (hostState.lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || + (!hostState.lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)); } private boolean getItemResultChanged(ListType.ItemsAll getItemResult) { return (hostState.lastGetItemResult == null) || - (hostState.lastGetItemResult.id != getItemResult.id) || - (!hostState.lastGetItemResult.label.equals(getItemResult.label)); + (hostState.lastGetItemResult.id != getItemResult.id) || + (!hostState.lastGetItemResult.label.equals(getItemResult.label)); } /** @@ -718,11 +874,11 @@ public class HostConnectionObserver List observers) { checkingWhatsPlaying = false; int currentCallResult = (getPropertiesResult.speed == 0) ? - PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; + PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; if (forceReply || - (hostState.lastCallResult != currentCallResult) || - getPropertiesResultChanged(getPropertiesResult) || - getItemResultChanged(getItemResult)) { + (hostState.lastCallResult != currentCallResult) || + getPropertiesResultChanged(getPropertiesResult) || + getItemResultChanged(getItemResult)) { hostState.lastCallResult = currentCallResult; hostState.lastGetActivePlayerResult = getActivePlayersResult; hostState.lastGetPropertiesResult = getPropertiesResult; diff --git a/app/src/main/java/org/xbmc/kore/host/actions/GetPlaylist.java b/app/src/main/java/org/xbmc/kore/host/actions/GetPlaylist.java new file mode 100644 index 0000000..d77be48 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/host/actions/GetPlaylist.java @@ -0,0 +1,174 @@ +/* + * Copyright 2018 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.host.actions; + + +import org.xbmc.kore.jsonrpc.ApiMethod; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.Playlist; +import org.xbmc.kore.jsonrpc.type.ListType; +import org.xbmc.kore.jsonrpc.type.PlaylistType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +/** + * Retrieves the playlist items for the first non-empty playlist or null if no playlists are + * available. + */ +public class GetPlaylist implements Callable> { + + private final static String[] propertiesToGet = new String[] { + ListType.FieldsAll.ART, + ListType.FieldsAll.ARTIST, + ListType.FieldsAll.ALBUMARTIST, + ListType.FieldsAll.ALBUM, + ListType.FieldsAll.DISPLAYARTIST, + ListType.FieldsAll.EPISODE, + ListType.FieldsAll.FANART, + ListType.FieldsAll.FILE, + ListType.FieldsAll.SEASON, + ListType.FieldsAll.SHOWTITLE, + ListType.FieldsAll.STUDIO, + ListType.FieldsAll.TAGLINE, + ListType.FieldsAll.THUMBNAIL, + ListType.FieldsAll.TITLE, + ListType.FieldsAll.TRACK, + ListType.FieldsAll.DURATION, + ListType.FieldsAll.RUNTIME, + }; + + static private HashMap playlistsTypesAndIds; + private String playlistType; + private int playlistId = -1; + private HostConnection hostConnection; + + /** + * Use this to get the first non-empty playlist + * @param hostConnection + */ + public GetPlaylist(HostConnection hostConnection) { + this.hostConnection = hostConnection; + } + + /** + * Use this to get a playlist for a specific playlist type + * @param hostConnection + * @param playlistType should be one of the types from {@link org.xbmc.kore.jsonrpc.type.PlaylistType.GetPlaylistsReturnType}. + * If null the first non-empty playlist is returned. + */ + public GetPlaylist(HostConnection hostConnection, String playlistType) { + this.hostConnection = hostConnection; + this.playlistType = playlistType; + } + + /** + * Use this to get a playlist for a specific playlist id + * @param hostConnection + * @param playlistId + */ + public GetPlaylist(HostConnection hostConnection, int playlistId) { + this.hostConnection = hostConnection; + this.playlistId = playlistId; + } + + @Override + public ArrayList call() throws ExecutionException, InterruptedException { + if (playlistsTypesAndIds == null) + playlistsTypesAndIds = getPlaylists(hostConnection); + + if (playlistType != null) { + GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForType(playlistType); + ArrayList playlists = new ArrayList<>(); + playlists.add(getPlaylistResult); + return playlists; + } else if (playlistId > -1 ) { + GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForId(playlistId); + ArrayList playlists = new ArrayList<>(); + playlists.add(getPlaylistResult); + return playlists; + } else + return retrieveNonEmptyPlaylists(); + } + + private GetPlaylistResult retrievePlaylistItemsForId(int playlistId) throws InterruptedException, + ExecutionException { + List playlistItems = retrievePlaylistItems(hostConnection, playlistId); + return new GetPlaylistResult(playlistId, getPlaylistType(playlistId), playlistItems); + } + + private GetPlaylistResult retrievePlaylistItemsForType(String type) throws InterruptedException, + ExecutionException { + List playlistItems = retrievePlaylistItems(hostConnection, playlistsTypesAndIds.get(type)); + return new GetPlaylistResult(playlistsTypesAndIds.get(type), type, playlistItems); + } + + private ArrayList retrieveNonEmptyPlaylists() throws InterruptedException, + ExecutionException { + ArrayList playlists = new ArrayList<>(); + + for (String type : playlistsTypesAndIds.keySet()) { + List playlistItems = retrievePlaylistItems(hostConnection, + playlistsTypesAndIds.get(type)); + if (!playlistItems.isEmpty()) + playlists.add(new GetPlaylistResult(playlistsTypesAndIds.get(type), type, playlistItems)); + } + return playlists; + } + + private HashMap getPlaylists(HostConnection hostConnection) + throws ExecutionException, InterruptedException { + HashMap playlistsHashMap = new HashMap<>(); + ArrayList playlistsReturnTypes = hostConnection.execute(new Playlist.GetPlaylists()).get(); + for (PlaylistType.GetPlaylistsReturnType type : playlistsReturnTypes) { + playlistsHashMap.put(type.type, type.playlistid); + } + return playlistsHashMap; + } + + private List retrievePlaylistItems(HostConnection hostConnection, + int playlistId) + throws InterruptedException, ExecutionException { + + ApiMethod> apiMethod = new Playlist.GetItems(playlistId, + propertiesToGet); + return hostConnection.execute(apiMethod).get(); + } + + private String getPlaylistType(int playlistId) { + for (String key : playlistsTypesAndIds.keySet()) { + if (playlistsTypesAndIds.get(key) == playlistId) + return key; + } + return null; + } + + public static class GetPlaylistResult { + final public String type; + final public int id; + final public List items; + + private GetPlaylistResult(int playlistId, String type, List items) { + this.id = playlistId; + this.type = type; + this.items = items; + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java index b38d98b..31e2cac 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java @@ -75,6 +75,10 @@ public class ApiException extends Exception { */ public static int API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING = 102; + public static int API_WAITING_ON_RESULT_TIMEDOUT = 103; + + public static int API_WAITING_ON_RESULT_INTERRUPTED = 104; + private int code; /** 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 44afb1f..afff28c 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java @@ -35,6 +35,7 @@ import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.jsonrpc.notification.Application; import org.xbmc.kore.jsonrpc.notification.Input; import org.xbmc.kore.jsonrpc.notification.Player; +import org.xbmc.kore.jsonrpc.notification.Playlist; import org.xbmc.kore.jsonrpc.notification.System; import org.xbmc.kore.utils.LogUtils; @@ -46,10 +47,13 @@ import java.net.ProtocolException; import java.net.Proxy; import java.net.Socket; import java.util.HashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Class responsible for communicating with the host. @@ -101,6 +105,12 @@ public class HostConnection { void onVolumeChanged(Application.OnVolumeChanged notification); } + public interface PlaylistNotificationsObserver { + void onPlaylistCleared(Playlist.OnClear notification); + void onPlaylistItemAdded(Playlist.OnAdd notification); + void onPlaylistItemRemoved(Playlist.OnRemove notification); + } + /** * Host to connect too */ @@ -119,10 +129,6 @@ public class HostConnection { * Socket used to communicate through TCP */ private Socket socket = null; - /** - * Listener {@link Thread} that will be listening on the TCP socket - */ - private Thread listenerThread = null; /** * {@link java.util.HashMap} that will hold the {@link MethodCallInfo} with the information @@ -154,6 +160,12 @@ public class HostConnection { private final HashMap applicationNotificationsObservers = new HashMap<>(); + /** + * The observers that will be notified of playlist notifications + */ + private final HashMap playlistNotificationsObservers = + new HashMap<>(); + private ExecutorService executorService; private final int connectTimeout; @@ -162,6 +174,8 @@ public class HostConnection { public static final int TCP_READ_TIMEOUT = 30000; // ms + private static final int CALLABLE_TIMEOUT = 30000; // ms + /** * OkHttpClient. Make sure it is initialized, by calling {@link #getOkHttpClient()} */ @@ -195,7 +209,7 @@ public class HostConnection { // Start with the default host protocol this.protocol = hostInfo.getProtocol(); // Create a single threaded executor - this.executorService = Executors.newSingleThreadExecutor(); + this.executorService = Executors.newFixedThreadPool(10); // Set timeout this.connectTimeout = connectTimeout; } @@ -283,7 +297,7 @@ public class HostConnection { } /** - * Registers an observer for input notifications + * Registers an observer for application notifications * @param observer The {@link InputNotificationsObserver} */ public void registerApplicationNotificationsObserver(ApplicationNotificationsObserver observer, @@ -292,13 +306,30 @@ public class HostConnection { } /** - * Unregisters and observer from the input notifications + * Unregisters and observer from the application notifications * @param observer The {@link InputNotificationsObserver} */ public void unregisterApplicationNotificationsObserver(ApplicationNotificationsObserver observer) { applicationNotificationsObservers.remove(observer); } + /** + * Registers an observer for playlist notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void registerPlaylistNotificationsObserver(PlaylistNotificationsObserver observer, + Handler handler) { + playlistNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the playlist notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void unregisterPlaylistNotificationsObserver(PlaylistNotificationsObserver observer) { + playlistNotificationsObservers.remove(observer); + } + /** * Calls the given method on the server * This call is always asynchronous. The results will be posted, through the @@ -334,7 +365,7 @@ public class HostConnection { if (protocol == PROTOCOL_HTTP) { executeThroughOkHttp(method, callback, handler); } else { - executeThroughTcp(method, callback, handler); + executeThroughTcp(method); } } }; @@ -380,6 +411,51 @@ public class HostConnection { }, null); return future; } + + /** + * Executes the {@link Callable} and waits for it to finish on a background thread. The + * result is returned using the {@link ApiCallback} and handler + * @param callable executed using an {@link ExecutorService} + * @param apiCallback used to return the result of the callable + * @param handler used to execute the {@link ApiCallback} methods + * @param + */ + public void execute(Callable callable, final ApiCallback apiCallback, final Handler handler) { + final Future future = executorService.submit(callable); + executorService.execute(new Runnable() { + @Override + public void run() { + try { + T result = future.get(CALLABLE_TIMEOUT, TimeUnit.MILLISECONDS); + handleSuccess(result); + } catch (ExecutionException e) { + handleError(ApiException.API_ERROR, e.getMessage()); + } catch (InterruptedException e) { + handleError(ApiException.API_WAITING_ON_RESULT_INTERRUPTED, e.getMessage()); + } catch (TimeoutException e) { + handleError(ApiException.API_WAITING_ON_RESULT_TIMEDOUT, e.getMessage()); + } + } + + private void handleSuccess(final T result) { + handler.post(new Runnable() { + @Override + public void run() { + apiCallback.onSuccess(result); + } + }); + } + + private void handleError(final int errorCode, final String message) { + handler.post(new Runnable() { + @Override + public void run() { + apiCallback.onError(errorCode, message); + } + }); + } + }); + } /** * Updates the client callback for the given {@link ApiMethod} if it is still pending. @@ -614,16 +690,14 @@ public class HostConnection { * Sends the JSON RPC request through TCP * Keeps a background thread running, listening on a socket */ - private void executeThroughTcp(final ApiMethod method, final ApiCallback callback, - final Handler handler) { + private void executeThroughTcp(final ApiMethod method) { String methodId = String.valueOf(method.getId()); try { // TODO: Validate if this shouldn't be enclosed by a synchronized. if (socket == null) { // Open connection to the server and setup reader thread socket = openTcpConnection(hostInfo); - listenerThread = newListenerThread(socket); - listenerThread.start(); + startListenerThread(socket); } // Write request @@ -677,9 +751,8 @@ public class HostConnection { } } - private Thread newListenerThread(final Socket socket) { - // Launch a new thread to read from the socket - return new Thread(new Runnable() { + private void startListenerThread(final Socket socket) { + executorService.execute(new Runnable() { @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); @@ -728,189 +801,235 @@ public class HostConnection { ObjectNode params = (ObjectNode)jsonResponse.get(ApiNotification.PARAMS_NODE); switch (notificationName) { - case Player.OnPause.NOTIFICATION_NAME: { - final Player.OnPause apiNotification = new Player.OnPause(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onPause(apiNotification); - } - }); + case Player.OnPause.NOTIFICATION_NAME: { + final Player.OnPause apiNotification = new Player.OnPause(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPause(apiNotification); + } + }); + } + break; + } + case Player.OnPlay.NOTIFICATION_NAME: { + final Player.OnPlay apiNotification = new Player.OnPlay(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPlay(apiNotification); + } + }); + } + break; + } + case Player.OnResume.NOTIFICATION_NAME: { + final Player.OnResume apiNotification = new Player.OnResume(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onResume(apiNotification); + } + }); + } + break; + } + case Player.OnSeek.NOTIFICATION_NAME: { + final Player.OnSeek apiNotification = new Player.OnSeek(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onSeek(apiNotification); + } + }); + } + break; + } + case Player.OnSpeedChanged.NOTIFICATION_NAME: { + final Player.OnSpeedChanged apiNotification = new Player.OnSpeedChanged(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onSpeedChanged(apiNotification); + } + }); + } + break; + } + case Player.OnStop.NOTIFICATION_NAME: { + final Player.OnStop apiNotification = new Player.OnStop(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onStop(apiNotification); + } + }); + } + break; + } + case Player.OnAVStart.NOTIFICATION_NAME: { + final Player.OnAVStart apiNotification = new Player.OnAVStart(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onAVStart(apiNotification); + } + }); + } + break; + } + case Player.OnAVChange.NOTIFICATION_NAME: { + final Player.OnAVChange apiNotification = new Player.OnAVChange(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onAVChange(apiNotification); + } + }); + } + break; + } + case Player.OnPropertyChanged.NOTIFICATION_NAME: { + final Player.OnPropertyChanged apiNotification = new Player.OnPropertyChanged(params); + for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPropertyChanged(apiNotification); + } + }); + } + break; + } + case System.OnQuit.NOTIFICATION_NAME: { + final System.OnQuit apiNotification = new System.OnQuit(params); + for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onQuit(apiNotification); + } + }); + } + break; + } + case System.OnRestart.NOTIFICATION_NAME: { + final System.OnRestart apiNotification = new System.OnRestart(params); + for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onRestart(apiNotification); + } + }); + } + break; + } + case System.OnSleep.NOTIFICATION_NAME: { + final System.OnSleep apiNotification = new System.OnSleep(params); + for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onSleep(apiNotification); + } + }); + } + break; + } + case Input.OnInputRequested.NOTIFICATION_NAME: { + final Input.OnInputRequested apiNotification = new Input.OnInputRequested(params); + for (final InputNotificationsObserver observer : inputNotificationsObservers.keySet()) { + Handler handler = inputNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onInputRequested(apiNotification); + } + }); + } + break; + } + case Application.OnVolumeChanged.NOTIFICATION_NAME: { + final Application.OnVolumeChanged apiNotification = new Application.OnVolumeChanged(params); + for (final ApplicationNotificationsObserver observer : applicationNotificationsObservers.keySet()) { + Handler handler = applicationNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onVolumeChanged(apiNotification); + } + }); + } + break; + } + case Playlist.OnClear.NOTIFICATION_NAME: { + final Playlist.OnClear apiNotification = + new Playlist.OnClear(params); + for (final PlaylistNotificationsObserver observer : + playlistNotificationsObservers.keySet()) { + Handler handler = playlistNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPlaylistCleared(apiNotification); + } + }); + } + break; + } + case Playlist.OnAdd.NOTIFICATION_NAME: { + final Playlist.OnAdd apiNotification = + new Playlist.OnAdd(params); + for (final PlaylistNotificationsObserver observer : + playlistNotificationsObservers.keySet()) { + Handler handler = playlistNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPlaylistItemAdded(apiNotification); + } + }); + } + break; + } + case Playlist.OnRemove.NOTIFICATION_NAME: { + final Playlist.OnRemove apiNotification = + new Playlist.OnRemove(params); + for (final PlaylistNotificationsObserver observer : + playlistNotificationsObservers.keySet()) { + Handler handler = playlistNotificationsObservers.get(observer); + postOrRunNow(handler, new Runnable() { + @Override + public void run() { + observer.onPlaylistItemRemoved(apiNotification); + } + }); + } + break; } - break; } - case Player.OnPlay.NOTIFICATION_NAME: { - final Player.OnPlay apiNotification = new Player.OnPlay(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onPlay(apiNotification); - } - }); - } - break; - } - case Player.OnResume.NOTIFICATION_NAME: { - final Player.OnResume apiNotification = new Player.OnResume(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onResume(apiNotification); - } - }); - } - break; - } - case Player.OnSeek.NOTIFICATION_NAME: { - final Player.OnSeek apiNotification = new Player.OnSeek(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onSeek(apiNotification); - } - }); - } - break; - } - case Player.OnSpeedChanged.NOTIFICATION_NAME: { - final Player.OnSpeedChanged apiNotification = new Player.OnSpeedChanged(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onSpeedChanged(apiNotification); - } - }); - } - break; - } - case Player.OnStop.NOTIFICATION_NAME: { - final Player.OnStop apiNotification = new Player.OnStop(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onStop(apiNotification); - } - }); - } - break; - } - case Player.OnAVStart.NOTIFICATION_NAME: { - final Player.OnAVStart apiNotification = new Player.OnAVStart(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onAVStart(apiNotification); - } - }); - } - break; - } - case Player.OnAVChange.NOTIFICATION_NAME: { - final Player.OnAVChange apiNotification = new Player.OnAVChange(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onAVChange(apiNotification); - } - }); - } - break; - } - case Player.OnPropertyChanged.NOTIFICATION_NAME: { - final Player.OnPropertyChanged apiNotification = new Player.OnPropertyChanged(params); - for (final PlayerNotificationsObserver observer : playerNotificationsObservers.keySet()) { - Handler handler = playerNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onPropertyChanged(apiNotification); - } - }); - } - break; - } - case System.OnQuit.NOTIFICATION_NAME: { - final System.OnQuit apiNotification = new System.OnQuit(params); - for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { - Handler handler = systemNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onQuit(apiNotification); - } - }); - } - break; - } - case System.OnRestart.NOTIFICATION_NAME: { - final System.OnRestart apiNotification = new System.OnRestart(params); - for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { - Handler handler = systemNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onRestart(apiNotification); - } - }); - } - break; - } - case System.OnSleep.NOTIFICATION_NAME: { - final System.OnSleep apiNotification = new System.OnSleep(params); - for (final SystemNotificationsObserver observer : systemNotificationsObservers.keySet()) { - Handler handler = systemNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onSleep(apiNotification); - } - }); - } - break; - } - case Input.OnInputRequested.NOTIFICATION_NAME: { - final Input.OnInputRequested apiNotification = new Input.OnInputRequested(params); - for (final InputNotificationsObserver observer : inputNotificationsObservers.keySet()) { - Handler handler = inputNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onInputRequested(apiNotification); - } - }); - } - break; - } - case Application.OnVolumeChanged.NOTIFICATION_NAME: { - final Application.OnVolumeChanged apiNotification = new Application.OnVolumeChanged(params); - for (final ApplicationNotificationsObserver observer : applicationNotificationsObservers.keySet()) { - Handler handler = applicationNotificationsObservers.get(observer); - postOrRunNow(handler, new Runnable() { - @Override - public void run() { - observer.onVolumeChanged(apiNotification); - } - }); - } - break; - }} - LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue()); + LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue()); } else { String methodId = jsonResponse.get(ApiMethod.ID_NODE).asText(); @@ -918,9 +1037,8 @@ public class HostConnection { // Error response callErrorCallback(methodId, new ApiException(ApiException.API_ERROR, jsonResponse)); } else { - // Sucess response + // Success response final MethodCallInfo methodCallInfo = clientCallbacks.get(methodId); -// LogUtils.LOGD(TAG, "Sending response to method: " + methodCallInfo.method.getMethodName()); if (methodCallInfo != null) { try { @@ -1002,6 +1120,7 @@ public class HostConnection { try { if (socket != null) { // Remove pending calls + clientCallbacks.clear(); if (!socket.isClosed()) { socket.close(); } diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Playlist.java b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Playlist.java new file mode 100644 index 0000000..9a0dad1 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/notification/Playlist.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.xbmc.kore.jsonrpc.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.jsonrpc.ApiNotification; +import org.xbmc.kore.utils.JsonUtils; + +/** + * All Playlist.* notifications + */ +public class Playlist { + + /** + * Player.OnClear notification + * Playlist has been cleared + */ + public static class OnClear extends ApiNotification { + public static final String NOTIFICATION_NAME = "Playlist.OnClear"; + + public final int playlistId; + + public OnClear(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + playlistId = JsonUtils.intFromJsonNode(dataNode, "playlistid"); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + public static class OnAdd extends ApiNotification { + public static final String NOTIFICATION_NAME = "Playlist.OnAdd"; + + public final int playlistId; + + public OnAdd(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + playlistId = JsonUtils.intFromJsonNode(dataNode, "playlistid"); + } + + @Override + public String getNotificationName() { + return NOTIFICATION_NAME; + } + } + + public static class OnRemove extends ApiNotification { + public static final String NOTIFICATION_NAME = "Playlist.OnRemove"; + + public final int playlistId; + + public OnRemove(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + playlistId = JsonUtils.intFromJsonNode(dataNode, "playlistid"); + } + + @Override + public String getNotificationName() { + return NOTIFICATION_NAME; + } + } +} 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 68a9db8..9ebcb66 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 @@ -39,8 +39,10 @@ import android.widget.Toast; import org.xbmc.kore.R; import org.xbmc.kore.host.HostConnectionObserver; import org.xbmc.kore.host.HostConnectionObserver.PlayerEventsObserver; +import org.xbmc.kore.host.HostConnectionObserver.PlaylistEventsObserver; import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.host.actions.GetPlaylist; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.ApiMethod; import org.xbmc.kore.jsonrpc.HostConnection; @@ -50,21 +52,25 @@ import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.ui.viewgroups.DynamicListView; +import org.xbmc.kore.ui.widgets.PlaylistsBar; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import butterknife.ButterKnife; import butterknife.BindView; +import butterknife.ButterKnife; import butterknife.Unbinder; /** * Playlist view */ public class PlaylistFragment extends Fragment - implements PlayerEventsObserver { + implements PlayerEventsObserver, PlaylistEventsObserver { private static final String TAG = LogUtils.makeLogTag(PlaylistFragment.class); /** @@ -82,16 +88,6 @@ public class PlaylistFragment extends Fragment */ private Handler callbackHandler = new Handler(); - /** - * The current active player id - */ - private int currentActivePlayerId = -1; - - /** - * Current playlist - */ - private int currentPlaylistId = -1; - /** * Playlist adapter */ @@ -99,6 +95,25 @@ public class PlaylistFragment extends Fragment private Unbinder unbinder; + /** + * Last call results + */ + private ListType.ItemsAll lastGetItemResult = null; + private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult; + private HashMap playlists = new HashMap<>(); + + private enum PLAYER_STATE { + CONNECTION_ERROR, + NO_RESULTS_YET, + PLAYING, + PAUSED, + STOPPED + } + + private PLAYER_STATE playerState; + + private boolean userSelectedTab; + /** * Injectable views */ @@ -108,6 +123,8 @@ public class PlaylistFragment extends Fragment @BindView(R.id.info_title) TextView infoTitle; @BindView(R.id.info_message) TextView infoMessage; + @BindView(R.id.playlists_bar) PlaylistsBar playlistsBar; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -127,11 +144,30 @@ public class PlaylistFragment extends Fragment playlistListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - Player.Open action = new Player.Open(Player.Open.TYPE_PLAYLIST, currentPlaylistId, position); + int playlistId = playlists.get(playlistsBar.getSelectedPlaylistType()).getPlaylistId(); + Player.Open action = new Player.Open(Player.Open.TYPE_PLAYLIST, playlistId, position); action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler); } }); + playlistsBar.setOnPlaylistSelectedListener(new PlaylistsBar.OnPlaylistSelectedListener() { + @Override + public void onPlaylistSelected(String playlistType) { + userSelectedTab = true; // do not switch to active playlist when user selected a tab + displayPlaylist(); + } + + @Override + public void onPlaylistDeselected(String playlistType) { + View v = playlistListView.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - playlistListView.getPaddingTop()); + + PlaylistHolder playlistHolder = playlists.get(playlistType); + if (playlistHolder != null) + playlistHolder.setListViewPosition(playlistListView.getFirstVisiblePosition(), top); + } + }); + return root; } @@ -145,13 +181,17 @@ public class PlaylistFragment extends Fragment @Override public void onResume() { super.onResume(); + hostConnectionObserver.registerPlayerObserver(this, true); + hostConnectionObserver.registerPlaylistObserver(this, true); } @Override public void onPause() { - super.onPause(); hostConnectionObserver.unregisterPlayerObserver(this); + hostConnectionObserver.unregisterPlaylistObserver(this); + + super.onPause(); } @Override @@ -170,10 +210,11 @@ public class PlaylistFragment extends Fragment public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_clear_playlist: - Playlist.Clear action = new Playlist.Clear(currentPlaylistId); + PlaylistHolder playlistHolder = playlists.get(playlistsBar.getSelectedPlaylistType()); + int playlistId = playlistHolder.getPlaylistId(); + playlistOnClear(playlistId); + Playlist.Clear action = new Playlist.Clear(playlistId); action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler); - // If we are playing something, refresh playlist - forceRefreshPlaylist(); break; default: break; @@ -182,11 +223,32 @@ public class PlaylistFragment extends Fragment return super.onOptionsItemSelected(item); } - public void forceRefreshPlaylist() { - // If we are playing something, refresh playlist - if ((lastCallResult == PLAYER_IS_PLAYING) || (lastCallResult == PLAYER_IS_PAUSED)) { - setupPlaylistInfo(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult); - } + private boolean refreshingPlaylist; + private void refreshPlaylist(GetPlaylist getPlaylist) { + if (refreshingPlaylist) + return; + + refreshingPlaylist = true; + hostManager.getConnection().execute(getPlaylist, + new ApiCallback>() { + @Override + public void onSuccess(ArrayList result) { + refreshingPlaylist = false; + + if(!isAdded()) + return; + + updatePlaylists(result); + displayPlaylist(); + } + + @Override + public void onError(int errorCode, String description) { + refreshingPlaylist = false; + + displayErrorGettingPlaylistMessage(description); + } + }, callbackHandler); } /** @@ -194,76 +256,72 @@ public class PlaylistFragment extends Fragment */ private ApiCallback defaultStringActionCallback = ApiMethod.getDefaultActionCallback(); - /** - * Last call results - */ - private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; - private ListType.ItemsAll lastGetItemResult = null; - private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult; - 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); + refreshPlaylist(new GetPlaylist(hostManager.getConnection(), lastGetActivePlayerResult.type)); } /** * HostConnectionObserver.PlayerEventsObserver interface callbacks */ + @Override public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult, PlayerType.PropertyValue getPropertiesResult, ListType.ItemsAll getItemResult) { - if ((lastGetPlaylistItemsResult == null) || - (lastCallResult != PlayerEventsObserver.PLAYER_IS_PLAYING) || - (currentActivePlayerId != getActivePlayerResult.playerid) || - (lastGetItemResult.id != getItemResult.id)) { - // Check if something is different, and only if so, start the chain calls - setupPlaylistInfo(getActivePlayerResult, getPropertiesResult, getItemResult); - currentActivePlayerId = getActivePlayerResult.playerid; - } else { - // Hopefully nothing changed, so just use the last results - displayPlaylist(getItemResult, lastGetPlaylistItemsResult); + playerState = PLAYER_STATE.PLAYING; + + lastGetItemResult = getItemResult; + lastGetActivePlayerResult = getActivePlayerResult; + + if (! userSelectedTab) { + playlistsBar.selectTab(getActivePlayerResult.type); } - // Save results - lastCallResult = PLAYER_IS_PLAYING; - lastGetActivePlayerResult = getActivePlayerResult; - lastGetPropertiesResult = getPropertiesResult; - lastGetItemResult = getItemResult; + playlistsBar.setIsPlaying(getActivePlayerResult.type, true); + + displayPlaylist(); + + PlaylistHolder playlistHolder = playlists.get(getActivePlayerResult.type); + if (playlistHolder != null && isPlaying(playlistHolder.getPlaylistResult)) { + highlightCurrentlyPlayingItem(); + } else { + playlistListView.clearChoices(); + } } + @Override public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, PlayerType.PropertyValue getPropertiesResult, ListType.ItemsAll getItemResult) { - if ((lastGetPlaylistItemsResult == null) || - (lastCallResult != PlayerEventsObserver.PLAYER_IS_PLAYING) || - (currentActivePlayerId != getActivePlayerResult.playerid) || - (lastGetItemResult.id != getItemResult.id)) { - setupPlaylistInfo(getActivePlayerResult, getPropertiesResult, getItemResult); - currentActivePlayerId = getActivePlayerResult.playerid; - } else { - // Hopefully nothing changed, so just use the last results - displayPlaylist(getItemResult, lastGetPlaylistItemsResult); + playerState = PLAYER_STATE.PAUSED; + + lastGetItemResult = getItemResult; + lastGetActivePlayerResult = getActivePlayerResult; + + if (! userSelectedTab) { + playlistsBar.selectTab(getActivePlayerResult.type); } - lastCallResult = PLAYER_IS_PAUSED; - lastGetActivePlayerResult = getActivePlayerResult; - lastGetPropertiesResult = getPropertiesResult; - lastGetItemResult = getItemResult; + playlistsBar.setIsPlaying(getActivePlayerResult.type, false); } + @Override public void playerOnStop() { - HostInfo hostInfo = hostManager.getHostInfo(); - switchToPanel(R.id.info_panel); - infoTitle.setText(R.string.nothing_playing); - infoMessage.setText(String.format(getString(R.string.connected_to), hostInfo.getName())); + playerState = PLAYER_STATE.STOPPED; - lastCallResult = PLAYER_IS_STOPPED; + if (lastGetActivePlayerResult != null) + playlistsBar.setIsPlaying(lastGetActivePlayerResult.type, false); + + displayPlaylist(); + + playlistListView.clearChoices(); } + @Override public void playerOnConnectionError(int errorCode, String description) { + playerState = PLAYER_STATE.CONNECTION_ERROR; + HostInfo hostInfo = hostManager.getHostInfo(); switchToPanel(R.id.info_panel); @@ -275,11 +333,12 @@ public class PlaylistFragment extends Fragment infoTitle.setText(R.string.no_xbmc_configured); infoMessage.setText(null); } - - lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; } + @Override public void playerNoResultsYet() { + playerState = PLAYER_STATE.NO_RESULTS_YET; + // Initialize info panel switchToPanel(R.id.info_panel); HostInfo hostInfo = hostManager.getHostInfo(); @@ -289,106 +348,129 @@ public class PlaylistFragment extends Fragment infoTitle.setText(R.string.no_xbmc_configured); } infoMessage.setText(null); - lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; } + @Override public void systemOnQuit() { playerNoResultsYet(); } // Ignore this + @Override public void inputOnInputRequested(String title, String type, String value) {} + @Override public void observerOnStopObserving() {} - /** - * Starts the call chain to display the playlist - */ - private void setupPlaylistInfo(final PlayerType.GetActivePlayersReturnType getActivePlayerResult, - final PlayerType.PropertyValue getPropertiesResult, - final ListType.ItemsAll getItemResult) { + @Override + public void playlistOnClear(int playlistId) { + Iterator it = playlists.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + if ( playlists.get(key).getPlaylistResult.id == playlistId ) { + it.remove(); + playlistsBar.setHasPlaylistAvailable(key, false); + playlistsBar.setIsPlaying(key, false); + } + } + displayPlaylist(); + } - currentPlaylistId = getPropertiesResult.playlistid; + @Override + public void playlistChanged(int playlistId) { + refreshPlaylist(new GetPlaylist(hostManager.getConnection(), playlistId)); + } - if (currentPlaylistId == -1) { - // Couldn't find a playlist of the same type, just report empty - displayEmptyPlaylistMessage(); - } else { - // Call GetItems - String[] propertiesToGet = new String[] { - ListType.FieldsAll.ART, - ListType.FieldsAll.ARTIST, - ListType.FieldsAll.ALBUMARTIST, - ListType.FieldsAll.ALBUM, - ListType.FieldsAll.DISPLAYARTIST, - ListType.FieldsAll.EPISODE, - ListType.FieldsAll.FANART, - ListType.FieldsAll.FILE, - ListType.FieldsAll.SEASON, - ListType.FieldsAll.SHOWTITLE, - ListType.FieldsAll.STUDIO, - ListType.FieldsAll.TAGLINE, - ListType.FieldsAll.THUMBNAIL, - ListType.FieldsAll.TITLE, - ListType.FieldsAll.TRACK, - ListType.FieldsAll.DURATION, - ListType.FieldsAll.RUNTIME, - }; - Playlist.GetItems getItems = new Playlist.GetItems(currentPlaylistId, propertiesToGet); - getItems.execute(hostManager.getConnection(), new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (!isAdded()) return; - // Ok, we've got all the info, save and display playlist - lastGetPlaylistItemsResult = result; - displayPlaylist(getItemResult, result); - } + @Override + public void playlistsAvailable(ArrayList playlists) { + updatePlaylists(playlists); - @Override - public void onError(int errorCode, String description) { - if (!isAdded()) return; - // Oops - displayErrorGettingPlaylistMessage(description); - } - }, callbackHandler); + if ( playerState == PLAYER_STATE.PLAYING ) // if item is currently playing displaying is already handled by playerOnPlay callback + return; + + // BUG: When playing movies playlist stops, audio tab gets selected when it contains a playlist. + // We might want a separate var to check if something has already played and turn off automatic + // playlist switching if playback stops + if (playerState == PLAYER_STATE.STOPPED && lastGetActivePlayerResult == null && !userSelectedTab) { // do not automatically switch to first available playlist if user manually selected a playlist + playlistsBar.selectTab(playlists.get(0).type); + } + displayPlaylist(); + } + + @Override + public void playlistOnError(int errorCode, String description) { + displayErrorGettingPlaylistMessage(description); + } + + private void updatePlaylists(ArrayList playlists) { + for (GetPlaylist.GetPlaylistResult getPlaylistResult : playlists) { + playlistsBar.setHasPlaylistAvailable(getPlaylistResult.type, true); + + PlaylistHolder playlistHolder = this.playlists.get(getPlaylistResult.type); + + if (playlistHolder == null) { + playlistHolder = new PlaylistHolder(); + this.playlists.put(getPlaylistResult.type, playlistHolder); + } + + playlistHolder.setPlaylist(getPlaylistResult); } } - private void displayPlaylist(final ListType.ItemsAll getItemResult, - final List playlistItems) { - if (playlistItems.isEmpty()) { + private void displayPlaylist() { + switchToPanel(R.id.playlist); + + PlaylistHolder playlistHolder = playlists.get(playlistsBar.getSelectedPlaylistType()); + if (playlistHolder == null) { displayEmptyPlaylistMessage(); return; } - switchToPanel(R.id.playlist); + + GetPlaylist.GetPlaylistResult getPlaylistResult = playlistHolder.getPlaylistResult; + if (getPlaylistResult == null) { + displayEmptyPlaylistMessage(); + return; + } + + // JSON RPC does not support picture items in Playlist.Item so we disable item movement + // for the picture playlist + if (getPlaylistResult.type.contentEquals(ListType.ItemBase.TYPE_PICTURE)) + playlistListView.enableItemDragging(false); + else + playlistListView.enableItemDragging(true); //If a user is dragging a list item we must not modify the adapter to prevent //the dragged item's adapter position from diverging from its listview position if (!playlistListView.isItemBeingDragged()) { - // Set items, which call notifyDataSetChanged - playListAdapter.setPlaylistItems(playlistItems); - highlightItem(getItemResult, playlistItems); - } else { - highlightItem(getItemResult, playListAdapter.playlistItems); + playListAdapter.setPlaylistItems(getPlaylistResult.items); } + + playlistListView.setSelectionFromTop(playlistHolder.index, playlistHolder.top); } - private void highlightItem(final ListType.ItemsAll item, - final List playlistItems) { + private boolean isPlaying(GetPlaylist.GetPlaylistResult getPlaylistResult) { + return playerState == PLAYER_STATE.PLAYING && lastGetActivePlayerResult != null && + getPlaylistResult.id == lastGetActivePlayerResult.playerid; + } + + private void highlightCurrentlyPlayingItem() { + if (! playlistsBar.getSelectedPlaylistType().contentEquals(lastGetActivePlayerResult.type)) + return; + + List playlistItems = playlists.get(playlistsBar.getSelectedPlaylistType()).getPlaylistResult.items; for (int i = 0; i < playlistItems.size(); i++) { - if ((playlistItems.get(i).id == item.id) && - (playlistItems.get(i).type.equals(item.type))) { + if ((playlistItems.get(i).id == lastGetItemResult.id) && + (playlistItems.get(i).type.equals(lastGetItemResult.type))) { //When user is dragging an item it is very annoying when we change the list position if (!playlistListView.isItemBeingDragged()) { playlistListView.setSelection(i); } - playlistListView.setItemChecked(i, true); + playlistListView.setItemChecked(i, true); } } } - /** * Switches the info panel shown (they are exclusive) * @param panelResId The panel to show @@ -420,9 +502,10 @@ public class PlaylistFragment extends Fragment * Displays empty playlist */ private void displayEmptyPlaylistMessage() { + HostInfo hostInfo = hostManager.getHostInfo(); switchToPanel(R.id.info_panel); infoTitle.setText(R.string.playlist_empty); - infoMessage.setText(null); + infoMessage.setText(String.format(getString(R.string.connected_to), hostInfo.getName())); } /** @@ -443,9 +526,9 @@ public class PlaylistFragment extends Fragment switch (item.getItemId()) { case R.id.action_remove_playlist_item: // Remove this item from the playlist - Playlist.Remove action = new Playlist.Remove(currentPlaylistId, position); + int playlistId = playlists.get(playlistsBar.getSelectedPlaylistType()).getPlaylistId(); + Playlist.Remove action = new Playlist.Remove(playlistId, position); action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler); - forceRefreshPlaylist(); return true; } return false; @@ -545,11 +628,12 @@ public class PlaylistFragment extends Fragment return; } - Playlist.Remove remove = new Playlist.Remove(currentPlaylistId, originalPosition); + final int playlistId = playlists.get(playlistsBar.getSelectedPlaylistType()).getPlaylistId(); + Playlist.Remove remove = new Playlist.Remove(playlistId, originalPosition); remove.execute(hostConnection, new ApiCallback() { @Override public void onSuccess(String result) { - Playlist.Insert insert = new Playlist.Insert(currentPlaylistId, finalPosition, createPlaylistTypeItem(playlistItems.get(finalPosition))); + Playlist.Insert insert = new Playlist.Insert(playlistId, finalPosition, createPlaylistTypeItem(playlistItems.get(finalPosition))); insert.execute(hostConnection, new ApiCallback() { @Override public void onSuccess(String result) { @@ -632,6 +716,12 @@ public class PlaylistFragment extends Fragment artUrl = item.thumbnail; duration = item.runtime; break; + case ListType.ItemsAll.TYPE_PICTURE: + title = TextUtils.isEmpty(item.label)? item.file : item.label; + details = item.type; + artUrl = item.thumbnail; + duration = 0; + break; default: // Don't yet recognize this type title = TextUtils.isEmpty(item.label)? item.file : item.label; @@ -663,9 +753,14 @@ public class PlaylistFragment extends Fragment artUrl, title, viewHolder.art, artWidth, artHeight); - // For the popupmenu - viewHolder.contextMenu.setTag(position); - viewHolder.contextMenu.setOnClickListener(playlistItemMenuClickListener); + if (!item.type.contentEquals(ListType.ItemsAll.TYPE_PICTURE)) { + // For the popupmenu + viewHolder.contextMenu.setVisibility(View.VISIBLE); + viewHolder.contextMenu.setTag(position); + viewHolder.contextMenu.setOnClickListener(playlistItemMenuClickListener); + } else { + viewHolder.contextMenu.setVisibility(View.INVISIBLE); + } return convertView; } @@ -717,4 +812,34 @@ public class PlaylistFragment extends Fragment } } + private static class PlaylistHolder { + private GetPlaylist.GetPlaylistResult getPlaylistResult; + private int top; + private int index; + + private PlaylistHolder() {} + + public void setPlaylist(GetPlaylist.GetPlaylistResult getPlaylistResult) { + this.getPlaylistResult = getPlaylistResult; + } + + public GetPlaylist.GetPlaylistResult getPlaylist() { + return getPlaylistResult; + } + + void setListViewPosition(int index, int top) { + this.index = index; + this.top = top; + } + + public int getTop() { + return top; + } + + public int getIndex() { + return index; + } + + public int getPlaylistId() { return getPlaylistResult.id; } + } } 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 870fb8d..4587e5b 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 @@ -432,7 +432,6 @@ public class RemoteActivity extends BaseActivity Toast.LENGTH_SHORT) .show(); } - refreshPlaylist(); } }); } catch (InterruptedException ignored) { @@ -670,13 +669,4 @@ public class RemoteActivity extends BaseActivity public void SwitchToRemotePanel() { viewPager.setCurrentItem(1); } - - private void refreshPlaylist() { - String tag = "android:switcher:" + viewPager.getId() + ":" + PLAYLIST_FRAGMENT_ID; - PlaylistFragment playlistFragment = (PlaylistFragment)getSupportFragmentManager() - .findFragmentByTag(tag); - if (playlistFragment != null) { - playlistFragment.forceRefreshPlaylist(); - } - } } diff --git a/app/src/main/java/org/xbmc/kore/ui/viewgroups/DynamicListView.java b/app/src/main/java/org/xbmc/kore/ui/viewgroups/DynamicListView.java index c9dff0a..71cc828 100644 --- a/app/src/main/java/org/xbmc/kore/ui/viewgroups/DynamicListView.java +++ b/app/src/main/java/org/xbmc/kore/ui/viewgroups/DynamicListView.java @@ -63,6 +63,12 @@ import org.xbmc.kore.utils.LogUtils; * When the hover cell is either above or below the bounds of the listview, this * listview also scrolls on its own so as to reveal additional content. */ + +/** + * TODO Replace with RecyclerView and ItemTouchHelper + * https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf + */ + public class DynamicListView extends ListView { private static final String TAG = LogUtils.makeLogTag(DynamicListView.class); @@ -99,6 +105,7 @@ public class DynamicListView extends ListView { private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; private boolean itemBeingDragged; + private boolean itemDraggingEnabled; public interface DynamicListAdapter extends ListAdapter { @@ -148,6 +155,10 @@ public class DynamicListView extends ListView { super.setAdapter(adapter); } + public void enableItemDragging(boolean enable) { + this.itemDraggingEnabled = enable; + } + /** * Use this to determine if an item is being repositioned in the list. * The data in the adapter must not be updated other then through the @@ -167,6 +178,9 @@ public class DynamicListView extends ListView { private AdapterView.OnItemLongClickListener mOnItemLongClickListener = new AdapterView.OnItemLongClickListener() { public boolean onItemLongClick(AdapterView arg0, View arg1, int position, long id) { + if (!itemDraggingEnabled) + return false; + mTotalOffset = 0; mOriginalPosition = position; diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/PlaylistsBar.java b/app/src/main/java/org/xbmc/kore/ui/widgets/PlaylistsBar.java new file mode 100644 index 0000000..31b3e13 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/widgets/PlaylistsBar.java @@ -0,0 +1,203 @@ +/* + * Copyright 2018 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.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.support.design.widget.TabLayout; +import android.util.AttributeSet; +import android.view.LayoutInflater; + +import org.xbmc.kore.R; +import org.xbmc.kore.jsonrpc.type.PlaylistType; + +import java.util.ArrayList; + +public class PlaylistsBar extends TabLayout { + + private int highlightColor; + private int defaultColor; + + public interface OnPlaylistSelectedListener { + void onPlaylistSelected(String playlistType); + void onPlaylistDeselected(String playlistType); + } + + final Handler handler = new Handler(); + + private ArrayList tabStates = new ArrayList<>(); + + public PlaylistsBar(Context context) { + super(context); + init(context); + } + + public PlaylistsBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public PlaylistsBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.playlist_bar, this); + + setStyle(context); + + for(int i = 0; i < getTabCount(); i++) { + TabState tabState = new TabState(); + tabState.position = i; + tabState.icon = getTabAt(i).getIcon().mutate(); + tabState.setEnabled(false); + tabStates.add(tabState); + } + } + + /** + * Sets the tab for the given playlist type to selected state. + * Note: this does not call the {@link OnPlaylistSelectedListener#onPlaylistSelected(String)} + * @param playlistType + */ + public void selectTab(String playlistType) { + Tab tab = getTabAt(getTabPositionForType(playlistType)); + tab.setTag(new Object()); // Make we do not trigger OnPlaylistSelectedListener + tab.select(); + } + + public void setHasPlaylistAvailable(String playlistType, boolean playlistAvailable) { + tabStates.get(getTabPositionForType(playlistType)).setEnabled(playlistAvailable); + } + + private Runnable runnable; + + public void setIsPlaying(final String playlistType, final boolean isPlaying) { + handler.removeCallbacks(runnable); + + runnable = new Runnable() { + @Override + public void run() { + TabState tabStatePlaying = tabStates.get(getTabPositionForType(playlistType)); + tabStatePlaying.setPlaying(isPlaying); + + for (TabState tabState : tabStates) { + if (tabStatePlaying != tabState) + tabState.setPlaying(false); + } + } + }; + + handler.postDelayed(runnable, 1000); + } + + public String getTypeForTabPosition(int tabPosition) { + switch (tabPosition) { + case 0: + return PlaylistType.GetPlaylistsReturnType.VIDEO; + case 1: + return PlaylistType.GetPlaylistsReturnType.AUDIO; + case 2: + return PlaylistType.GetPlaylistsReturnType.PICTURE; + default: + return PlaylistType.GetPlaylistsReturnType.VIDEO; + } + } + + public String getSelectedPlaylistType() { + return getTypeForTabPosition(getSelectedTabPosition()); + } + + public void setOnPlaylistSelectedListener(final OnPlaylistSelectedListener onPlaylistSelectedListener) { + addOnTabSelectedListener(new OnTabSelectedListener() { + @Override + public void onTabSelected(Tab tab) { + if (tab.getTag() == null) + onPlaylistSelectedListener.onPlaylistSelected(getTypeForTabPosition(tab.getPosition())); + + tab.setTag(null); + } + + @Override + public void onTabUnselected(Tab tab) { + onPlaylistSelectedListener.onPlaylistDeselected(getTypeForTabPosition(tab.getPosition())); + } + + @Override + public void onTabReselected(Tab tab) { + tab.setTag(null); + } + }); + } + + private int getTabPositionForType(String playlistType) { + switch(playlistType) { + case PlaylistType.GetPlaylistsReturnType.VIDEO: + return 0; + case PlaylistType.GetPlaylistsReturnType.AUDIO: + return 1; + case PlaylistType.GetPlaylistsReturnType.PICTURE: + return 2; + default: + return 0; + } + } + + private void setStyle(Context context) { + if (!this.isInEditMode()) { + TypedArray styledAttributes = context.getTheme() + .obtainStyledAttributes(new int[]{R.attr.colorAccent, + R.attr.defaultButtonColorFilter}); + highlightColor = styledAttributes.getColor(styledAttributes.getIndex(0), + context.getResources().getColor(R.color.accent_default)); + defaultColor = styledAttributes.getColor(styledAttributes.getIndex(1), + context.getResources().getColor(R.color.white)); + styledAttributes.recycle(); + } + } + + private class TabState { + boolean enabled; + boolean isPlaying; + int position; + Drawable icon; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if(enabled) { + icon.setAlpha(255); + } else { + icon.setAlpha(127); + setPlaying(false); + } + } + + public void setPlaying(boolean playing) { + isPlaying = playing; + if (playing) { + icon.setColorFilter(highlightColor, PorterDuff.Mode.SRC_ATOP); + } else { + icon.setColorFilter(defaultColor, PorterDuff.Mode.SRC_ATOP); + } + } + } +} diff --git a/app/src/main/res/layout/fragment_playlist.xml b/app/src/main/res/layout/fragment_playlist.xml index 8e4e8d6..bb58589 100644 --- a/app/src/main/res/layout/fragment_playlist.xml +++ b/app/src/main/res/layout/fragment_playlist.xml @@ -15,20 +15,35 @@ limitations under the License. --> - + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + \ No newline at end of file diff --git a/doc/diagrams/activity/playlistfragment.puml b/doc/diagrams/activity/playlistfragment.puml new file mode 100644 index 0000000..ea1342d --- /dev/null +++ b/doc/diagrams/activity/playlistfragment.puml @@ -0,0 +1,42 @@ +@startuml +(*) --> "OnResume" +"OnResume" --> "Register Player Observer" +"OnResume" --> "Register Playlist Observer" +"Register Playlist Observer" --> ===A1=== +===A1=== --> "playlistsAvailable" +"playlistsAvailable" --> "Store playlists" +"Store playlists" --> "Set tab icons to playlist available" +"Set tab icons to playlist available" --> "Check player state" +"Check player state" --> ===A2=== +===A2=== --> "Stopped after playing" +--> "Update playlist" +===A2=== --> "Nothing played yet" +--> if "User selected tab?" + --> [No] "Switch to first available playlist" + else + --> [Yes] "Update playlist" + endif +===A2=== --> "Paused" +--> "Update playlist" +===A2=== --> "Playing" +--> "Do nothing" +===A1=== --> "playlistOnClear" +"playlistOnClear" --> "Remove playlist" +"Remove playlist" --> "Show empty playlist message" +"Register Player Observer" --> ===B1=== +===B1=== --> "playerOnConnectionError" +===B1=== --> "playerOnPause" +===B1=== --> "playerOnPlay" +===B1=== --> "playerOnStop" +"playerOnStop" --> "Keep showing current playlist" +"Keep showing current playlist" --> "Set tab icon to NOT playing" +"playerOnConnectionError" --> "Clear local playlists" +"Clear local playlists" --> "Show error message" +"Show error message" --> "Set tab icons to no_playlist state" +"playerOnPause" --> ===B2=== +"playerOnPlay" --> ===B2=== +===B2=== --> "Set tab icon to playing" +--> if "User did not select a tab?" + --> [Yes] "Switch to tab for given playlist" +endif +@enduml \ No newline at end of file