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