Implemented showing playlists even when not playing (#618)

Implemented showing a music/video/picture playlist in PlaylistFragment even
when playback has stopped but the playlist is still available on Kodi.
The playlist is now only cleared if the Playlist.OnClear event is received
from Kodi.

As the pictures playlist does not support moving/removing items this has
been disabled for the pictures playlist only.

Added a new package org.xbmc.kore.host.actions to hold all Callable's that
we use to handle complex interactions with Kodi that use multiple JSON RPC
calls.

Fixed issue in HostConnectionObserver in method notifySomethingIsPlaying
where hostState.lastGetItemResult.label might not be set.

Reduced calling (force)refreshPlaylist considerably as it should now be
handled by the playlist observer.
This commit is contained in:
Martijn Brekhof 2019-03-30 12:08:58 +01:00 committed by Synced Synapse
parent 46d4b5ffe1
commit cdbdd98d6a
12 changed files with 1317 additions and 378 deletions

View File

@ -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<GetPlaylist.GetPlaylistResult> 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<PlayerEventsObserver> playerEventsObservers = new ArrayList<>();
private List<ApplicationEventsObserver> applicationEventsObservers = new ArrayList<>();
private List<PlaylistEventsObserver> 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<GetPlaylist.GetPlaylistResult> prevGetPlaylistResults = new ArrayList<>();
private boolean isCheckingPlaylist = false;
private void checkPlaylist() {
if (isCheckingPlaylist)
return;
isCheckingPlaylist = true;
connection.execute(new GetPlaylist(connection), new ApiCallback<ArrayList<GetPlaylist.GetPlaylistResult>>() {
@Override
public void onSuccess(ArrayList<GetPlaylist.GetPlaylistResult> 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<GetPlaylist.GetPlaylistResult> 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<PlayerType.PropertyValue>() {
@ -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<ListType.ItemsAll>() {
@ -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<PlayerEventsObserver> 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;

View File

@ -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<ArrayList<GetPlaylist.GetPlaylistResult>> {
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<String, Integer> 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<GetPlaylistResult> call() throws ExecutionException, InterruptedException {
if (playlistsTypesAndIds == null)
playlistsTypesAndIds = getPlaylists(hostConnection);
if (playlistType != null) {
GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForType(playlistType);
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
playlists.add(getPlaylistResult);
return playlists;
} else if (playlistId > -1 ) {
GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForId(playlistId);
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
playlists.add(getPlaylistResult);
return playlists;
} else
return retrieveNonEmptyPlaylists();
}
private GetPlaylistResult retrievePlaylistItemsForId(int playlistId) throws InterruptedException,
ExecutionException {
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection, playlistId);
return new GetPlaylistResult(playlistId, getPlaylistType(playlistId), playlistItems);
}
private GetPlaylistResult retrievePlaylistItemsForType(String type) throws InterruptedException,
ExecutionException {
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection, playlistsTypesAndIds.get(type));
return new GetPlaylistResult(playlistsTypesAndIds.get(type), type, playlistItems);
}
private ArrayList<GetPlaylistResult> retrieveNonEmptyPlaylists() throws InterruptedException,
ExecutionException {
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
for (String type : playlistsTypesAndIds.keySet()) {
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection,
playlistsTypesAndIds.get(type));
if (!playlistItems.isEmpty())
playlists.add(new GetPlaylistResult(playlistsTypesAndIds.get(type), type, playlistItems));
}
return playlists;
}
private HashMap<String, Integer> getPlaylists(HostConnection hostConnection)
throws ExecutionException, InterruptedException {
HashMap<String, Integer> playlistsHashMap = new HashMap<>();
ArrayList<PlaylistType.GetPlaylistsReturnType> playlistsReturnTypes = hostConnection.execute(new Playlist.GetPlaylists()).get();
for (PlaylistType.GetPlaylistsReturnType type : playlistsReturnTypes) {
playlistsHashMap.put(type.type, type.playlistid);
}
return playlistsHashMap;
}
private List<ListType.ItemsAll> retrievePlaylistItems(HostConnection hostConnection,
int playlistId)
throws InterruptedException, ExecutionException {
ApiMethod<List<ListType.ItemsAll>> 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<ListType.ItemsAll> items;
private GetPlaylistResult(int playlistId, String type, List<ListType.ItemsAll> items) {
this.id = playlistId;
this.type = type;
this.items = items;
}
}
}

View File

@ -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;
/**

View File

@ -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<ApplicationNotificationsObserver, Handler> applicationNotificationsObservers =
new HashMap<>();
/**
* The observers that will be notified of playlist notifications
*/
private final HashMap<PlaylistNotificationsObserver, Handler> 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 <T>
*/
public <T> void execute(Callable<T> callable, final ApiCallback<T> apiCallback, final Handler handler) {
final Future<T> 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 <T> void executeThroughTcp(final ApiMethod<T> method, final ApiCallback<T> callback,
final Handler handler) {
private <T> void executeThroughTcp(final ApiMethod<T> 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();
}

View File

@ -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;
}
}
}

View File

@ -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<String, PlaylistHolder> 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<ArrayList<GetPlaylist.GetPlaylistResult>>() {
@Override
public void onSuccess(ArrayList<GetPlaylist.GetPlaylistResult> 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<String> 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<ListType.ItemsAll> lastGetPlaylistItemsResult = null;
@Override
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
if (notificationsData.property.shuffled != null)
setupPlaylistInfo(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult);
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<String> 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<List<ListType.ItemsAll>>() {
@Override
public void onSuccess(List<ListType.ItemsAll> 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<GetPlaylist.GetPlaylistResult> 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<GetPlaylist.GetPlaylistResult> 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<ListType.ItemsAll> 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<ListType.ItemsAll> 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<ListType.ItemsAll> 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<String>() {
@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<String>() {
@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; }
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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<TabState> 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);
}
}
}
}

View File

@ -15,20 +15,35 @@
limitations under the License.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/remote_info_panel"/>
<org.xbmc.kore.ui.widgets.PlaylistsBar
android:id="@+id/playlists_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/buttonbar_height"
android:layout_alignParentBottom="true"
app:tabMode="fixed"
app:tabGravity="fill"
app:tabTextColor="@color/white_dim_50pct"
app:tabSelectedTextColor="@color/white"
app:tabBackground="?attr/contentBackgroundColor"
style="@style/Widget.Button.Borderless"
android:tint="?attr/defaultButtonColorFilter"
android:elevation="2dp"/>
<!-- Playlist -->
<org.xbmc.kore.ui.viewgroups.DynamicListView
android:id="@+id/playlist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_above="@id/playlists_bar"
android:paddingLeft="@dimen/small_padding"
android:paddingRight="@dimen/small_padding"
android:paddingTop="@dimen/remote_page_indicator_height"

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.design.widget.TabItem
android:id="@+id/pb_tabitem_videos"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:icon="?attr/iconMovies"/>
<android.support.design.widget.TabItem
android:id="@+id/pb_tabitem_audio"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:icon="?attr/iconMusic"/>
<android.support.design.widget.TabItem
android:id="@+id/pb_tabitem_pictures"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:icon="?attr/iconPicture"/>
</merge>

View File

@ -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