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:
Mon Zafra 2018-01-26 22:03:30 +08:00 committed by Synced Synapse
parent 5f734bbd5c
commit 1e9160c733
5 changed files with 385 additions and 130 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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