From 37ef130a7c31f1c43a3a5263fab25816ef54dc6e Mon Sep 17 00:00:00 2001 From: Francesco Bonazzi Date: Mon, 16 Dec 2019 19:37:46 +0100 Subject: [PATCH] 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 --- app/src/main/AndroidManifest.xml | 45 +++++++ .../kore/ui/sections/localfile/HttpApp.java | 115 ++++++++++++++++-- .../ui/sections/remote/OpenSharedUrl.java | 55 +++++---- .../ui/sections/remote/QueueActivity.java | 12 ++ .../ui/sections/remote/RemoteActivity.java | 87 +++++++++++-- app/src/main/res/values/strings.xml | 2 + 6 files changed, 275 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/remote/QueueActivity.java 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