From 1e9160c7334534d3522f4124fbde1b2c98aa4ec1 Mon Sep 17 00:00:00 2001 From: Mon Zafra Date: Fri, 26 Jan 2018 22:03:30 +0800 Subject: [PATCH] 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. --- .../java/org/xbmc/kore/host/HostManager.java | 54 +++++- .../java/org/xbmc/kore/jsonrpc/ApiFuture.java | 108 ++++++++++++ .../org/xbmc/kore/jsonrpc/HostConnection.java | 88 +++++++--- .../ui/sections/remote/OpenSharedUrl.java | 108 ++++++++++++ .../ui/sections/remote/RemoteActivity.java | 157 +++++++----------- 5 files changed, 385 insertions(+), 130 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/remote/OpenSharedUrl.java diff --git a/app/src/main/java/org/xbmc/kore/host/HostManager.java b/app/src/main/java/org/xbmc/kore/host/HostManager.java index e79e78c..0facaef 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostManager.java +++ b/app/src/main/java/org/xbmc/kore/host/HostManager.java @@ -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 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. + *

+ * 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 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 Future withCurrentHost(final Session session) { + final HostConnection conn = getConnection(); + if (conn != null) { + return SESSIONS.submit(new Callable() { + @Override + public T call() throws Exception { + return session.using(conn); + } + }); + } + return null; + } + /** * Returns the current host list * @return Host list diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java new file mode 100644 index 0000000..04fb1b3 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java @@ -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. + *

+ * Instantiable only through {@link HostConnection#execute(ApiMethod)}. + * + * @param The type of the result of the remote method call. + */ +class ApiFuture implements Future { + 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 Future from(HostConnection host, ApiMethod method) { + final ApiFuture future = new ApiFuture<>(); + host.execute(method, new ApiCallback() { + @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; + } + +} diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java index 0d86fe5..cb9dccf 100644 --- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java +++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java @@ -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 Method return type */ public void execute(final ApiMethod method, final ApiCallback 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 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 Future execute(ApiMethod 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 callback = (ApiCallback) 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 callback = (ApiCallback) 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 callback = (ApiCallback)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 diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/OpenSharedUrl.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/OpenSharedUrl.java new file mode 100644 index 0000000..2101b29 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/OpenSharedUrl.java @@ -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. + *

+ * 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 { + + /** + * 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 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); + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java index 6b228f7..618f5ec 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteActivity.java @@ -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 pendingShare; + + private Future 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) 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>() { - @Override - public void onSuccess(ArrayList 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. + *

+ * 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() { + private void awaitShare() { + awaitingShare = hostManager.withCurrentHost(new HostManager.Session() { @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() { - @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() { + 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