diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d1a9994..008dbac 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,6 +29,14 @@
+
+
+
+
+
+
+
+
@@ -58,6 +66,43 @@
android:value=".service.HostChooserTargetService" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/localfile/HttpApp.java b/app/src/main/java/org/xbmc/kore/ui/sections/localfile/HttpApp.java
index 49ef0a8..0c6b77c 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/localfile/HttpApp.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/localfile/HttpApp.java
@@ -2,7 +2,11 @@ package org.xbmc.kore.ui.sections.localfile;
import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
import android.net.wifi.WifiManager;
+import android.provider.OpenableColumns;
+import android.webkit.MimeTypeMap;
import org.xbmc.kore.utils.LogUtils;
@@ -12,6 +16,7 @@ import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -27,36 +32,78 @@ public class HttpApp extends NanoHTTPD {
super(port);
this.applicationContext = applicationContext;
this.localFileLocationList = new LinkedList<>();
+ this.localUriList = new LinkedList<>();
+ this.token = generateToken();
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
+ private final int TOKEN_LENGTH = 12;
+
+ private String generateToken() {
+ String token = "";
+
+ SecureRandom sr = new SecureRandom();
+
+ for (int i = 0; i < TOKEN_LENGTH; i++) {
+ int n = sr.nextInt(26*2 + 10);
+ if (n < 26) {
+ n += 'A';
+ } else if (n < 26*2) {
+ n += 'a' - 26;
+ } else {
+ n += '0' - 26*2;
+ }
+ token += Character.toString((char) n);
+ }
+
+ return token;
+ }
+
private Context applicationContext;
private LinkedList localFileLocationList = null;
+ private LinkedList localUriList = null;
private int currentIndex;
+ private boolean currentIsFile;
+ private String token;
+
+ private final Response forbidden = newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
@Override
public Response serve(IHTTPSession session) {
Map> params = session.getParameters();
if (localFileLocationList == null) {
- return newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
+ return forbidden;
}
- if (!params.containsKey("number")) {
- return null;
+ if (!params.containsKey("token")) {
+ return forbidden;
+ }
+ if (!params.get("token").get(0).equals(this.token)) {
+ return forbidden;
}
-
- int file_number = Integer.parseInt(params.get("number").get(0));
FileInputStream fis = null;
- LocalFileLocation localFileLocation = localFileLocationList.get(file_number);
+ String mimeType = null;
try {
- fis = new FileInputStream(localFileLocation.fullPath);
+ if (params.containsKey("number")) {
+ int file_number = Integer.parseInt(params.get("number").get(0));
+
+ LocalFileLocation localFileLocation = localFileLocationList.get(file_number);
+ fis = new FileInputStream(localFileLocation.fullPath);
+ mimeType = localFileLocation.getMimeType();
+ } else if (params.containsKey("uri")) {
+ int uri_number = Integer.parseInt(params.get("uri").get(0));
+
+ fis = (FileInputStream) applicationContext.getContentResolver().openInputStream(localUriList.get(uri_number));
+ } else {
+ return forbidden;
+ }
} catch (FileNotFoundException e) {
LogUtils.LOGW(LogUtils.makeLogTag(HttpApp.class), e.toString());
- return newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
+ return forbidden;
}
- String mimeType = localFileLocation.getMimeType();
+
return newChunkedResponse(Response.Status.OK, mimeType, fis);
}
@@ -68,6 +115,17 @@ public class HttpApp extends NanoHTTPD {
this.localFileLocationList.add(localFileLocation);
currentIndex = localFileLocationList.size() - 1;
}
+ currentIsFile = true;
+ }
+
+ public void addUri(Uri uri) {
+ if (localUriList.contains(uri)) {
+ currentIndex = localUriList.indexOf(uri);
+ } else {
+ this.localUriList.add(uri);
+ currentIndex = localUriList.size() - 1;
+ }
+ currentIsFile = false;
}
private String getIpAddress() throws UnknownHostException {
@@ -100,7 +158,44 @@ public class HttpApp extends NanoHTTPD {
} catch (IOException ioe) {
LogUtils.LOGE(LogUtils.makeLogTag(HttpApp.class), ioe.getMessage());
}
- return "http://" + ip + ":" + getListeningPort() + "/" + localFileLocationList.get(currentIndex).fileName + "?number=" + currentIndex;
+ String path = null;
+ if (currentIsFile) {
+ path = localFileLocationList.get(currentIndex).fileName + "?number=" + currentIndex;
+ } else {
+ Uri uri = localUriList.get(currentIndex);
+ String filename = getFileNameFromUri(uri);
+ path = filename + "?uri=" + currentIndex;
+ }
+ return "http://" + ip + ":" + getListeningPort() + "/" + path + "&token=" + token;
+ }
+
+ private String getFileNameFromUri(Uri contentUri) {
+ String fileName = "";
+ // Let's parse the Uri to detect the filename:
+ if (contentUri.toString().startsWith("content://")) {
+ Cursor cursor = null;
+ try {
+ cursor = applicationContext.getContentResolver().query(contentUri, null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ // If the mimeType determined by Andoid is not equal to the one determined by
+ // the filename, add an extra extension to make sure Kodi recognizes the file type:
+ String mimeType = applicationContext.getContentResolver().getType(contentUri);
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String extensionFromFilename = mimeTypeMap.getMimeTypeFromExtension(
+ MimeTypeMap.getFileExtensionFromUrl(fileName));
+ if (
+ (extensionFromFilename == null) || (!extensionFromFilename.equals(mimeType))
+ ) {
+ fileName += "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ }
+ return fileName;
}
private static HttpApp http_app = null;
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
index c015f06..f39a26e 100644
--- 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
@@ -39,22 +39,26 @@ public class OpenSharedUrl implements Callable {
private static final String TAG = LogUtils.makeLogTag(OpenSharedUrl.class);
private final HostConnection hostConnection;
- private final String pluginUrl;
+ private final String url;
private final String notificationTitle;
private final String notificationText;
+ private boolean queue;
+ private int playlistType;
/**
- * @param pluginUrl The url to play
+ * @param url 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(HostConnection hostConnection, String pluginUrl, String notificationTitle, String notificationText) {
+ public OpenSharedUrl(HostConnection hostConnection, String url, String notificationTitle, String notificationText, boolean queue, int playlistType) {
this.hostConnection = hostConnection;
- this.pluginUrl = pluginUrl;
+ this.url = url;
this.notificationTitle = notificationTitle;
this.notificationText = notificationText;
+ this.queue = queue;
+ this.playlistType = playlistType;
}
/**
@@ -71,37 +75,46 @@ public class OpenSharedUrl implements Callable {
List players =
hostConnection.execute(new Player.GetActivePlayers())
.get();
- boolean videoIsPlaying = false;
+ boolean mediaIsPlaying = false;
for (PlayerType.GetActivePlayersReturnType player : players) {
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) {
- videoIsPlaying = true;
+ mediaIsPlaying = true;
break;
}
}
stage = R.string.error_queue_media_file;
- if (!videoIsPlaying) {
- LogUtils.LOGD(TAG, "Clearing video playlist");
- hostConnection.execute(new Playlist.Clear(PlaylistType.VIDEO_PLAYLISTID))
+ if (!mediaIsPlaying) {
+ LogUtils.LOGD(TAG, "Clearing playlist number " + playlistType);
+ hostConnection.execute(new Playlist.Clear(playlistType))
.get();
}
- LogUtils.LOGD(TAG, "Queueing file");
- PlaylistType.Item item = new PlaylistType.Item();
- item.file = pluginUrl;
- hostConnection.execute(new Playlist.Add(PlaylistType.VIDEO_PLAYLISTID, item))
- .get();
+ if (queue) {
+ // Queue media file to playlist:
- if (!videoIsPlaying) {
- stage = R.string.error_play_media_file;
- hostConnection.execute(new Player.Open(Player.Open.TYPE_PLAYLIST, PlaylistType.VIDEO_PLAYLISTID))
- .get();
+ LogUtils.LOGD(TAG, "Queueing file");
+ PlaylistType.Item item = new PlaylistType.Item();
+ item.file = url;
+ hostConnection.execute(new Playlist.Add(playlistType, item))
+ .get();
+
+ if (!mediaIsPlaying) {
+ stage = R.string.error_play_media_file;
+ hostConnection.execute(new Player.Open(Player.Open.TYPE_PLAYLIST, playlistType))
+ .get();
+ } else {
+ // no get() to ignore the exception that will be thrown by OkHttp
+ hostConnection.execute(new Player.Notification(notificationTitle, notificationText));
+ }
} else {
- // no get() to ignore the exception that will be thrown by OkHttp
- hostConnection.execute(new Player.Notification(notificationTitle, notificationText));
+ // Don't queue, just play the media file directly:
+ PlaylistType.Item item = new PlaylistType.Item();
+ item.file = url;
+ hostConnection.execute(new Player.Open(item));
}
- return videoIsPlaying;
+ return mediaIsPlaying;
} catch (ExecutionException e) {
throw new Error(stage, e.getCause());
} catch (RuntimeException e) {
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/remote/QueueActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/remote/QueueActivity.java
new file mode 100644
index 0000000..1b9c8cc
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/remote/QueueActivity.java
@@ -0,0 +1,12 @@
+package org.xbmc.kore.ui.sections.remote;
+
+import android.content.Intent;
+
+public class QueueActivity extends RemoteActivity {
+
+ @Override
+ protected void handleStartIntent(Intent intent) {
+ handleStartIntent(intent, true);
+ }
+
+}
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 fc6300b..f641389 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
@@ -49,18 +49,21 @@ 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;
import org.xbmc.kore.ui.generic.SendTextDialogFragment;
import org.xbmc.kore.ui.generic.VolumeControllerDialogFragmentListener;
import org.xbmc.kore.ui.sections.hosts.AddHostActivity;
+import org.xbmc.kore.ui.sections.localfile.HttpApp;
import org.xbmc.kore.ui.views.CirclePageIndicator;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
@@ -362,9 +365,13 @@ public class RemoteActivity extends BaseActivity
* Handles the intent that started this activity, namely to start playing something on Kodi
* @param intent Start intent for the activity
*/
- private void handleStartIntent(Intent intent) {
+ protected void handleStartIntent(Intent intent) {
+ handleStartIntent(intent, false);
+ }
+
+ protected void handleStartIntent(Intent intent, boolean queue) {
if (pendingShare != null) {
- awaitShare();
+ awaitShare(queue);
return;
}
@@ -381,11 +388,16 @@ public class RemoteActivity extends BaseActivity
} else {
videoUri = intent.getData();
}
- if (videoUri == null) return;
- String videoUrl = toPluginUrl(videoUri);
- if (videoUrl == null) {
- videoUrl = videoUri.toString();
+ String url;
+ if (videoUri == null) {
+ url = getShareLocalUri(intent);
+ } else {
+ url = toPluginUrl(videoUri);
+ }
+
+ if (url == null) {
+ url = videoUri.toString();
}
// If a host was passed from the intent use it
@@ -400,11 +412,60 @@ public class RemoteActivity extends BaseActivity
}
}
+ // Determine which playlist to use
+ String intentType = intent.getType();
+ int playlistType;
+ if (intentType == null) {
+ playlistType = PlaylistType.VIDEO_PLAYLISTID;
+ } else {
+ if (intentType.matches("audio.*")) {
+ playlistType = PlaylistType.MUSIC_PLAYLISTID;
+ } else if (intentType.matches("video.*")) {
+ playlistType = PlaylistType.VIDEO_PLAYLISTID;
+ } else if (intentType.matches("image.*")) {
+ playlistType = PlaylistType.PICTURE_PLAYLISTID;
+ } else {
+ // Generic links? Default to video:
+ playlistType = PlaylistType.VIDEO_PLAYLISTID;
+ }
+ }
+
String title = getString(R.string.app_name);
String text = getString(R.string.item_added_to_playlist);
- pendingShare = getShareExecutor().submit(new OpenSharedUrl(hostManager.getConnection(), videoUrl, title, text));
- awaitShare();
+ pendingShare = getShareExecutor().submit(
+ new OpenSharedUrl(hostManager.getConnection(), url, title, text, queue, playlistType));
+
+ awaitShare(queue);
intent.setAction(null);
+
+ // Don't display Kore after sharing content from another app:
+ finish();
+ }
+
+ private String getShareLocalUri(Intent intent) {
+ Uri contentUri = intent.getData();
+
+ if (contentUri == null) {
+ Bundle bundle = intent.getExtras();
+ contentUri = (Uri) bundle.get(Intent.EXTRA_STREAM);
+ }
+ if (contentUri == null) {
+ return null;
+ }
+
+ HttpApp http_app = null;
+ try {
+ http_app = HttpApp.getInstance(getApplicationContext(), 8080);
+ } catch (IOException ioe) {
+ Toast.makeText(getApplicationContext(),
+ getString(R.string.error_starting_http_server),
+ Toast.LENGTH_LONG).show();
+ return null;
+ }
+ http_app.addUri(contentUri);
+ String url = http_app.getLinkToFile();
+
+ return url;
}
/**
@@ -417,7 +478,7 @@ public class RemoteActivity extends BaseActivity
* 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 awaitShare() {
+ private void awaitShare(final boolean queue) {
awaitingShare = getShareExecutor().submit(new Callable() {
@Override
public Void call() throws Exception {
@@ -428,8 +489,14 @@ public class RemoteActivity extends BaseActivity
@Override
public void run() {
if (wasAlreadyPlaying) {
+ String msg;
+ if (queue) {
+ msg = getString(R.string.item_added_to_playlist);
+ } else {
+ msg = getString(R.string.item_sent_to_kodi);
+ }
Toast.makeText(RemoteActivity.this,
- getString(R.string.item_added_to_playlist),
+ msg,
Toast.LENGTH_SHORT)
.show();
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8e258d2..af71674 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -282,6 +282,7 @@
Add to playlist
Added to playlist
+ Sent to Kodi
No suitable playlist found to add media type.
IMDb
@@ -389,6 +390,7 @@
Play on Kodi
+ Queue on Kodi
Incoming call
Check your phone, someone is calling you