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:
Synced Synapse 2015-04-08 22:28:19 +01:00
parent c0073d5f29
commit 611dafc101
8 changed files with 219 additions and 62 deletions

View File

@ -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"/>

View File

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

View File

@ -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

View 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

View File

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

View File

@ -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

View File

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

View File

@ -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>