Share local files - part 2 (#693)

* Local media files (image, audio, video) can now be shared with Kore. Added token to HttpApp to improve security.

* Added 'Queue on Kodi' share option. 'Play on Kodi' skips the playlist while 'Queue on Kodi' puts the media link at the end of the playlist
This commit is contained in:
Francesco Bonazzi 2019-12-16 19:37:46 +01:00 committed by Synced Synapse
parent 5d0969c66e
commit 37ef130a7c
6 changed files with 275 additions and 41 deletions

View File

@ -29,6 +29,14 @@
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<intent-filter android:label="@string/play_on_kodi">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="audio/*" />
</intent-filter>
<!-- Intent filter for sharing from youtube player -->
<intent-filter android:label="@string/play_on_kodi">
<action android:name="android.intent.action.SEND" />
@ -58,6 +66,43 @@
android:value=".service.HostChooserTargetService" />
</activity>
<activity android:name=".ui.sections.remote.QueueActivity">
<intent-filter android:label="@string/queue_on_kodi">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="audio/*" />
</intent-filter>
<!-- Intent filter for sharing from youtube player -->
<intent-filter android:label="@string/queue_on_kodi">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain"/>
</intent-filter>
<!-- Intent filter for sharing youtube URLs -->
<intent-filter android:label="@string/queue_on_kodi">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtube.com"/>
<data android:host="m.youtube.com"/>
<data android:host="www.youtube.com"/>
<data android:host="vimeo.com"/>
<data android:host="www.vimeo.com"/>
<data android:host="player.vimeo.com"/>
<data android:host="www.svtplay.se"/>
<data android:host="soundcloud.com"/>
<data android:host="m.soundcloud.com"/>
</intent-filter>
</activity>
<activity android:name="org.xbmc.kore.ui.sections.hosts.HostManagerActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.hosts.AddHostActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.hosts.EditHostActivity"/>

View File

@ -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<LocalFileLocation> localFileLocationList = null;
private LinkedList<Uri> 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<String, List<String>> 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;

View File

@ -39,22 +39,26 @@ public class OpenSharedUrl implements Callable<Boolean> {
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<Boolean> {
List<PlayerType.GetActivePlayersReturnType> 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) {

View File

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

View File

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

View File

@ -282,6 +282,7 @@
<string name="add_to_playlist">Add to playlist</string>
<string name="item_added_to_playlist">Added to playlist</string>
<string name="item_sent_to_kodi">Sent to Kodi</string>
<string name="no_suitable_playlist">No suitable playlist found to add media type.</string>
<string name="imdb">IMDb</string>
@ -389,6 +390,7 @@
<!--<string name="purchase_thanks">Thanks for your support!</string>-->
<string name="play_on_kodi">Play on Kodi</string>
<string name="queue_on_kodi">Queue on Kodi</string>
<string name="pause_call_incoming_title">Incoming call</string>
<string name="pause_call_incoming_message">Check your phone, someone is calling you</string>