Skip to content

Commit

Permalink
Simplify sharing intent handling
Browse files Browse the repository at this point in the history
Redesign ApiFuture to be more generic and independent of other classes
  • Loading branch information
SyncedSynapse authored and poisdeux committed Apr 24, 2018
1 parent 1d6f9c2 commit 75f8326
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 106 deletions.
45 changes: 0 additions & 45 deletions app/src/main/java/org/xbmc/kore/host/HostManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@

import java.io.File;
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
Expand All @@ -53,21 +49,6 @@
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;

Expand Down Expand Up @@ -121,32 +102,6 @@ public static HostManager getInstance(Context context) {
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
Expand Down
75 changes: 39 additions & 36 deletions app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import java.util.concurrent.TimeoutException;

/**
* A Java future wrapping the result of a Kodi remote method call.
* A Java future implementation, with explicit methods to complete the Future
* <p>
* Instantiable only through {@link HostConnection#execute(ApiMethod)}.
* Don't forget that a call to {@link ApiFuture#get()} blocks the current
* thread until it's unblocked by {@link ApiFuture#cancel(boolean)},
* {@link ApiFuture#complete(Object)} or {@link ApiFuture#completeExceptionally(Throwable)}
*
* @param <T> The type of the result of the remote method call.
* @param <T> The type of the result returned by {@link ApiFuture#get()}
*/
class ApiFuture<T> implements Future<T> {
private enum Status { WAITING, OK, ERROR, CANCELLED }
Expand All @@ -22,38 +24,14 @@ private enum Status { WAITING, OK, ERROR, CANCELLED }
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() {}
ApiFuture() {}

@Override
public T get() throws InterruptedException, ExecutionException {
try {
return get(0, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("impossible");
throw new IllegalStateException("Request timed out. This should not happen when time out is disabled!");
}
}

Expand Down Expand Up @@ -83,18 +61,26 @@ public T get(long timeout, @NonNull TimeUnit unit)
}
}

@Override
public boolean cancel(boolean b) {
if (status != Status.WAITING) {
return false;
}
private boolean setResultAndNotify(Status status, T ok, Throwable error) {
synchronized (lock) {
status = Status.CANCELLED;
lock.notifyAll();
if (this.status != Status.WAITING) {
return false;
}

this.status = status;
if (status == Status.OK) this.ok = ok;
if (status == Status.ERROR) this.error = error;

this.lock.notifyAll();
return true;
}
}

@Override
public boolean cancel(boolean b) {
return setResultAndNotify(Status.CANCELLED, null, null);
}

@Override
public boolean isCancelled() {
return status == Status.CANCELLED;
Expand All @@ -105,4 +91,21 @@ public boolean isDone() {
return status != Status.WAITING;
}

/**
* If not already completed, sets the value returned by get() to the given value.
* @param value - the result value
* @return true if this invocation caused this CompletableFuture to transition to a completed state, else false
*/
public boolean complete(T value) {
return setResultAndNotify(Status.OK, value, null);
}

/**
* If not already completed, causes invocations of get() to throw the given exception.
* @param ex = the exception
* @return true if this invocation caused this CompletableFuture to transition to a completed state, else false
*/
public boolean completeExceptionally(Throwable ex) {
return setResultAndNotify(Status.ERROR, null, ex);
}
}
16 changes: 13 additions & 3 deletions app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
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;
Expand Down Expand Up @@ -354,10 +353,21 @@ public void run() {
* @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);
final ApiFuture<T> future = new ApiFuture<>();
execute(method, new ApiCallback<T>() {
@Override
public void onSuccess(T result) {
future.complete(result);
}

@Override
public void onError(int errorCode, String description) {
future.completeExceptionally(new ApiException(errorCode, description));
}
}, null);
return future;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

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;
Expand All @@ -14,17 +13,17 @@
import org.xbmc.kore.utils.LogUtils;

import java.util.List;
import java.util.concurrent.Callable;
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)}
* This is meant to be passed to {@link java.util.concurrent.Executor}
* 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}.
* interested in the result).
*/
public class OpenSharedUrl implements HostManager.Session<Boolean> {
public class OpenSharedUrl implements Callable<Boolean> {

/**
* Indicates the stage where the error happened.
Expand All @@ -39,6 +38,7 @@ public Error(int stage, Throwable cause) {
}

private static final String TAG = LogUtils.makeLogTag(OpenSharedUrl.class);
private final HostConnection hostConnection;
private final String pluginUrl;
private final String notificationTitle;
private final String notificationText;
Expand All @@ -50,26 +50,27 @@ public Error(int stage, Throwable cause) {
* @param notificationText The notification to be shown when the host is currently
* playing a video
*/
public OpenSharedUrl(String pluginUrl, String notificationTitle, String notificationText) {
public OpenSharedUrl(HostConnection hostConnection, String pluginUrl, String notificationTitle, String notificationText) {
this.hostConnection = hostConnection;
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 {
public Boolean call() throws Error, InterruptedException {
int stage = R.string.error_get_active_player;
try {
List<PlayerType.GetActivePlayersReturnType> players =
host.execute(new Player.GetActivePlayers()).get();
hostConnection.execute(new Player.GetActivePlayers())
.get();
boolean videoIsPlaying = false;
for (PlayerType.GetActivePlayersReturnType player : players) {
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) {
Expand All @@ -81,21 +82,23 @@ public Boolean using(HostConnection host) throws Error, InterruptedException {
stage = R.string.error_queue_media_file;
if (!videoIsPlaying) {
LogUtils.LOGD(TAG, "Clearing video playlist");
host.execute(new Playlist.Clear(PlaylistType.VIDEO_PLAYLISTID)).get();
hostConnection.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();
hostConnection.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();
hostConnection.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));
hostConnection.execute(new Player.Notification(notificationTitle, notificationText));
}

return videoIsPlaying;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostConnectionObserver;
import org.xbmc.kore.host.HostManager;
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;
Expand All @@ -64,7 +63,10 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
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.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -340,6 +342,17 @@ private void setToolbarTitle(Toolbar toolbar, int position) {
}
}

/**
* Provides the thread where the intent will be handled
*/
private static ExecutorService SHARE_EXECUTOR = null;
private static ExecutorService getShareExecutor() {
if (SHARE_EXECUTOR == null) {
SHARE_EXECUTOR = Executors.newSingleThreadExecutor();
}
return SHARE_EXECUTOR;
}

/**
* Handles the intent that started this activity, namely to start playing something on Kodi
* @param intent Start intent for the activity
Expand Down Expand Up @@ -372,7 +385,7 @@ private void handleStartIntent(Intent intent) {

String title = getString(R.string.app_name);
String text = getString(R.string.item_added_to_playlist);
pendingShare = hostManager.withCurrentHost(new OpenSharedUrl(videoUrl, title, text));
pendingShare = getShareExecutor().submit(new OpenSharedUrl(hostManager.getConnection(), videoUrl, title, text));
awaitShare();
intent.setAction(null);
}
Expand All @@ -388,9 +401,9 @@ private void handleStartIntent(Intent intent) {
* again when the activity is resumed and a {@link #pendingShare} exists.
*/
private void awaitShare() {
awaitingShare = hostManager.withCurrentHost(new HostManager.Session<Void>() {
awaitingShare = getShareExecutor().submit(new Callable<Void>() {
@Override
public Void using(HostConnection host) throws Exception {
public Void call() throws Exception {
try {
final boolean wasAlreadyPlaying = pendingShare.get();
pendingShare = null;
Expand All @@ -399,8 +412,9 @@ public Void using(HostConnection host) throws Exception {
public void run() {
if (wasAlreadyPlaying) {
Toast.makeText(RemoteActivity.this,
getString(R.string.item_added_to_playlist),
Toast.LENGTH_SHORT).show();
getString(R.string.item_added_to_playlist),
Toast.LENGTH_SHORT)
.show();
}
refreshPlaylist();
}
Expand All @@ -414,8 +428,8 @@ public void run() {
@Override
public void run() {
Toast.makeText(RemoteActivity.this,
getString(e.stage, e.getMessage()),
Toast.LENGTH_SHORT).show();
getString(e.stage, e.getMessage()),
Toast.LENGTH_SHORT).show();
}
});
} finally {
Expand Down

0 comments on commit 75f8326

Please sign in to comment.