Add new intent to "Play on Kodi". This allows sharing videos from the YouTube app to Kodi (unfortunately the share option will appear in every app that shares plain text links, as that is what the YouTube app shares).
Tweaked the Now Playing and Playlist screens to better support YouTube videos Changed HostConnectionObserver to notify clients not only when the id of what's playing changes but also when the label changes (YouTube videos and pictures for instance don't have ids, so the remote wasn't getting notified of a change in what's playing)
This commit is contained in:
parent
c0073d5f29
commit
611dafc101
|
@ -21,6 +21,14 @@
|
|||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent filter for sharing from youtube player -->
|
||||
<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="text/plain"/>
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="org.xbmc.kore.ui.hosts.HostManagerActivity"/>
|
||||
|
|
|
@ -554,11 +554,12 @@ public class HostConnectionObserver
|
|||
int currentCallResult = (getPropertiesResult.speed == 0) ?
|
||||
PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING;
|
||||
if (forceReply ||
|
||||
(lastCallResult != currentCallResult) ||
|
||||
(lastGetPropertiesResult.speed != getPropertiesResult.speed) ||
|
||||
(lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) ||
|
||||
(!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) ||
|
||||
(lastGetItemResult.id != getItemResult.id)) {
|
||||
(lastCallResult != currentCallResult) ||
|
||||
(lastGetPropertiesResult.speed != getPropertiesResult.speed) ||
|
||||
(lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) ||
|
||||
(!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) ||
|
||||
(lastGetItemResult.id != getItemResult.id) ||
|
||||
(!lastGetItemResult.label.equals(getItemResult.label))) {
|
||||
lastCallResult = currentCallResult;
|
||||
lastGetActivePlayerResult = getActivePlayersResult;
|
||||
lastGetPropertiesResult = getPropertiesResult;
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
|||
import org.xbmc.kore.utils.LogUtils;
|
||||
import org.xbmc.kore.utils.UIUtils;
|
||||
import org.xbmc.kore.utils.Utils;
|
||||
import org.xbmc.kore.utils.jsonrpcCommonCalls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
@ -345,8 +346,6 @@ public class MediaFileListFragment extends Fragment {
|
|||
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
while (mediaQueueFileLocation.size() > 0) {
|
||||
queueMediaFile(mediaQueueFileLocation.poll());
|
||||
}
|
||||
|
@ -355,13 +354,11 @@ public class MediaFileListFragment extends Fragment {
|
|||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
Toast.makeText(getActivity(),
|
||||
String.format(getString(R.string.error_play_media_file), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
|
||||
}
|
||||
|
||||
private void queueMediaFile(final FileLocation loc) {
|
||||
|
@ -371,15 +368,12 @@ public class MediaFileListFragment extends Fragment {
|
|||
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result ) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
startPlayingIfNoActivePlayers();
|
||||
jsonrpcCommonCalls.startPlaylistIfNoActivePlayers(getActivity(), playlistId, callbackHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
Toast.makeText(getActivity(),
|
||||
String.format(getString(R.string.error_queue_media_file), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
@ -388,50 +382,6 @@ public class MediaFileListFragment extends Fragment {
|
|||
|
||||
}
|
||||
|
||||
private void startPlayingIfNoActivePlayers() {
|
||||
Player.GetActivePlayers action = new Player.GetActivePlayers();
|
||||
action.execute(hostManager.getConnection(), new ApiCallback<ArrayList<PlayerType.GetActivePlayersReturnType>>() {
|
||||
@Override
|
||||
public void onSuccess(ArrayList<PlayerType.GetActivePlayersReturnType> result ) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
// find out if any player is running. If it is not, start one
|
||||
if (result.size() == 0) {
|
||||
startPlaying(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
Toast.makeText(getActivity(),
|
||||
String.format(getString(R.string.error_get_active_player), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
|
||||
}
|
||||
|
||||
private void startPlaying(int playlistID) {
|
||||
Player.Open action = new Player.Open(playlistID);
|
||||
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result ) {
|
||||
if (!isAdded()) return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
Toast.makeText(getActivity(),
|
||||
String.format(getString(R.string.error_play_media_file), description),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}, callbackHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* return the path of the parent based on path
|
||||
* @param path of the current media file
|
||||
|
|
|
@ -714,8 +714,8 @@ public class NowPlayingFragment extends Fragment
|
|||
poster = getItemResult.thumbnail;
|
||||
|
||||
genreSeason = null;
|
||||
year = null;
|
||||
descriptionPlot = null;
|
||||
year = getItemResult.premiered;
|
||||
descriptionPlot = removeYouTubeMarkup(getItemResult.plot);
|
||||
rating = 0;
|
||||
maxRating = null;
|
||||
votes = null;
|
||||
|
@ -889,6 +889,17 @@ public class NowPlayingFragment extends Fragment
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes some markup that appears on the plot for youtube videos
|
||||
*
|
||||
* @param plot Plot as returned by youtube plugin
|
||||
* @return Plot without markup
|
||||
*/
|
||||
private String removeYouTubeMarkup(String plot) {
|
||||
if (plot == null) return null;
|
||||
return plot.replaceAll("\\[.*\\]", "");
|
||||
}
|
||||
|
||||
private int mediaTotalTime = 0,
|
||||
mediaCurrentTime = 0; // s
|
||||
private static final int SEEK_BAR_UPDATE_INTERVAL = 1000; // ms
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.xbmc.kore.ui;
|
|||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -617,7 +618,7 @@ public class PlaylistFragment extends Fragment
|
|||
break;
|
||||
default:
|
||||
// Don't yet recognize this type
|
||||
title = item.label;
|
||||
title = TextUtils.isEmpty(item.label)? item.file : item.label;
|
||||
details = item.type;
|
||||
artUrl = item.thumbnail;
|
||||
duration = item.runtime;
|
||||
|
|
|
@ -17,6 +17,7 @@ package org.xbmc.kore.ui;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
|
@ -40,17 +41,27 @@ 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.GlobalType;
|
||||
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.NotificationService;
|
||||
import org.xbmc.kore.ui.hosts.AddHostActivity;
|
||||
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.jsonrpcCommonCalls;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.InjectView;
|
||||
|
@ -120,6 +131,9 @@ public class RemoteActivity extends BaseActivity
|
|||
|
||||
setupActionBar();
|
||||
|
||||
// If we should start playing something
|
||||
handleStartIntent(getIntent());
|
||||
|
||||
// // Setup system bars and content padding
|
||||
// setupSystemBarsColors();
|
||||
// // Set the padding of views.
|
||||
|
@ -160,12 +174,16 @@ public class RemoteActivity extends BaseActivity
|
|||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
new Application.SetVolume(GlobalType.IncrementDecrement.INCREMENT).execute(hostManager.getConnection(), null, null);
|
||||
new Application
|
||||
.SetVolume(GlobalType.IncrementDecrement.INCREMENT)
|
||||
.execute(hostManager.getConnection(), null, null);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
new Application.SetVolume(GlobalType.IncrementDecrement.DECREMENT).execute(hostManager.getConnection(), null, null);
|
||||
new Application
|
||||
.SetVolume(GlobalType.IncrementDecrement.DECREMENT)
|
||||
.execute(hostManager.getConnection(), null, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -297,6 +315,101 @@ 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) {
|
||||
final String action = intent.getAction();
|
||||
// Check action
|
||||
if ((action == null) || !action.equals(Intent.ACTION_SEND)) return;
|
||||
|
||||
// Get the URI, which is stored in Extras
|
||||
final Uri youTubeUri = getYouTubeUri(intent.getStringExtra(Intent.EXTRA_TEXT));
|
||||
if (youTubeUri == null) return;
|
||||
|
||||
final String videoId = getYouTubeVideoId(youTubeUri);
|
||||
if (videoId == null) return;
|
||||
|
||||
String kodiAddonUrl = "plugin://plugin.video.youtube/?path=/root/search&action=play_video&videoid="
|
||||
+ videoId;
|
||||
|
||||
queueMediaFile(VIDEO_PLAYLISTID, kodiAddonUrl, new Handler());
|
||||
}
|
||||
|
||||
private static final int VIDEO_PLAYLISTID = 1;
|
||||
|
||||
private void queueMediaFile(final int playlistId, final String file, final Handler callbackHandler) {
|
||||
PlaylistType.Item item = new PlaylistType.Item();
|
||||
item.file = file;
|
||||
Playlist.Add action = new Playlist.Add(playlistId, item);
|
||||
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result ) {
|
||||
jsonrpcCommonCalls.startPlaylistIfNoActivePlayers(RemoteActivity.this, playlistId, 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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the YouTube Uri that the YouTube app passes in EXTRA_TEXT
|
||||
* YouTube sends something like: [Video title]: [YouTube URL] so we need
|
||||
* to get the second part
|
||||
*
|
||||
* @param extraText EXTRA_TEXT passed in the intent
|
||||
* @return Uri present in extraText if present
|
||||
*/
|
||||
private Uri getYouTubeUri(String extraText) {
|
||||
if (extraText == null) return null;
|
||||
|
||||
for (String word : extraText.split(" ")) {
|
||||
if (word.startsWith("http://") || word.startsWith("https://")) {
|
||||
try {
|
||||
URL validUri = new URL(word);
|
||||
return Uri.parse(word);
|
||||
} catch (MalformedURLException exc) {
|
||||
LogUtils.LOGD(TAG, "Got a malformed URL in an intent: " + word);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the youtube video ID from its URL
|
||||
*
|
||||
* @param playuri Youtube URL
|
||||
* @return Youtube Video ID
|
||||
*/
|
||||
private String getYouTubeVideoId(Uri playuri) {
|
||||
if (playuri.getHost().endsWith("youtube.com") || playuri.getHost().endsWith("youtu.be")) {
|
||||
// We'll need to get the v= parameter from the URL
|
||||
final Pattern pattern =
|
||||
Pattern.compile("(?:https?:\\/\\/)?(?:www\\.)?youtu(?:.be\\/|be\\.com\\/watch\\?v=)([\\w-]{11})",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
// final Pattern pattern = Pattern.compile("^http(:?s)?:\\/\\/(?:www\\.)?(?:youtube\\.com|youtu\\.be)\\/watch\\?(?=.*v=([\\w-]+))(?:\\S+)?$", Pattern.CASE_INSENSITIVE);
|
||||
// final Pattern pattern = Pattern.compile(".*v=([a-z0-9_\\-]+)(?:&.)*", Pattern.CASE_INSENSITIVE);
|
||||
final Matcher matcher = pattern.matcher(playuri.toString());
|
||||
if (matcher.matches()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Default page change listener, that doesn't scroll images
|
||||
ViewPager.OnPageChangeListener defaultOnPageChangeListener = new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.xbmc.kore.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Player;
|
||||
import org.xbmc.kore.jsonrpc.type.PlayerType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Common jsonrpc method calls, that appear more than once on the code
|
||||
*/
|
||||
public class jsonrpcCommonCalls {
|
||||
|
||||
/**
|
||||
* Starts a playlist if no active players are playing
|
||||
*
|
||||
* @param context Context
|
||||
* @param playlistId PlaylistID
|
||||
* @param callbackHandler Handler on which to post method callbacks
|
||||
*/
|
||||
public static void startPlaylistIfNoActivePlayers(final Context context, final int playlistId, final Handler callbackHandler) {
|
||||
final HostConnection connection = HostManager.getInstance(context).getConnection();
|
||||
Player.GetActivePlayers action = new Player.GetActivePlayers();
|
||||
action.execute(connection, new ApiCallback<ArrayList<PlayerType.GetActivePlayersReturnType>>() {
|
||||
@Override
|
||||
public void onSuccess(ArrayList<PlayerType.GetActivePlayersReturnType> result ) {
|
||||
// find out if any player is running. If it is not, start one
|
||||
if (result.size() == 0) {
|
||||
startPlaying(connection, playlistId, callbackHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) { }
|
||||
}, callbackHandler);
|
||||
|
||||
}
|
||||
|
||||
private static void startPlaying(final HostConnection connection, final int playlistId, final Handler callbackHandler) {
|
||||
Player.Open action = new Player.Open(playlistId);
|
||||
action.execute(connection, new ApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result ) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) { }
|
||||
}, callbackHandler);
|
||||
}
|
||||
|
||||
}
|
|
@ -319,4 +319,6 @@
|
|||
<!--<string name="error_during_purchased">An error occurred during purchase.</string>-->
|
||||
<!--<string name="purchase_thanks">Thanks for your support!</string>-->
|
||||
|
||||
<string name="play_on_kodi">Play on Kodi</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue