package eu.se_bastiaan.popcorntimeremote.rpc; import android.util.Log; import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import com.squareup.okhttp.Authenticator; import com.squareup.okhttp.Call; import com.squareup.okhttp.Callback; import com.squareup.okhttp.Credentials; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import java.io.IOException; import java.net.Proxy; import java.util.ArrayList; import java.util.Arrays; import eu.se_bastiaan.popcorntimeremote.utils.LogUtils; import eu.se_bastiaan.popcorntimeremote.utils.Version; public class PopcornTimeRpcClient { private static final String VOLUME = "volume"; private static final String OLD_API_METHOD_NOT_IMPLEMENTED = "Old API, method not implemented"; private static final String ZERO_VERSION = "0.0.0"; private final OkHttpClient mClient = new OkHttpClient(); private final Gson mGson = new Gson(); private String mIpAddress, mPort, mUrl, mUsername, mPassword, mVersion; public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); public enum RequestId { PING, UP, DOWN, LEFT, RIGHT, ENTER, BACK, QUALITY, NEXT_SEASON, PREV_SEASON, TOGGLE_PLAY, PLAY_TRAILER, TOGGLE_TABS, TOGGLE_FULLSCREEN, TOGGLE_FAVOURITE, TOGGLE_WATCHED, TOGGLE_MUTE, SET_VOLUME, GET_VOLUME, GET_PLAYING, FILTER_GENRE, FILTER_SORTER, FILTER_TYPE, FILTER_SEARCH, CLEAR_SEARCH, SEEK, GET_VIEWSTACK, SET_SELECTION, GET_SELECTION, GET_FULLSCREEN, GET_SUBTITLES, GET_PLAYERS, SET_PLAYER, SET_SUBTITLE, LISTENNOTIFICATIONS } public PopcornTimeRpcClient(String ipAddress, String port, String username, String password, String version) { mVersion = version; init(ipAddress, port, username, password); } public PopcornTimeRpcClient(String ipAddress, String port, String username, String password) { mVersion = ZERO_VERSION; init(ipAddress, port, username, password); } private void init(String ipAddress, String port, String username, String password) { mIpAddress = ipAddress; mPort = port; mUsername = username; mPassword = password; checkUrl(); mUrl = mIpAddress + ":" + mPort + "/"; } public void setVersion(String version) { mVersion = version; } public String getVersion() { return mVersion != null ? mVersion : ZERO_VERSION; } public Call ping(Callback callback) { RpcRequest request = new RpcRequest("ping", RequestId.PING); return request(request, callback); } public Call up(Callback callback) { RpcRequest request = new RpcRequest("up", RequestId.UP); return request(request, callback); } public Call down(Callback callback) { RpcRequest request = new RpcRequest("down", RequestId.DOWN); return request(request, callback); } public Call left(Callback callback) { RpcRequest request = new RpcRequest("left", RequestId.LEFT); return request(request, callback); } public Call right(Callback callback) { RpcRequest request = new RpcRequest("right", RequestId.RIGHT); return request(request, callback); } public Call enter(Callback callback) { RpcRequest request = new RpcRequest("enter", RequestId.ENTER); return request(request, callback); } public Call back(Callback callback) { RpcRequest request = new RpcRequest("back", RequestId.BACK); return request(request, callback); } public Call toggleQuality(Callback callback) { RpcRequest request; if(Version.compare(mVersion, ZERO_VERSION)) { request = new RpcRequest("togglequality", RequestId.QUALITY); } else { request = new RpcRequest("quality", RequestId.QUALITY); } return request(request, callback); } public Call nextSeason(Callback callback) { RpcRequest request = new RpcRequest("nextseason", RequestId.NEXT_SEASON); return request(request, callback); } public Call prevSeason(Callback callback) { RpcRequest request = new RpcRequest("previousseason", RequestId.PREV_SEASON); return request(request, callback); } public Call togglePlay(Callback callback) { RpcRequest request = new RpcRequest("toggleplaying", RequestId.TOGGLE_PLAY); return request(request, callback); } public Call playTrailer(Callback callback) { RpcRequest request = new RpcRequest("watchtrailer", RequestId.PLAY_TRAILER); return request(request, callback); } public Call toggleTabs(Callback callback) { RpcRequest request; if(Version.compare(mVersion, ZERO_VERSION)) { request = new RpcRequest("toggletab", RequestId.TOGGLE_TABS); } else { request = new RpcRequest("togglemoviesshows", RequestId.TOGGLE_TABS); } return request(request, callback); } public Call toggleFullscreen(Callback callback) { RpcRequest request = new RpcRequest("togglefullscreen", RequestId.TOGGLE_FULLSCREEN); return request(request, callback); } public Call toggleFavourite(Callback callback) { RpcRequest request = new RpcRequest("togglefavourite", RequestId.TOGGLE_FAVOURITE); return request(request, callback); } public Call toggleWatched(Callback callback) { RpcRequest request = new RpcRequest("togglewatched", RequestId.TOGGLE_WATCHED); return request(request, callback); } /** * Toggle volume mute * @param callback Callback for the request * @return ResponseFuture */ public Call toggleMute(Callback callback) { RpcRequest request = new RpcRequest("togglemute", RequestId.TOGGLE_MUTE); return request(request, callback); } /** * Set volume of video player * @param volume Volume to set * @param callback Callback for the request * @return ResponseFuture */ public Call setVolume(Double volume, Callback callback) { RpcRequest request; if(Version.compare(mVersion, ZERO_VERSION)) { request = new RpcRequest(VOLUME, Arrays.asList(volume), RequestId.SET_VOLUME); } else { request = new RpcRequest("setvolume", Arrays.asList(volume), RequestId.SET_VOLUME); } return request(request, callback); } /** * Get volume of player * @param callback Callback for the request * @return ResponseFuture */ public Call getVolume(Callback callback) { RpcRequest request; if(Version.compare(mVersion, ZERO_VERSION)) { request = new RpcRequest(VOLUME, RequestId.GET_VOLUME); } else { RpcResponse response = new RpcResponse(); LinkedTreeMap<String, Object> map = new LinkedTreeMap<String, Object>(); map.put(VOLUME, 1); response.result = map; callback.onCompleted(null, response); return null; } return request(request, callback); } /** * Filter by genre * @param genre Genre to filter by * @param callback Callback for the request * @return ResponseFuture */ public Call filterGenre(String genre, Callback callback) { RpcRequest request = new RpcRequest("filtergenre", Arrays.asList(genre), RequestId.FILTER_GENRE); return request(request, callback); } /** * Sort items by sorter * @param sorter Type to sort by * @param callback Callback for the request * @return ResponseFuture */ public Call filterSorter(String sorter, Callback callback) { RpcRequest request = new RpcRequest("filtersorter", Arrays.asList(sorter), RequestId.FILTER_SORTER); return request(request, callback); } /** * Filter by type * @param type Type to select * @param callback Callback for the request * @return ResponseFuture */ public Call filterType(String type, Callback callback) { RpcRequest request = new RpcRequest("filtertype", Arrays.asList(type), RequestId.FILTER_TYPE); return request(request, callback); } /** * Filter items by keywords * @param keywords Keywords to search by * @param callback Callback for the request * @return ResponseFuture */ public Call filterSearch(String keywords, Callback callback) { RpcRequest request; request = new RpcRequest("filtersearch", Arrays.asList(keywords), RequestId.FILTER_SEARCH); return request(request, callback); } /** * Clear the search input * @param callback Callback for the request * @return ResponseFuture */ public Call clearSearch(Callback callback) { RpcRequest request = new RpcRequest("clearsearch", RequestId.CLEAR_SEARCH); return request(request, callback); } /** * Seek {seconds} in the video * @param seconds Seconds to seek forward or backward * @param callback Callback for the request * @return ResponseFuture */ public Call seek(Integer seconds, Callback callback) { RpcRequest request; request = new RpcRequest("seek", Arrays.asList(seconds)); request.id = RequestId.SEEK.ordinal(); return request(request, callback); } /** * Get current viewstack of the application * @param callback Callback for the request * @return ResponseFuture */ public Call getViewstack(final Callback callback) { RpcRequest request = new RpcRequest("getviewstack", RequestId.GET_VIEWSTACK); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { return request(request, new Callback() { @Override public void onCompleted(Exception e, RpcResponse result) { try { if (e == null && result != null && result.result != null) { if (result.result instanceof ArrayList) { ArrayList list = (ArrayList) result.result; LinkedTreeMap<String, Object> map = new LinkedTreeMap<String, Object>(); map.put("viewstack", list.get(0)); result.result = map; } } callback.onCompleted(e, result); } catch(Exception exception) { exception.printStackTrace(); } } }); } } /** * Get available subtitles for the current playing or selected video * @param callback Callback for the request * @return ResponseFuture */ public Call getSubtitles(final Callback callback) { RpcRequest request = new RpcRequest("getsubtitles", RequestId.GET_SUBTITLES); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { return request(request, new Callback() { @Override public void onCompleted(Exception e, RpcResponse result) { if (e == null && result != null && result.result != null) { ArrayList list = (ArrayList) result.result; LinkedTreeMap<String, Object> map = new LinkedTreeMap<String, Object>(); map.put("subtitles", list.get(0)); result.result = map; } callback.onCompleted(e, result); } }); } } /** * Get selection in the list browser or the opened detail * @param callback Callback for the request * @return ResponseFuture */ public Call getSelection(final Callback callback) { RpcRequest request = new RpcRequest("getselection", RequestId.GET_SELECTION); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { callback.onCompleted(new UnsupportedOperationException(OLD_API_METHOD_NOT_IMPLEMENTED), null); return null; } } /** * Set selection in the list browser * @param index Index of the item that has to become selected * @param callback Callback for the request * @return ResponseFuture */ public Call setSelection(Integer index, final Callback callback) { RpcRequest request = new RpcRequest("setselection", Arrays.asList(index), RequestId.SET_SELECTION); if(Version.compare(mVersion, "0.3.4")) { return request(request, callback); } else { callback.onCompleted(new UnsupportedOperationException(OLD_API_METHOD_NOT_IMPLEMENTED), null); return null; } } /** * Get the current playing video information * @param callback Callback for the request * @return ResponseFuture */ public Call getPlaying(final Callback callback) { RpcRequest request = new RpcRequest("getplaying", RequestId.GET_PLAYING); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { callback.onCompleted(new UnsupportedOperationException(OLD_API_METHOD_NOT_IMPLEMENTED), null); return null; } } /** * Get the current fullscreen status of the application * @param callback Callback for the request * @return ResponseFuture */ public Call getFullscreen(final Callback callback) { RpcRequest request = new RpcRequest("getfullscreen", RequestId.GET_FULLSCREEN); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { callback.onCompleted(new UnsupportedOperationException(OLD_API_METHOD_NOT_IMPLEMENTED), null); return null; } } /** * Get available media players for the instance * @param callback Callback for the request * @return ResponseFuture */ public Call getPlayers(final Callback callback) { RpcRequest request = new RpcRequest("getplayers", RequestId.GET_PLAYERS); if(Version.compare(mVersion, ZERO_VERSION)) { return request(request, callback); } else { callback.onCompleted(new UnsupportedOperationException(OLD_API_METHOD_NOT_IMPLEMENTED), null); return null; } } /** * Set the media player of instance * @param playerId New id of the player * @param callback Callback for the request * @return ResponseFuture */ public Call setPlayer(String playerId, Callback callback) { RpcRequest request = new RpcRequest("setplayer", Arrays.asList(playerId), RequestId.SET_PLAYER); return request(request, callback); } /** * Set subtitle language of instance * @param subLang New language for subtitles * @param callback Callback for the request * @return ResponseFuture */ public Call setSubtitle(String subLang, Callback callback) { RpcRequest request = new RpcRequest("setsubtitle", Arrays.asList(subLang), RequestId.SET_SUBTITLE); return request(request, callback); } public Call listenNotifications(Callback callback) { RpcRequest request = new RpcRequest("listennotifications", RequestId.LISTENNOTIFICATIONS); return request(request, callback); } /** * Send JSON RPC request to the instance * @param rpc Request data * @param callback Callback for the request * @return ResponseFuture */ private Call request(final RpcRequest rpc, final Callback callback) { RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, mGson.toJson(rpc)); Request request = new Request.Builder() .url(mUrl) .header("Authorization", Credentials.basic(mUsername, mPassword)) .post(requestBody) .build(); Call call = mClient.newCall(request); call.enqueue(new com.squareup.okhttp.Callback() { @Override public void onFailure(Request request, IOException e) { callback.onCompleted(e, null); } @Override public void onResponse(Response response) throws IOException { RpcResponse result = null; Exception e = null; try { if (response != null && response.isSuccessful()) { String responseStr = response.body().string(); //LogUtils.d("PopcornTimeRpcClient", "Response: " + responseStr); result = mGson.fromJson(responseStr, RpcResponse.class); LinkedTreeMap<String, Object> map = result.getMapResult(); if (map.containsKey("popcornVersion")) { mVersion = (String) map.get("popcornVersion"); } } } catch (Exception ex) { ex.printStackTrace(); e = ex; mVersion = ZERO_VERSION; if(rpc.id == RequestId.GET_SELECTION.ordinal()) { mVersion = "0.3.4"; } } callback.onCompleted(e, result); } }); return call; } /** * Check the ip address to start with http// */ private void checkUrl() { if(!mIpAddress.startsWith("http://") && !mIpAddress.startsWith("https://")) { mIpAddress = "http://" + mIpAddress; } if(mIpAddress.endsWith("/")) { mIpAddress = mIpAddress.substring(0, mIpAddress.length() - 2); } } private class RpcRequest { final public String jsonrpc = "2.0"; final public String method; public Object params = new ArrayList(); public Integer id = 10; public RpcRequest(String method) { this.method = method; } public RpcRequest(String method, Object params) { this(method); this.params = params; } public RpcRequest(String method, Object params, RequestId id) { this(method, params); this.id = id.ordinal(); } public RpcRequest(String method, RequestId id) { this(method); this.id = id.ordinal(); } } public class RpcResponse { public Object result; public Integer id; public LinkedTreeMap<String, Object> getMapResult() { if(result instanceof LinkedTreeMap) { return (LinkedTreeMap<String, Object>) result; } return null; } } public interface Callback { public void onCompleted(Exception e, RpcResponse result); } }