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" /> <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter> </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 for sharing from youtube player -->
<intent-filter android:label="@string/play_on_kodi"> <intent-filter android:label="@string/play_on_kodi">
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -58,6 +66,43 @@
android:value=".service.HostChooserTargetService" /> android:value=".service.HostChooserTargetService" />
</activity> </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.HostManagerActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.hosts.AddHostActivity"/> <activity android:name="org.xbmc.kore.ui.sections.hosts.AddHostActivity"/>
<activity android:name="org.xbmc.kore.ui.sections.hosts.EditHostActivity"/> <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.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.provider.OpenableColumns;
import android.webkit.MimeTypeMap;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
@ -12,6 +16,7 @@ import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -27,36 +32,78 @@ public class HttpApp extends NanoHTTPD {
super(port); super(port);
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.localFileLocationList = new LinkedList<>(); this.localFileLocationList = new LinkedList<>();
this.localUriList = new LinkedList<>();
this.token = generateToken();
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 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 Context applicationContext;
private LinkedList<LocalFileLocation> localFileLocationList = null; private LinkedList<LocalFileLocation> localFileLocationList = null;
private LinkedList<Uri> localUriList = null;
private int currentIndex; private int currentIndex;
private boolean currentIsFile;
private String token;
private final Response forbidden = newFixedLengthResponse(Response.Status.FORBIDDEN, "", "");
@Override @Override
public Response serve(IHTTPSession session) { public Response serve(IHTTPSession session) {
Map<String, List<String>> params = session.getParameters(); Map<String, List<String>> params = session.getParameters();
if (localFileLocationList == null) { if (localFileLocationList == null) {
return newFixedLengthResponse(Response.Status.FORBIDDEN, "", ""); return forbidden;
} }
if (!params.containsKey("number")) { if (!params.containsKey("token")) {
return null; 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; FileInputStream fis = null;
LocalFileLocation localFileLocation = localFileLocationList.get(file_number); String mimeType = null;
try { 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) { } catch (FileNotFoundException e) {
LogUtils.LOGW(LogUtils.makeLogTag(HttpApp.class), e.toString()); 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); return newChunkedResponse(Response.Status.OK, mimeType, fis);
} }
@ -68,6 +115,17 @@ public class HttpApp extends NanoHTTPD {
this.localFileLocationList.add(localFileLocation); this.localFileLocationList.add(localFileLocation);
currentIndex = localFileLocationList.size() - 1; 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 { private String getIpAddress() throws UnknownHostException {
@ -100,7 +158,44 @@ public class HttpApp extends NanoHTTPD {
} catch (IOException ioe) { } catch (IOException ioe) {
LogUtils.LOGE(LogUtils.makeLogTag(HttpApp.class), ioe.getMessage()); 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; 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 static final String TAG = LogUtils.makeLogTag(OpenSharedUrl.class);
private final HostConnection hostConnection; private final HostConnection hostConnection;
private final String pluginUrl; private final String url;
private final String notificationTitle; private final String notificationTitle;
private final String notificationText; 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 * @param notificationTitle The title of the notification to be shown when the
* host is currently playing a video * host is currently playing a video
* @param notificationText The notification to be shown when the host is currently * @param notificationText The notification to be shown when the host is currently
* playing a video * 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.hostConnection = hostConnection;
this.pluginUrl = pluginUrl; this.url = url;
this.notificationTitle = notificationTitle; this.notificationTitle = notificationTitle;
this.notificationText = notificationText; this.notificationText = notificationText;
this.queue = queue;
this.playlistType = playlistType;
} }
/** /**
@ -71,37 +75,46 @@ public class OpenSharedUrl implements Callable<Boolean> {
List<PlayerType.GetActivePlayersReturnType> players = List<PlayerType.GetActivePlayersReturnType> players =
hostConnection.execute(new Player.GetActivePlayers()) hostConnection.execute(new Player.GetActivePlayers())
.get(); .get();
boolean videoIsPlaying = false; boolean mediaIsPlaying = false;
for (PlayerType.GetActivePlayersReturnType player : players) { for (PlayerType.GetActivePlayersReturnType player : players) {
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) { if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) {
videoIsPlaying = true; mediaIsPlaying = true;
break; break;
} }
} }
stage = R.string.error_queue_media_file; stage = R.string.error_queue_media_file;
if (!videoIsPlaying) { if (!mediaIsPlaying) {
LogUtils.LOGD(TAG, "Clearing video playlist"); LogUtils.LOGD(TAG, "Clearing playlist number " + playlistType);
hostConnection.execute(new Playlist.Clear(PlaylistType.VIDEO_PLAYLISTID)) hostConnection.execute(new Playlist.Clear(playlistType))
.get(); .get();
} }
LogUtils.LOGD(TAG, "Queueing file"); if (queue) {
PlaylistType.Item item = new PlaylistType.Item(); // Queue media file to playlist:
item.file = pluginUrl;
hostConnection.execute(new Playlist.Add(PlaylistType.VIDEO_PLAYLISTID, item))
.get();
if (!videoIsPlaying) { LogUtils.LOGD(TAG, "Queueing file");
stage = R.string.error_play_media_file; PlaylistType.Item item = new PlaylistType.Item();
hostConnection.execute(new Player.Open(Player.Open.TYPE_PLAYLIST, PlaylistType.VIDEO_PLAYLISTID)) item.file = url;
.get(); 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 { } else {
// no get() to ignore the exception that will be thrown by OkHttp // Don't queue, just play the media file directly:
hostConnection.execute(new Player.Notification(notificationTitle, notificationText)); PlaylistType.Item item = new PlaylistType.Item();
item.file = url;
hostConnection.execute(new Player.Open(item));
} }
return videoIsPlaying; return mediaIsPlaying;
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new Error(stage, e.getCause()); throw new Error(stage, e.getCause());
} catch (RuntimeException e) { } 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.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.ListType; import org.xbmc.kore.jsonrpc.type.ListType;
import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.jsonrpc.type.PlayerType;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.service.ConnectionObserversManagerService; import org.xbmc.kore.service.ConnectionObserversManagerService;
import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.BaseActivity;
import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
import org.xbmc.kore.ui.generic.SendTextDialogFragment; import org.xbmc.kore.ui.generic.SendTextDialogFragment;
import org.xbmc.kore.ui.generic.VolumeControllerDialogFragmentListener; import org.xbmc.kore.ui.generic.VolumeControllerDialogFragmentListener;
import org.xbmc.kore.ui.sections.hosts.AddHostActivity; 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.ui.views.CirclePageIndicator;
import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter; import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils; import org.xbmc.kore.utils.Utils;
import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; 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 * Handles the intent that started this activity, namely to start playing something on Kodi
* @param intent Start intent for the activity * @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) { if (pendingShare != null) {
awaitShare(); awaitShare(queue);
return; return;
} }
@ -381,11 +388,16 @@ public class RemoteActivity extends BaseActivity
} else { } else {
videoUri = intent.getData(); videoUri = intent.getData();
} }
if (videoUri == null) return;
String videoUrl = toPluginUrl(videoUri); String url;
if (videoUrl == null) { if (videoUri == null) {
videoUrl = videoUri.toString(); url = getShareLocalUri(intent);
} else {
url = toPluginUrl(videoUri);
}
if (url == null) {
url = videoUri.toString();
} }
// If a host was passed from the intent use it // 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 title = getString(R.string.app_name);
String text = getString(R.string.item_added_to_playlist); String text = getString(R.string.item_added_to_playlist);
pendingShare = getShareExecutor().submit(new OpenSharedUrl(hostManager.getConnection(), videoUrl, title, text)); pendingShare = getShareExecutor().submit(
awaitShare(); new OpenSharedUrl(hostManager.getConnection(), url, title, text, queue, playlistType));
awaitShare(queue);
intent.setAction(null); 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 * paused (it will drop itself when cancelled or finished). This should be called
* again when the activity is resumed and a {@link #pendingShare} exists. * 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>() { awaitingShare = getShareExecutor().submit(new Callable<Void>() {
@Override @Override
public Void call() throws Exception { public Void call() throws Exception {
@ -428,8 +489,14 @@ public class RemoteActivity extends BaseActivity
@Override @Override
public void run() { public void run() {
if (wasAlreadyPlaying) { 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, Toast.makeText(RemoteActivity.this,
getString(R.string.item_added_to_playlist), msg,
Toast.LENGTH_SHORT) Toast.LENGTH_SHORT)
.show(); .show();
} }

View File

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