Refactored share logic to show a notification when an item is added to the playlist (#473)
- added method to HostConnection that returns a Future object instead of taking a callback. - added logic for handling null Handlers in HostConnection methods - added method to HostManager to run a function that takes a HostConnection in a background thread where the Future API results above can be synchronously composed. - replaced chain of callbacks in RemoteActivity with a sequence of future gets in OpenSharedUrl.
This commit is contained in:
parent
5f734bbd5c
commit
1e9160c733
|
@ -22,32 +22,27 @@ import android.database.Cursor;
|
|||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.squareup.okhttp.Interceptor;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.Response;
|
||||
import com.squareup.picasso.OkHttpDownloader;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import org.xbmc.kore.BuildConfig;
|
||||
import org.xbmc.kore.Settings;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Application;
|
||||
import org.xbmc.kore.jsonrpc.method.System;
|
||||
import org.xbmc.kore.jsonrpc.type.ApplicationType;
|
||||
import org.xbmc.kore.provider.MediaContract;
|
||||
import org.xbmc.kore.utils.BasicAuthUrlConnectionDownloader;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
import org.xbmc.kore.utils.NetUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Manages XBMC Hosts
|
||||
|
@ -58,6 +53,21 @@ import java.util.ArrayList;
|
|||
public class HostManager {
|
||||
private static final String TAG = LogUtils.makeLogTag(HostManager.class);
|
||||
|
||||
/**
|
||||
* A block of code that is run in the background thread and receives a
|
||||
* reference to the current host.
|
||||
*
|
||||
* @see #withCurrentHost(Session)
|
||||
*/
|
||||
public interface Session<T> {
|
||||
T using(HostConnection host) throws Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the thread where all sessions are run.
|
||||
*/
|
||||
private static final ExecutorService SESSIONS = Executors.newSingleThreadExecutor();
|
||||
|
||||
// Singleton instance
|
||||
private static volatile HostManager instance = null;
|
||||
|
||||
|
@ -111,6 +121,32 @@ public class HostManager {
|
|||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a session block.
|
||||
* <p>
|
||||
* This method provides a context for awaiting {@link org.xbmc.kore.jsonrpc.ApiFuture
|
||||
* future} objects returned by callback-less remote method invocations. This
|
||||
* enables a more natural style of doing a sequence of remote calls instead
|
||||
* of nesting or chaining callbacks.
|
||||
*
|
||||
* @param session The function to run
|
||||
* @param <T> The type of the value returned by the session
|
||||
* @return a future wrapping the value returned (or exception thrown) by the
|
||||
* session; null when there's no current host.
|
||||
*/
|
||||
public <T> Future<T> withCurrentHost(final Session<T> session) {
|
||||
final HostConnection conn = getConnection();
|
||||
if (conn != null) {
|
||||
return SESSIONS.submit(new Callable<T>() {
|
||||
@Override
|
||||
public T call() throws Exception {
|
||||
return session.using(conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current host list
|
||||
* @return Host list
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package org.xbmc.kore.jsonrpc;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* A Java future wrapping the result of a Kodi remote method call.
|
||||
* <p>
|
||||
* Instantiable only through {@link HostConnection#execute(ApiMethod)}.
|
||||
*
|
||||
* @param <T> The type of the result of the remote method call.
|
||||
*/
|
||||
class ApiFuture<T> implements Future<T> {
|
||||
private enum Status { WAITING, OK, ERROR, CANCELLED }
|
||||
private final Object lock = new Object();
|
||||
private Status status = Status.WAITING;
|
||||
private T ok;
|
||||
private Throwable error;
|
||||
|
||||
static <T> Future<T> from(HostConnection host, ApiMethod<T> method) {
|
||||
final ApiFuture<T> future = new ApiFuture<>();
|
||||
host.execute(method, new ApiCallback<T>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
synchronized (future.lock) {
|
||||
future.ok = result;
|
||||
future.status = Status.OK;
|
||||
future.lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
synchronized (future.lock) {
|
||||
future.error = new ApiException(errorCode, description);
|
||||
future.status = Status.ERROR;
|
||||
future.lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
return future;
|
||||
}
|
||||
|
||||
private ApiFuture() {}
|
||||
|
||||
@Override
|
||||
public T get() throws InterruptedException, ExecutionException {
|
||||
try {
|
||||
return get(0, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("impossible");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(long timeout, @NonNull TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
boolean timed = timeout > 0;
|
||||
long remaining = unit.toNanos(timeout);
|
||||
while (true) synchronized (lock) {
|
||||
switch (status) {
|
||||
case OK: return ok;
|
||||
case ERROR: throw new ExecutionException(error);
|
||||
case CANCELLED: throw new CancellationException();
|
||||
case WAITING:
|
||||
if (timed && remaining <= 0) {
|
||||
throw new TimeoutException();
|
||||
}
|
||||
if (!timed) {
|
||||
lock.wait();
|
||||
} else {
|
||||
long start = System.nanoTime();
|
||||
TimeUnit.NANOSECONDS.timedWait(lock, remaining);
|
||||
remaining -= System.nanoTime() - start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean b) {
|
||||
if (status != Status.WAITING) {
|
||||
return false;
|
||||
}
|
||||
synchronized (lock) {
|
||||
status = Status.CANCELLED;
|
||||
lock.notifyAll();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return status == Status.CANCELLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return status != Status.WAITING;
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import com.squareup.okhttp.RequestBody;
|
|||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.notification.Application;
|
||||
import org.xbmc.kore.jsonrpc.notification.Input;
|
||||
import org.xbmc.kore.jsonrpc.notification.Player;
|
||||
|
@ -48,6 +49,7 @@ import java.net.Socket;
|
|||
import java.util.HashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -285,7 +287,9 @@ public class HostConnection {
|
|||
*
|
||||
* @param method Method object that represents the methood too call
|
||||
* @param callback {@link ApiCallback} to post the response to
|
||||
* @param handler {@link Handler} to invoke callbacks on
|
||||
* @param handler {@link Handler} to invoke callbacks on. When null, the
|
||||
* callbacks are invoked on the same thread as the request.
|
||||
* You cannot do UI manipulation in the callbacks when this is null.
|
||||
* @param <T> Method return type
|
||||
*/
|
||||
public <T> void execute(final ApiMethod<T> method, final ApiCallback<T> callback,
|
||||
|
@ -310,6 +314,34 @@ public class HostConnection {
|
|||
executorService.execute(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the remote method in the background and returns a future that may be
|
||||
* awaited on any thread.
|
||||
* <p>
|
||||
* Not a good idea to await on the UI thread although you can. The app will become
|
||||
* unresponsive until the request is completed if you do. Android can't know that
|
||||
* you're effectively doing a network request on the main thread so it won't stop you.
|
||||
* <p>
|
||||
* This is meant to be used in a background thread for doing requests that depend
|
||||
* on the result of another. Nested callbacks make it hard to follow the logic,
|
||||
* tend to make you repeat error handling at every level, and also slower because
|
||||
* of constant switching between the worker and UI threads.
|
||||
* <p>
|
||||
* If you don't care about the result and just want to fire a request, you could
|
||||
* call this but not call {@link Future#get()} on the result. It's safe to do
|
||||
* in the UI thread but you wouldn't know if an error happened or not.
|
||||
*
|
||||
* @param method The remote method to invoke
|
||||
* @param <T> The type of the return value of the method
|
||||
* @return the future result of the method call. API errors will be wrapped in
|
||||
* an {@link java.util.concurrent.ExecutionException ExecutionException} like
|
||||
* regular futures.
|
||||
* @see org.xbmc.kore.host.HostManager#withCurrentHost(HostManager.Session)
|
||||
*/
|
||||
public <T> Future<T> execute(ApiMethod<T> method) {
|
||||
return ApiFuture.from(this, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the JSON RPC request through HTTP (using OkHttp library)
|
||||
*/
|
||||
|
@ -327,8 +359,8 @@ public class HostConnection {
|
|||
Response response = sendOkHttpRequest(client, request);
|
||||
final T result = method.resultFromJson(parseJsonResponse(handleOkHttpResponse(response)));
|
||||
|
||||
if ((handler != null) && (callback != null)) {
|
||||
handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(result);
|
||||
|
@ -337,8 +369,8 @@ public class HostConnection {
|
|||
}
|
||||
} catch (final ApiException e) {
|
||||
// Got an error, call error handler
|
||||
if ((handler != null) && (callback != null)) {
|
||||
handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(e.getCode(), e.getMessage());
|
||||
|
@ -490,8 +522,8 @@ public class HostConnection {
|
|||
// Check if a method with this id is already running and raise an error if so
|
||||
synchronized (clientCallbacks) {
|
||||
if (clientCallbacks.containsKey(methodId)) {
|
||||
if ((handler != null) && (callback != null)) {
|
||||
handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
|
||||
|
@ -606,7 +638,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onPause(apiNotification);
|
||||
|
@ -618,7 +650,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onPlay(apiNotification);
|
||||
|
@ -630,7 +662,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onSeek(apiNotification);
|
||||
|
@ -642,7 +674,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onSpeedChanged(apiNotification);
|
||||
|
@ -654,7 +686,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onStop(apiNotification);
|
||||
|
@ -666,7 +698,7 @@ public class HostConnection {
|
|||
for (final PlayerNotificationsObserver observer :
|
||||
playerNotificationsObservers.keySet()) {
|
||||
Handler handler = playerNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onPropertyChanged(apiNotification);
|
||||
|
@ -678,7 +710,7 @@ public class HostConnection {
|
|||
for (final SystemNotificationsObserver observer :
|
||||
systemNotificationsObservers.keySet()) {
|
||||
Handler handler = systemNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onQuit(apiNotification);
|
||||
|
@ -690,7 +722,7 @@ public class HostConnection {
|
|||
for (final SystemNotificationsObserver observer :
|
||||
systemNotificationsObservers.keySet()) {
|
||||
Handler handler = systemNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onRestart(apiNotification);
|
||||
|
@ -702,7 +734,7 @@ public class HostConnection {
|
|||
for (final SystemNotificationsObserver observer :
|
||||
systemNotificationsObservers.keySet()) {
|
||||
Handler handler = systemNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onSleep(apiNotification);
|
||||
|
@ -714,7 +746,7 @@ public class HostConnection {
|
|||
for (final InputNotificationsObserver observer :
|
||||
inputNotificationsObservers.keySet()) {
|
||||
Handler handler = inputNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onInputRequested(apiNotification);
|
||||
|
@ -727,7 +759,7 @@ public class HostConnection {
|
|||
for (final ApplicationNotificationsObserver observer :
|
||||
applicationNotificationsObservers.keySet()) {
|
||||
Handler handler = applicationNotificationsObservers.get(observer);
|
||||
handler.post(new Runnable() {
|
||||
postOrRunNow(handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
observer.onVolumeChanged(apiNotification);
|
||||
|
@ -755,8 +787,8 @@ public class HostConnection {
|
|||
@SuppressWarnings("unchecked")
|
||||
final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;
|
||||
|
||||
if ((methodCallInfo.handler != null) && (callback != null)) {
|
||||
methodCallInfo.handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(methodCallInfo.handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(result);
|
||||
|
@ -785,8 +817,8 @@ public class HostConnection {
|
|||
@SuppressWarnings("unchecked")
|
||||
final ApiCallback<T> callback = (ApiCallback<T>) methodCallInfo.callback;
|
||||
|
||||
if ((methodCallInfo.handler != null) && (callback != null)) {
|
||||
methodCallInfo.handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(methodCallInfo.handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(error.getCode(), error.getMessage());
|
||||
|
@ -802,8 +834,8 @@ public class HostConnection {
|
|||
@SuppressWarnings("unchecked")
|
||||
final ApiCallback<T> callback = (ApiCallback<T>)methodCallInfo.callback;
|
||||
|
||||
if ((methodCallInfo.handler != null) && (callback != null)) {
|
||||
methodCallInfo.handler.post(new Runnable() {
|
||||
if (callback != null) {
|
||||
postOrRunNow(methodCallInfo.handler, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(error.getCode(), error.getMessage());
|
||||
|
@ -838,6 +870,14 @@ public class HostConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private static void postOrRunNow(Handler handler, Runnable r) {
|
||||
if (handler != null) {
|
||||
handler.post(r);
|
||||
} else {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to aggregate a method, callback and handler
|
||||
* @param <T>
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package org.xbmc.kore.ui.sections.remote;
|
||||
|
||||
/*
|
||||
* This file is a part of the Kore project.
|
||||
*/
|
||||
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Player;
|
||||
import org.xbmc.kore.jsonrpc.method.Playlist;
|
||||
import org.xbmc.kore.jsonrpc.type.PlayerType;
|
||||
import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Sends a series of commands to Kodi in a background thread to open the video.
|
||||
* <p>
|
||||
* This is meant to be passed to {@link HostManager#withCurrentHost(HostManager.Session)}
|
||||
* and the resulting future should be awaited in a background thread as well (if you're
|
||||
* interested in the result), either in an {@link android.os.AsyncTask} or another
|
||||
* {@link HostManager.Session}.
|
||||
*/
|
||||
public class OpenSharedUrl implements HostManager.Session<Boolean> {
|
||||
|
||||
/**
|
||||
* Indicates the stage where the error happened.
|
||||
*/
|
||||
public static class Error extends Exception {
|
||||
public final int stage;
|
||||
|
||||
public Error(int stage, Throwable cause) {
|
||||
super(cause);
|
||||
this.stage = stage;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = LogUtils.makeLogTag(OpenSharedUrl.class);
|
||||
private final String pluginUrl;
|
||||
private final String notificationTitle;
|
||||
private final String notificationText;
|
||||
|
||||
/**
|
||||
* @param pluginUrl The url to play
|
||||
* @param notificationTitle The title of the notification to be shown when the
|
||||
* host is currently playing a video
|
||||
* @param notificationText The notification to be shown when the host is currently
|
||||
* playing a video
|
||||
*/
|
||||
public OpenSharedUrl(String pluginUrl, String notificationTitle, String notificationText) {
|
||||
this.pluginUrl = pluginUrl;
|
||||
this.notificationTitle = notificationTitle;
|
||||
this.notificationText = notificationText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host The host to send the commands to
|
||||
* @return whether the host is currently playing a video. If so, the shared url
|
||||
* is added to the playlist and not played immediately.
|
||||
* @throws Error when any of the commands sent fails
|
||||
* @throws InterruptedException when {@code cancel(true)} is called on the resulting
|
||||
* future while waiting on one of the internal futures.
|
||||
*/
|
||||
@Override
|
||||
public Boolean using(HostConnection host) throws Error, InterruptedException {
|
||||
int stage = R.string.error_get_active_player;
|
||||
try {
|
||||
List<PlayerType.GetActivePlayersReturnType> players =
|
||||
host.execute(new Player.GetActivePlayers()).get();
|
||||
boolean videoIsPlaying = false;
|
||||
for (PlayerType.GetActivePlayersReturnType player : players) {
|
||||
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) {
|
||||
videoIsPlaying = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stage = R.string.error_queue_media_file;
|
||||
if (!videoIsPlaying) {
|
||||
LogUtils.LOGD(TAG, "Clearing video playlist");
|
||||
host.execute(new Playlist.Clear(PlaylistType.VIDEO_PLAYLISTID)).get();
|
||||
}
|
||||
|
||||
LogUtils.LOGD(TAG, "Queueing file");
|
||||
PlaylistType.Item item = new PlaylistType.Item();
|
||||
item.file = pluginUrl;
|
||||
host.execute(new Playlist.Add(PlaylistType.VIDEO_PLAYLISTID, item)).get();
|
||||
|
||||
if (!videoIsPlaying) {
|
||||
stage = R.string.error_play_media_file;
|
||||
host.execute(new Player
|
||||
.Open(Player.Open.TYPE_PLAYLIST, PlaylistType.VIDEO_PLAYLISTID)).get();
|
||||
} else {
|
||||
// no get() to ignore the exception that will be thrown by OkHttp
|
||||
host.execute(new Player.Notification(notificationTitle, notificationText));
|
||||
}
|
||||
|
||||
return videoIsPlaying;
|
||||
} catch (ExecutionException e) {
|
||||
throw new Error(stage, e.getCause());
|
||||
} catch (RuntimeException e) {
|
||||
throw new Error(stage, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import android.graphics.Point;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.text.TextDirectionHeuristicsCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
|
@ -41,19 +40,15 @@ import org.xbmc.kore.R;
|
|||
import org.xbmc.kore.Settings;
|
||||
import org.xbmc.kore.host.HostConnectionObserver;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Application;
|
||||
import org.xbmc.kore.jsonrpc.method.AudioLibrary;
|
||||
import org.xbmc.kore.jsonrpc.method.GUI;
|
||||
import org.xbmc.kore.jsonrpc.method.Input;
|
||||
import org.xbmc.kore.jsonrpc.method.Player;
|
||||
import org.xbmc.kore.jsonrpc.method.Playlist;
|
||||
import org.xbmc.kore.jsonrpc.method.System;
|
||||
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
|
||||
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.service.ConnectionObserversManagerService;
|
||||
import org.xbmc.kore.ui.BaseActivity;
|
||||
import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
|
||||
|
@ -71,7 +66,8 @@ import org.xbmc.kore.utils.Utils;
|
|||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -104,6 +100,10 @@ public class RemoteActivity extends BaseActivity
|
|||
|
||||
private VolumeKeyActionHandler volumeKeyActionHandler;
|
||||
|
||||
private Future<Boolean> pendingShare;
|
||||
|
||||
private Future<Void> awaitingShare;
|
||||
|
||||
@InjectView(R.id.background_image) ImageView backgroundImage;
|
||||
@InjectView(R.id.pager_indicator) CirclePageIndicator pageIndicator;
|
||||
@InjectView(R.id.pager) ViewPager viewPager;
|
||||
|
@ -162,6 +162,9 @@ public class RemoteActivity extends BaseActivity
|
|||
// // Only set top and right, to allow bottom to overlap in each fragment
|
||||
// UIUtils.setPaddingForSystemBars(this, viewPager, true, true, false);
|
||||
// UIUtils.setPaddingForSystemBars(this, pageIndicator, true, true, false);
|
||||
|
||||
//noinspection unchecked
|
||||
pendingShare = (Future<Boolean>) getLastCustomNonConfigurationInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -206,6 +209,12 @@ public class RemoteActivity extends BaseActivity
|
|||
super.onPause();
|
||||
if (hostConnectionObserver != null) hostConnectionObserver.unregisterPlayerObserver(this);
|
||||
hostConnectionObserver = null;
|
||||
if (awaitingShare != null) awaitingShare.cancel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainCustomNonConfigurationInstance() {
|
||||
return pendingShare;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -343,6 +352,11 @@ public class RemoteActivity extends BaseActivity
|
|||
* @param intent Start intent for the activity
|
||||
*/
|
||||
private void handleStartIntent(Intent intent) {
|
||||
if (pendingShare != null) {
|
||||
awaitShare();
|
||||
return;
|
||||
}
|
||||
|
||||
final String action = intent.getAction();
|
||||
// Check action
|
||||
if ((action == null) ||
|
||||
|
@ -363,113 +377,62 @@ public class RemoteActivity extends BaseActivity
|
|||
videoUrl = videoUri.toString();
|
||||
}
|
||||
|
||||
final String fvideoUrl = videoUrl;
|
||||
|
||||
// Check if any video player is active and clear the playlist before queuing if so
|
||||
final HostConnection connection = hostManager.getConnection();
|
||||
final Handler callbackHandler = new Handler();
|
||||
Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers();
|
||||
getActivePlayers.execute(connection, new ApiCallback<ArrayList<PlayerType.GetActivePlayersReturnType>>() {
|
||||
@Override
|
||||
public void onSuccess(ArrayList<PlayerType.GetActivePlayersReturnType> result) {
|
||||
boolean videoIsPlaying = false;
|
||||
|
||||
for (PlayerType.GetActivePlayersReturnType player : result) {
|
||||
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO))
|
||||
videoIsPlaying = true;
|
||||
}
|
||||
|
||||
if (!videoIsPlaying) {
|
||||
// Clear the playlist
|
||||
clearPlaylistAndQueueFile(fvideoUrl, connection, callbackHandler);
|
||||
} else {
|
||||
queueFile(fvideoUrl, false, connection, callbackHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
LogUtils.LOGD(TAG, "Couldn't get active player when handling start intent.");
|
||||
Toast.makeText(RemoteActivity.this,
|
||||
String.format(getString(R.string.error_get_active_player), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
String title = getString(R.string.app_name);
|
||||
String text = getString(R.string.item_added_to_playlist);
|
||||
pendingShare = hostManager.withCurrentHost(new OpenSharedUrl(videoUrl, title, text));
|
||||
awaitShare();
|
||||
intent.setAction(null);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears Kodi's playlist, queues the given media file and starts the playlist
|
||||
* @param file File to play
|
||||
* @param connection Host connection
|
||||
* @param callbackHandler Handler to use for posting callbacks
|
||||
* Awaits the completion of the share request in the same background thread
|
||||
* where the request is running.
|
||||
* <p>
|
||||
* This needs to run stuff in the UI thread so the activity reference is
|
||||
* inevitable, but unlike the share request this doesn't need to outlive the
|
||||
* activity. The resulting future __must__ be cancelled when the activity is
|
||||
* paused (it will drop itself when cancelled or finished). This should be called
|
||||
* again when the activity is resumed and a {@link #pendingShare} exists.
|
||||
*/
|
||||
private void clearPlaylistAndQueueFile(final String file,
|
||||
final HostConnection connection, final Handler callbackHandler) {
|
||||
LogUtils.LOGD(TAG, "Clearing video playlist");
|
||||
Playlist.Clear action = new Playlist.Clear(PlaylistType.VIDEO_PLAYLISTID);
|
||||
action.execute(connection, new ApiCallback<String>() {
|
||||
private void awaitShare() {
|
||||
awaitingShare = hostManager.withCurrentHost(new HostManager.Session<Void>() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
// Now queue and start the file
|
||||
queueFile(file, true, connection, callbackHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
Toast.makeText(RemoteActivity.this,
|
||||
String.format(getString(R.string.error_queue_media_file), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the given media file and optionally starts the playlist
|
||||
* @param file File to play
|
||||
* @param startPlaylist Whether to start playing the playlist after add
|
||||
* @param connection Host connection
|
||||
* @param callbackHandler Handler to use for posting callbacks
|
||||
*/
|
||||
private void queueFile(final String file, final boolean startPlaylist,
|
||||
final HostConnection connection, final Handler callbackHandler) {
|
||||
LogUtils.LOGD(TAG, "Queing file");
|
||||
PlaylistType.Item item = new PlaylistType.Item();
|
||||
item.file = file;
|
||||
Playlist.Add action = new Playlist.Add(PlaylistType.VIDEO_PLAYLISTID, item);
|
||||
action.execute(connection, new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result ) {
|
||||
if (startPlaylist) {
|
||||
Player.Open action = new Player.Open(Player.Open.TYPE_PLAYLIST, PlaylistType.VIDEO_PLAYLISTID);
|
||||
action.execute(connection, new ApiCallback<String>() {
|
||||
public Void using(HostConnection host) throws Exception {
|
||||
try {
|
||||
final boolean wasAlreadyPlaying = pendingShare.get();
|
||||
pendingShare = null;
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
public void run() {
|
||||
if (wasAlreadyPlaying) {
|
||||
Toast.makeText(RemoteActivity.this,
|
||||
getString(R.string.item_added_to_playlist),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
refreshPlaylist();
|
||||
}
|
||||
|
||||
});
|
||||
} catch (InterruptedException ignored) {
|
||||
} catch (ExecutionException ex) {
|
||||
pendingShare = null;
|
||||
final OpenSharedUrl.Error e = (OpenSharedUrl.Error) ex.getCause();
|
||||
LogUtils.LOGE(TAG, "Share failed", e);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
public void run() {
|
||||
Toast.makeText(RemoteActivity.this,
|
||||
String.format(getString(R.string.error_play_media_file), description),
|
||||
getString(e.stage, e.getMessage()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
});
|
||||
} finally {
|
||||
awaitingShare = null;
|
||||
}
|
||||
|
||||
refreshPlaylist();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
Toast.makeText(RemoteActivity.this,
|
||||
String.format(getString(R.string.error_queue_media_file), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the YouTube Uri that the YouTube app passes in EXTRA_TEXT
|
||||
* YouTube sends something like: [Video title]: [YouTube URL] so we need
|
||||
|
|
Loading…
Reference in New Issue