Merge branch 'master' into player-overlay-redesign

This commit is contained in:
Florian authored and GitHub committed 2020-01-10 15:26:04 +01:00
commit 2ada7b65a8
49 files changed
+690 -1405

No files matched your search

+6 -17
View File
@@ -1,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def version = "0.10.2"
android {
@@ -29,28 +29,12 @@ android {
debuggable true
}
}
flavorDimensions "version"
productFlavors {
google {
dimension = "version"
versionName = version + "g"
}
amazon {
dimension = "version"
versionName = version + "a"
}
}
}
dependencies {
def acraVersion = '5.4.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
amazonImplementation 'com.github.jellyfin:exomedia:amazon~2.5.6-SNAPSHOT'
amazonImplementation 'com.github.amzn:exoplayer-amazon-port:amazon-r1.5.6-SNAPSHOT'
googleImplementation 'com.devbrackets.android:exomedia:2.5.6'
googleImplementation 'com.google.android.exoplayer:exoplayer:r1.5.6'
// This is split like this because this is a modular gradle project on jitpack
// see https://github.com/jitpack/gradle-modular
@@ -59,6 +43,7 @@ dependencies {
// is better than nothing.
implementation 'com.github.jellyfin.jellyfin-apiclient-java:android:master-SNAPSHOT'
implementation 'com.github.jellyfin.jellyfin-apiclient-java:library:master-SNAPSHOT'
implementation 'com.amazon.android:exoplayer:2.10.6'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
@@ -76,6 +61,10 @@ dependencies {
testImplementation 'junit:junit:4.12'
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.tvprovider:tvprovider:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
}
repositories {
mavenCentral()
+3 -7
View File
@@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Needed for ChannelManager on older devices (< API 23) -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -81,13 +84,6 @@
</intent-filter>
</receiver>
<provider
android:authorities="${applicationId}.recommendations"
android:name=".integration.RecommendationContentProvider"
android:exported="true"
android:grantUriPermissions="true"
tools:ignore="ExportedContentProvider" />
<activity
android:name=".details.ExpandedTextActivity"
android:label="@string/title_activity_expanded_text" />
@@ -1,26 +1,17 @@
package org.jellyfin.androidtv;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.AudioManager;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.palette.graphics.Palette;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
@@ -30,19 +21,15 @@ import org.acra.sender.HttpSender;
import org.jellyfin.androidtv.base.BaseActivity;
import org.jellyfin.androidtv.livetv.TvManager;
import org.jellyfin.androidtv.model.DisplayPriorityType;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.playback.ExternalPlayerActivity;
import org.jellyfin.androidtv.playback.MediaManager;
import org.jellyfin.androidtv.playback.PlaybackController;
import org.jellyfin.androidtv.playback.PlaybackManager;
import org.jellyfin.androidtv.playback.PlaybackOverlayActivity;
import org.jellyfin.androidtv.search.SearchActivity;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.util.DeviceUtils;
import org.jellyfin.androidtv.util.Utils;
import java.util.Calendar;
import java.util.HashMap;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.IConnectionManager;
@@ -57,6 +44,12 @@ import org.jellyfin.apiclient.model.entities.DisplayPreferences;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
import java.util.Calendar;
import java.util.HashMap;
import androidx.core.content.ContextCompat;
import androidx.palette.graphics.Palette;
@AcraCore(buildConfigClass = BuildConfig.class)
@AcraHttpSender(
uri = "https://collector.tracepot.com/a2eda9d9",
@@ -78,7 +71,6 @@ public class TvApp extends Application {
public static final int VIDEO_QUEUE_OPTION_ID = 3000;
public static final int LIVE_TV_SCHEDULE_OPTION_ID = 4000;
public static final int LIVE_TV_SERIES_OPTION_ID = 5000;
public static final int SEARCH_PERMISSION = 0;
private static final String TAG = "Jellyfin-AndroidTV";
@@ -116,8 +108,6 @@ public class TvApp extends Application {
private long lastMusicPlayback = System.currentTimeMillis();
private long lastUserInteraction = System.currentTimeMillis();
private boolean voiceSearchAllowed = Build.VERSION.SDK_INT < 23;
private GradientDrawable currentBackgroundGradient;
private boolean audioMuted;
@@ -237,32 +227,9 @@ public class TvApp extends Application {
}
public void showSearch(final Activity activity, boolean musicOnly) {
if (!voiceSearchAllowed &&
ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
//request necessary permission
logger.Info("Requesting search permission...");
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO)) {
//show explanation
logger.Info("Show rationale for permission");
new AlertDialog.Builder(activity)
.setTitle("Search Permission")
.setMessage("Search requires permission to record audio in order to use the microphone for voice search")
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
ActivityCompat.requestPermissions(activity, new String[] {Manifest.permission.RECORD_AUDIO}, SEARCH_PERMISSION);
}
}).show();
} else {
ActivityCompat.requestPermissions(activity, new String[] {Manifest.permission.RECORD_AUDIO}, SEARCH_PERMISSION);
}
} else {
showSearchWithPermission(activity, musicOnly);
}
}
public static void showSearchWithPermission(Context activity, boolean musicOnly) {
Intent intent = new Intent(activity, SearchActivity.class);
if (musicOnly) intent.putExtra("MusicOnly", true);
intent.putExtra("MusicOnly", musicOnly);
activity.startActivity(intent);
}
@@ -309,7 +276,7 @@ public class TvApp extends Application {
case Video:
case Series:
case Recording:
return getPrefs().getBoolean("pref_video_use_external", false);
return getPrefs().getString("pref_video_player", "auto").equals("external");
case TvChannel:
case Program:
return getPrefs().getBoolean("pref_live_tv_use_external", false);
@@ -553,14 +520,6 @@ public class TvApp extends Application {
this.httpClient = httpClient;
}
public boolean isVoiceSearchAllowed() {
return voiceSearchAllowed;
}
public void setVoiceSearchAllowed(boolean voiceSearchAllowed) {
this.voiceSearchAllowed = voiceSearchAllowed;
}
public BaseItemDto getLastPlayedItem() {
return lastPlayedItem;
}
@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.base;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
@@ -13,14 +12,13 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.util.Utils;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
public abstract class BaseActivity extends FragmentActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
private TvApp app = TvApp.getApplication();
private long timeoutInterval = 3600000;
@@ -191,16 +189,4 @@ public abstract class BaseActivity extends FragmentActivity implements ActivityC
TvApp.getApplication().showSearch(this, false);
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// Search functionality requests access to RECORD_AUDIO
if (requestCode == TvApp.SEARCH_PERMISSION) {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
TvApp.getApplication().setVoiceSearchAllowed(true);
}
TvApp.showSearchWithPermission(this, false);
}
}
}
@@ -10,8 +10,8 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.CustomMessage;
import org.jellyfin.androidtv.base.IMessageListener;
import org.jellyfin.androidtv.channels.ChannelManager;
import org.jellyfin.androidtv.constants.HomeSectionType;
import org.jellyfin.androidtv.integration.RecommendationManager;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.livetv.LiveTvGuideActivity;
import org.jellyfin.androidtv.model.ChangeTriggerType;
@@ -70,6 +70,8 @@ public class HomeFragment extends StdBrowseFragment {
private HomeFragmentNowPlayingRow nowPlaying;
private HomeFragmentFooterRow footer;
private ChannelManager channelManager;
private AudioEventListener audioEventListener = new AudioEventListener() {
@Override
public void onQueueStatusChanged(boolean hasQueue) {
@@ -90,8 +92,8 @@ public class HomeFragment extends StdBrowseFragment {
TvApp.getApplication().getLogger().ErrorException("Unable to save login credentials", e);
}
// Init recommendations
RecommendationManager.init();
// Init leanback home channels;
channelManager = new ChannelManager();
// Get auto bitrate
TvApp.getApplication().determineAutoBitrate();
@@ -146,6 +148,9 @@ public class HomeFragment extends StdBrowseFragment {
public void onResume() {
super.onResume();
// Update leanback channels
channelManager.update();
//make sure rows have had a chance to be created
new Handler().postDelayed(new Runnable() {
@Override
@@ -0,0 +1,124 @@
package org.jellyfin.androidtv.channels
import android.content.Intent
import android.net.Uri
import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms
import androidx.tvprovider.media.tv.WatchNextProgram
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.TvApp
import org.jellyfin.androidtv.startup.StartupActivity
import org.jellyfin.androidtv.util.apiclient.getNextUpEpisodes
import org.jellyfin.apiclient.model.drawing.ImageFormat
import org.jellyfin.apiclient.model.dto.BaseItemDto
import org.jellyfin.apiclient.model.dto.ImageOptions
import org.jellyfin.apiclient.model.querying.ItemFields
import org.jellyfin.apiclient.model.querying.NextUpQuery
/**
* Manages channels on the android tv home screen
*
* More info: https://developer.android.com/training/tv/discovery/recommendations-channel
*/
class ChannelManager {
private companion object {
/**
* Amount of ticks found in a millisecond, used for calculation
*/
private const val TICKS_IN_MILLISECOND = 10000
}
private val application = TvApp.getApplication()
/**
* Check if the app can use Leanback features
*/
private val isSupported = application.packageManager.hasSystemFeature("android.software.leanback")
/**
* Update all channels for the currently authenticated user
*/
fun update() {
// Don't do anything if not supported
if (!isSupported) return
// Launch in separate coroutine scope
GlobalScope.launch {
updateWatchNext()
}
}
/**
* Updates the "watch next" row with new and unfinished episodes
* does not include movies, music or other types of media
*/
private suspend fun updateWatchNext() = withContext(Dispatchers.Default) {
// Delete current items
application.contentResolver.delete(WatchNextPrograms.CONTENT_URI, null, null)
// Get user or return if no user is found (not authenticated)
val user = application.currentUser ?: return@withContext
// Get new items
val response = application.apiClient.getNextUpEpisodes(NextUpQuery().apply {
userId = user.id
imageTypeLimit = 1
limit = 10
fields = arrayOf(ItemFields.DateCreated)
})
// Add new items
response?.items?.forEach { item ->
application.contentResolver.insert(
WatchNextPrograms.CONTENT_URI,
getBaseItemAsWatchNextProgram(item).toContentValues()
)
}
}
/**
* Convert [BaseItemDto] to [WatchNextProgram]
*
* Assumes the item type is "episode"
*/
private fun getBaseItemAsWatchNextProgram(item: BaseItemDto) = WatchNextProgram.Builder().apply {
setInternalProviderId(item.id)
setType(WatchNextPrograms.TYPE_TV_EPISODE)
setTitle("${item.seriesName} - ${item.name}")
// Poster image
setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
setPosterArtUri(Uri.parse(application.apiClient.GetImageUrl(item, ImageOptions().apply {
format = ImageFormat.Png
height = 288
width = 512
})))
// Use date created or fallback to current time if unavailable
setLastEngagementTimeUtcMillis(item.dateCreated?.time ?: System.currentTimeMillis())
when {
// User has started playing the episode
item.canResume -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
setLastPlaybackPositionMillis((item.resumePositionTicks / TICKS_IN_MILLISECOND).toInt())
setDurationMillis((item.runTimeTicks / TICKS_IN_MILLISECOND).toInt())
}
// First episode of the season
item.indexNumber == 0 -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_NEW)
}
// Default
else -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_NEXT)
}
}
// Set intent to open the episode
setIntent(Intent(application, StartupActivity::class.java).apply {
putExtra("ItemId", item.id)
})
}.build()
}
@@ -1,51 +0,0 @@
package org.jellyfin.androidtv.integration;
public class Recommendation {
private RecommendationType type;
private String itemId;
private Integer recId;
private long dateAdded;
public Recommendation(RecommendationType type, String id) {
itemId = id;
this.type = type;
this.dateAdded = System.currentTimeMillis();
}
public String getItemId() {
return itemId;
}
public void setItemId(String mItemId) {
this.itemId = mItemId;
}
public Integer getRecId() {
return recId;
}
public void setRecId(Integer mRecId) {
this.recId = mRecId;
}
public long getDateAdded() {
return dateAdded;
}
public RecommendationType getType() {
return type;
}
public void setType(RecommendationType mType) {
this.type = mType;
}
@Override
public boolean equals(Object o) {
if (o instanceof Recommendation) {
Recommendation compare = (Recommendation)o;
return compare.getType() == this.getType() && compare.getItemId().equals(this.getItemId());
}
return false;
}
}
@@ -1,127 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import org.jellyfin.androidtv.util.Utils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* Created by Eric on 3/1/2015.
*/
public class RecommendationBuilder {
private static final String TAG = "RecommendationBuilder";
private static final String NOTIFICATION_CHANNEL = "Recommendations";
private Context mContext;
private int mId;
private int mPriority;
private int mSmallIcon;
private String mTitle;
private String mDescription;
private Bitmap mBitmap;
private String mBackgroundUri;
private PendingIntent mIntent;
public RecommendationBuilder() {
}
public RecommendationBuilder setContext(Context context) {
mContext = context;
return this;
}
public RecommendationBuilder setId(int id) {
mId = id;
return this;
}
public RecommendationBuilder setPriority(int priority) {
mPriority = priority;
return this;
}
public RecommendationBuilder setTitle(String title) {
mTitle = title;
return this;
}
public RecommendationBuilder setDescription(String description) {
mDescription = description;
return this;
}
public RecommendationBuilder setBitmap(Bitmap bitmap) {
mBitmap = bitmap;
return this;
}
public RecommendationBuilder setBackground(String uri) {
mBackgroundUri = uri;
return this;
}
public RecommendationBuilder setIntent(PendingIntent intent) {
mIntent = intent;
return this;
}
public RecommendationBuilder setSmallIcon(int resourceId) {
mSmallIcon = resourceId;
return this;
}
public Notification build() {
Log.d(TAG, "Building notification - " + this.toString());
Bundle extras = new Bundle();
if (mBackgroundUri != null) {
try {
Log.d(TAG, "Background - " + Uri.parse(RecommendationContentProvider.CONTENT_URI + URLEncoder.encode(mBackgroundUri, "UTF-8") + "/0").toString());
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, Uri.parse(RecommendationContentProvider.CONTENT_URI + URLEncoder.encode(mBackgroundUri, "UTF-8")).toString());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL)
.setContentTitle(mTitle)
.setContentText(mDescription)
.setPriority(mPriority)
.setLocalOnly(true)
.setOngoing(true)
.setColor(Utils.getBrandColor(mContext))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setLargeIcon(mBitmap)
.setSmallIcon(mSmallIcon)
.setContentIntent(mIntent)
.setExtras(extras))
.build();
return notification;
}
@Override
public String toString() {
return "RecommendationBuilder{" +
", mId=" + mId +
", mPriority=" + mPriority +
", mSmallIcon=" + mSmallIcon +
", mTitle='" + mTitle + '\'' +
", mDescription='" + mDescription + '\'' +
", mBitmap='" + mBitmap + '\'' +
", mBackgroundUri='" + mBackgroundUri + '\'' +
", mIntent=" + mIntent +
'}';
}}
@@ -1,157 +0,0 @@
package org.jellyfin.androidtv.integration;
/**
* The MIT License (MIT)
* Copyright (c) 2014 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
/**
* Provides a background image for Recommendations for a RecommendationCardView.
*
* The card view changed to require a content provider be created. It requests
* items via the openFile(Uri uri) method. This is an example of how to retrieve
* an image from a web site and provide a ParcelFileDescriptor back that
* contains the contents.
*
* This is based on code from the following stackoverflow description on how to
* populate a ParcelFileDescriptor from any input stream.
*
* http://stackoverflow.com/a/14734310/1950264
*
* You still need to setup a ContentProvider entry and Authority in the
* AndroidManifest.xml
*
* See
* http://developer.android.com/reference/android/content/ContentProvider.html
*
*/
public class RecommendationContentProvider extends ContentProvider {
public static String AUTHORITY = "org.jellyfin.androidtv.recommendations";
public static String CONTENT_URI = "content://" + AUTHORITY + "/";
@Override
public boolean onCreate() {
return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
ParcelFileDescriptor[] pipe = null;
String url = uri.getPath();
try {
String decodedUrl = URLDecoder.decode(url.replaceFirst("/", ""),
"UTF-8");
pipe = ParcelFileDescriptor.createPipe();
// This uses OKHttp to
OkHttpClient httpClient = new OkHttpClient();
HttpURLConnection connection = new OkUrlFactory(httpClient).open(new URL(decodedUrl));
new TransferThread(connection.getInputStream(),
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]))
.start();
} catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
throw new FileNotFoundException("Could not open pipe for: "
+ uri.toString());
}
return (pipe[0]);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(Uri uri) {
return "image/*";
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}
static class TransferThread extends Thread {
InputStream in;
OutputStream out;
TransferThread(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[8192];
int len;
try {
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
Log.e(getClass().getSimpleName(),
"Exception transferring file", e);
}
}
}
}
@@ -1,332 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.browsing.MainActivity;
import org.jellyfin.androidtv.querying.StdItemQuery;
import org.jellyfin.androidtv.startup.StartupActivity;
import org.jellyfin.androidtv.util.ImageUtils;
import org.jellyfin.androidtv.util.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import org.jellyfin.apiclient.model.querying.ItemSortBy;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
/**
* Created by Eric on 3/1/2015.
*/
public class RecommendationManager {
private final String REC_FILE_NAME = "reccomendations.json";
private final Integer MAX_TV_RECS = 3;
private final Integer MAX_MOVIE_RECS = 4;
private boolean isEnabled;
private static RecommendationManager instance;
private Recommendations mRecommendations;
public RecommendationManager() {
isEnabled = Build.VERSION.SDK_INT >= 21;
mRecommendations = loadRecs();
if (isEnabled) {
validate();
} else {
TvApp.getApplication().getLogger().Info("Recommendations not enabled on this device");
}
}
public static RecommendationManager getInstance() {
if (instance == null) instance = new RecommendationManager();
return instance;
}
public static void init() {
if (instance == null) {
getInstance();
} else {
instance.validate();
}
}
private Recommendations loadRecs() {
if (isEnabled) {
try {
InputStream recFile = TvApp.getApplication().openFileInput(REC_FILE_NAME);
String json = Utils.readStringFromStream(recFile);
recFile.close();
return (Recommendations) TvApp.getApplication().getSerializer().DeserializeFromString(json, Recommendations.class);
} catch (IOException e) {
// none saved
return new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
}
} else {
return new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
}
}
public boolean validate() {
if (isEnabled) {
//Now validate that these are for this server and user.
if (mRecommendations == null || mRecommendations.getServerId() == null || TvApp.getApplication().getApiClient().getServerInfo() == null || !mRecommendations.getServerId().equals(TvApp.getApplication().getApiClient().getServerInfo().getId())
|| mRecommendations.getUserId() == null || TvApp.getApplication().getCurrentUser() == null || !mRecommendations.getUserId().equals(TvApp.getApplication().getCurrentUser().getId())) {
//Nope - clear them out and start over for this user
clearAll();
createAll();
TvApp.getApplication().getLogger().Info("Recommendations re-set for user "+TvApp.getApplication().getCurrentUser().getName());
return false;
}
createAll();
TvApp.getApplication().getLogger().Info("Recommendations re-created for user "+TvApp.getApplication().getCurrentUser().getName());
}
return true;
}
private void saveRecs() {
if (isEnabled) {
try {
OutputStream recFile = TvApp.getApplication().openFileOutput(REC_FILE_NAME, Context.MODE_PRIVATE);
recFile.write(TvApp.getApplication().getSerializer().SerializeToString(mRecommendations).getBytes());
recFile.close();
} catch (IOException e) {
TvApp.getApplication().getLogger().ErrorException("Error saving recommendations",e);
}
}
}
public void clearAll() {
NotificationManager nm = (NotificationManager)
TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancelAll();
mRecommendations = new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
saveRecs();
}
public void createAll() {
if (isEnabled) {
//Create recs for next up tv and movies
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
nextUpQuery.setLimit(MAX_TV_RECS);
nextUpQuery.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio});
TvApp.getApplication().getApiClient().GetNextUpEpisodesAsync(nextUpQuery, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
if (response.getTotalRecordCount() > 0) {
for (BaseItemDto episode : response.getItems()) {
if (episode.getLocationType() != LocationType.Virtual) addRecommendation(episode, RecommendationType.Tv);
}
}
}
});
//First try for resumables
StdItemQuery resumeMovies = new StdItemQuery();
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
resumeMovies.setRecursive(true);
resumeMovies.setLimit(2);
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeMovies.setSortOrder(SortOrder.Descending);
TvApp.getApplication().getApiClient().GetItemsAsync(resumeMovies, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
int movieItems = 0;
if (response.getTotalRecordCount() > 0) {
for (BaseItemDto movie : response.getItems()) {
recommend(movie.getId());
movieItems++;
}
}
if (movieItems < MAX_MOVIE_RECS) {
//Now fill in with latest movies
StdItemQuery suggMovies = new StdItemQuery();
suggMovies.setIncludeItemTypes(new String[]{"Movie"});
suggMovies.setRecursive(true);
suggMovies.setLimit(MAX_MOVIE_RECS - movieItems);
suggMovies.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
suggMovies.setSortBy(new String[]{ItemSortBy.DateCreated});
suggMovies.setSortOrder(SortOrder.Descending);
TvApp.getApplication().getApiClient().GetItemsAsync(suggMovies, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult suggResponse) {
if (suggResponse.getTotalRecordCount() > 0) {
for (BaseItemDto movie : suggResponse.getItems()) {
addRecommendation(movie, RecommendationType.Movie);
}
}
}
});
}
}
});
}
}
public void recommend(final String itemId) {
if (isEnabled) {
if (itemId == null) {
TvApp.getApplication().getLogger().Error("Attempt to recommend null Item");
return;
}
//No matter what it is, if it is resumable, recommend this item (need to re-retrieve for current user data)
TvApp.getApplication().getApiClient().GetItemAsync(itemId, TvApp.getApplication().getCurrentUser().getId(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
if (response == null) {
TvApp.getApplication().getLogger().Error("No item found with ID: "+itemId);
return;
}
if (response.getCanResume()) {
addRecommendation(response, RecommendationType.Movie);
} else {
switch (response.getBaseItemType()) {
case Movie:
//First remove us if we were a recommendation
mRecommendations.remove(RecommendationType.Movie, response.getId());
//Suggest a similar movie
SimilarItemsQuery similar = new SimilarItemsQuery();
similar.setId(response.getId());
similar.setLimit(1);
similar.setUserId(TvApp.getApplication().getCurrentUser().getId());
TvApp.getApplication().getApiClient().GetSimilarItems(similar, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult similarResponse) {
if (similarResponse.getTotalRecordCount() > 0) {
addRecommendation(similarResponse.getItems()[0], RecommendationType.Movie);
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
break;
case Episode:
//First remove us if we were a recommendation
mRecommendations.remove(RecommendationType.Tv, response.getId());
//Suggest the next up episode
NextUpQuery next = new NextUpQuery();
next.setSeriesId(response.getSeriesId());
next.setUserId(TvApp.getApplication().getCurrentUser().getId());
next.setLimit(1);
TvApp.getApplication().getApiClient().GetNextUpEpisodesAsync(next, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult nextResponse) {
if (nextResponse.getTotalRecordCount() > 0 && nextResponse.getItems()[0].getLocationType() != LocationType.Virtual) {
addRecommendation(nextResponse.getItems()[0], RecommendationType.Tv);
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
break;
}
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
}
}
public boolean addRecommendation(BaseItemDto item, RecommendationType type) {
if (isEnabled) {
//No need if we are already there
if (mRecommendations.get(type, item.getId()) != null) return false;
//Not so build one
new AsyncRunner().execute(item, type);
return true;
} else {
return false;
}
}
private class AsyncRunner extends AsyncTask<Object, Integer, Boolean> {
@Override
protected Boolean doInBackground(Object... params) {
BaseItemDto item = (BaseItemDto) params[0];
RecommendationType type = (RecommendationType) params[1];
Recommendation rec = new Recommendation(type, item.getId());
rec.setRecId(mRecommendations.getRecId(type, type == RecommendationType.Movie ? MAX_MOVIE_RECS : MAX_TV_RECS));
RecommendationBuilder builder = new RecommendationBuilder()
.setContext(TvApp.getApplication())
.setSmallIcon(R.drawable.app_icon_medium);
Notification recommendation = builder
.setId(rec.getRecId())
.setPriority(0)
.setTitle(item.getName())
.setDescription(item.getOverview())
.setBitmap(Utils.getBitmapFromURL(ImageUtils.getPrimaryImageUrl(item, TvApp.getApplication().getApiClient(), true, true, false, 300)))
.setBackground(ImageUtils.getBackdropImageUrl(item, TvApp.getApplication().getApiClient(), true))
.setIntent(buildPendingIntent(item))
.build();
mRecommendations.add(rec);
saveRecs();
NotificationManager notificationManager = (NotificationManager)
TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(rec.getRecId(), recommendation);
return true;
}
}
private PendingIntent buildPendingIntent(BaseItemDto item) {
Intent directIntent = new Intent(TvApp.getApplication(), StartupActivity.class);
directIntent.putExtra("ItemId", item.getId());
directIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(TvApp.getApplication());
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(directIntent);
// Ensure a unique PendingIntents, otherwise all recommendations end up with the same
// PendingIntent
directIntent.setAction(item.getId());
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
@@ -1,6 +0,0 @@
package org.jellyfin.androidtv.integration;
public enum RecommendationType {
Movie,
Tv
}
@@ -1,129 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.NotificationManager;
import android.content.Context;
import org.jellyfin.androidtv.TvApp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by Eric on 3/1/2015.
*/
public class Recommendations {
private String serverId;
private String userId;
private List<Recommendation> tvRecommendations = new ArrayList<>();
private List<Recommendation> movieRecommendations = new ArrayList<>();
public Recommendations(String serverId, String userId) {
this.userId = userId;
this.serverId = serverId;
}
public List<Recommendation> getTvRecommendations() {
return tvRecommendations;
}
public void setTvRecommendations(List<Recommendation> tvRecommendations) {
this.tvRecommendations = tvRecommendations;
}
public List<Recommendation> getMovieRecommendations() {
return movieRecommendations;
}
public void setMovieRecommendations(List<Recommendation> movieRecommendations) {
this.movieRecommendations = movieRecommendations;
}
public Recommendation get(RecommendationType type, String id) {
Recommendation compare = new Recommendation(type, id);
switch (type) {
case Movie:
return movieRecommendations.contains(compare) ? movieRecommendations.get(movieRecommendations.indexOf(compare)) : null;
case Tv:
return tvRecommendations.contains(compare) ? tvRecommendations.get(tvRecommendations.indexOf(compare)) : null;
}
return null;
}
public boolean add(Recommendation rec) {
switch (rec.getType()) {
case Movie:
movieRecommendations.add(rec);
break;
case Tv:
tvRecommendations.add(rec);
break;
}
return true;
}
public Integer getRecId(RecommendationType type, int max) {
switch (type) {
case Movie:
if (movieRecommendations.size() < max) return 100 + movieRecommendations.size()+1;
return replaceOldest(type);
case Tv:
if (tvRecommendations.size() < max) return 200 + tvRecommendations.size()+1;
return replaceOldest(type);
}
throw new IllegalArgumentException("type");
}
private Integer replaceOldest(RecommendationType type) {
List<Recommendation> list = type == RecommendationType.Movie ? movieRecommendations : tvRecommendations;
Recommendation oldest = Collections.min(list, new Comparator<Recommendation>() {
@Override
public int compare(Recommendation lhs, Recommendation rhs) {
return Long.compare(lhs.getDateAdded(), rhs.getDateAdded());
}
});
list.remove(oldest);
((NotificationManager) TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE)).cancel(oldest.getRecId());
return oldest.getRecId();
}
public boolean remove(RecommendationType type, String id) {
List<Recommendation> list = type == RecommendationType.Movie ? movieRecommendations : tvRecommendations;
Recommendation existing = get(type, id);
if (existing != null) {
list.remove(existing);
((NotificationManager) TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE)).cancel(existing.getRecId());
return true;
}
return false;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getServerId() {
return serverId;
}
public void setServerId(String serverId) {
this.serverId = serverId;
}
}
@@ -52,7 +52,6 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.CustomMessage;
import org.jellyfin.androidtv.base.IMessageListener;
import org.jellyfin.androidtv.integration.RecommendationManager;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.livetv.ILiveTvGuide;
@@ -85,6 +84,8 @@ import java.util.Date;
import java.util.List;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
@@ -94,6 +95,24 @@ import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackEvent;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackInfo;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
public class CustomPlaybackOverlayFragment extends Fragment implements IPlaybackOverlayFragment, ILiveTvGuide {
ImageView mLogoImage;
View mTopPanel;
@@ -735,8 +754,6 @@ public class CustomPlaybackOverlayFragment extends Fragment implements IPlayback
public void onDestroy() {
super.onDestroy();
if (mPlaybackController != null) {
if (mPlaybackController.getCurrentlyPlayingItem() != null)
RecommendationManager.getInstance().recommend(mPlaybackController.getCurrentlyPlayingItem().getId());
mPlaybackController.removePreviousQueueItems();
}
}
@@ -134,7 +134,7 @@ public class ExternalPlayerActivity extends FragmentActivity {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences prefs = mApplication.getPrefs();
prefs.edit().putBoolean("pref_video_use_external", false).apply();
prefs.edit().putString("pref_video_player", "auto").apply();
prefs.edit().putBoolean("pref_live_tv_use_external", false).apply();
}
})
@@ -7,14 +7,18 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.text.InputType;
import android.widget.EditText;
import com.devbrackets.android.exomedia.EMAudioPlayer;
import com.devbrackets.android.exomedia.event.EMMediaProgressEvent;
import com.devbrackets.android.exomedia.listener.EMProgressCallback;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
@@ -32,6 +36,12 @@ import org.jellyfin.androidtv.util.RemoteControlReceiver;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationRequest;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationResult;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
@@ -41,13 +51,6 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationRequest;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationResult;
public class MediaManager {
private static ItemRowAdapter mCurrentMediaAdapter;
private static int mCurrentMediaPosition = -1;
@@ -63,7 +66,7 @@ public class MediaManager {
private static LibVLC mLibVLC;
private static org.videolan.libvlc.MediaPlayer mVlcPlayer;
private static VlcEventHandler mVlcHandler = new VlcEventHandler();
private static EMAudioPlayer mExoplayer;
private static SimpleExoPlayer mExoPlayer;
private static AudioManager mAudioManager;
private static boolean audioInitialized;
private static boolean nativeMode = false;
@@ -157,7 +160,7 @@ public class MediaManager {
}
private static boolean isPaused() {
return nativeMode ? !mExoplayer.isPlaying() : !mVlcPlayer.isPlaying();
return nativeMode ? !mExoPlayer.isPlaying() : !mVlcPlayer.isPlaying();
}
private static void reportProgress() {
@@ -165,7 +168,7 @@ public class MediaManager {
if (System.currentTimeMillis() < lastProgressEvent + 750) return;
lastProgressEvent = System.currentTimeMillis();
mCurrentAudioPosition = nativeMode ? mExoplayer.getCurrentPosition() : mVlcPlayer.getTime();
mCurrentAudioPosition = nativeMode ? mExoPlayer.getCurrentPosition() : mVlcPlayer.getTime();
//fire external listeners if there
for (AudioEventListener listener : mAudioEventListeners) {
@@ -199,19 +202,21 @@ public class MediaManager {
// Create a new media player based on platform
if (DeviceUtils.is60()) {
nativeMode = true;
mExoplayer = new EMAudioPlayer(TvApp.getApplication());
mExoplayer.setProgressCallback(new EMProgressCallback() {
mExoPlayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication());
mExoPlayer.addListener(new Player.EventListener() {
@Override
public boolean onProgressUpdated(EMMediaProgressEvent progressEvent) {
reportProgress();
return false;
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
startProgressLoop();
} else if (playbackState == Player.STATE_ENDED) {
onComplete();
stopProgressLoop();
}
}
});
mExoplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
onComplete();
public void onPlayerError(ExoPlaybackException error) {
stopProgressLoop();
}
});
} else {
@@ -253,6 +258,25 @@ public class MediaManager {
return true;
}
private static Runnable progressLoop;
private static Handler mHandler = new Handler();
private static void startProgressLoop() {
progressLoop = new Runnable() {
@Override
public void run() {
reportProgress();
mHandler.postDelayed(this, 500);
}
};
mHandler.post(progressLoop);
}
private static void stopProgressLoop() {
if (progressLoop != null) {
mHandler.removeCallbacks(progressLoop);
}
}
private static AudioManager.OnAudioFocusChangeListener mAudioFocusChanged = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -454,7 +478,7 @@ public class MediaManager {
}
}
public static boolean isPlayingAudio() { return audioInitialized && (nativeMode ? mExoplayer.isPlaying() : mVlcPlayer.isPlaying()); }
public static boolean isPlayingAudio() { return audioInitialized && (nativeMode ? mExoPlayer.isPlaying() : mVlcPlayer.isPlaying()); }
private static boolean ensureInitialized() {
if (!audioInitialized) {
@@ -564,8 +588,10 @@ public class MediaManager {
mCurrentAudioQueuePosition = pos;
mCurrentAudioPosition = 0;
if (nativeMode) {
mExoplayer.setDataSource(TvApp.getApplication(), Uri.parse(response.ToUrl(apiClient.getApiUrl(), apiClient.getAccessToken())));
mExoplayer.start();
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "ATV/ExoPlayer");
mExoPlayer.setPlayWhenReady(true);
mExoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(response.ToUrl(apiClient.getApiUrl(), apiClient.getAccessToken()))));
} else {
TvApp.getApplication().getLogger().Info("Playback attempt via VLC of " + response.getMediaUrl());
Media media = new Media(mLibVLC, Uri.parse(response.getMediaUrl()));
@@ -667,7 +693,7 @@ public class MediaManager {
if (mCurrentAudioQueue == null || (!mRepeat && mCurrentAudioQueue.size() == 0)) return -1;
if (isPlayingAudio() && mCurrentAudioPosition > 10000) {
//just back up to the beginning of current item
if (nativeMode) mExoplayer.seekTo(0);
if (nativeMode) mExoPlayer.seekTo(0);
else mVlcPlayer.setTime(0);
return mCurrentAudioQueuePosition;
}
@@ -689,7 +715,7 @@ public class MediaManager {
}
private static void stop() {
if (nativeMode) mExoplayer.stopPlayback();
if (nativeMode) mExoPlayer.stop(true);
else mVlcPlayer.stop();
}
@@ -708,7 +734,7 @@ public class MediaManager {
}
private static void pause() {
if (nativeMode) mExoplayer.pause();
if (nativeMode) mExoPlayer.setPlayWhenReady(false);
else mVlcPlayer.pause();
}
@@ -730,7 +756,7 @@ public class MediaManager {
public static void resumeAudio() {
if (mCurrentAudioItem != null) {
ensureAudioFocus();
if (nativeMode) mExoplayer.start();
if (nativeMode) mExoPlayer.setPlayWhenReady(true);
else mVlcPlayer.play();
updateCurrentAudioItemPlaying(true);
ReportingHelper.reportStart(mCurrentAudioItem, mCurrentAudioPosition * 10000);
@@ -24,9 +24,6 @@ import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import java.util.List;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
@@ -43,6 +40,8 @@ import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackInfo;
import org.jellyfin.apiclient.model.session.PlayMethod;
import java.util.List;
public class PlaybackController {
// Frequency to report playback progress
private final static long PROGRESS_REPORTING_INTERVAL = TimeUtils.secondsToMillis(3);
@@ -91,7 +90,7 @@ public class PlaybackController {
private boolean isLiveTv;
private boolean directStreamLiveTv;
private String liveTvChannelName = "";
private boolean useVlc = false;
private boolean useVlc;
private boolean vlcErrorEncountered;
private boolean exoErrorEncountered;
@@ -112,6 +111,9 @@ public class PlaybackController {
refreshRateSwitchingEnabled = DeviceUtils.is60() && mApplication.getPrefs().getBoolean("pref_refresh_switching", false);
if (refreshRateSwitchingEnabled) getDisplayModes();
// Set default value for useVlc field
// when set to auto the default will be exoplayer
useVlc = mApplication.getPrefs().getString("pref_video_player", "auto").equals("vlc");
}
public void init(VideoManager mgr, View spinner) {
@@ -473,26 +475,38 @@ public class PlaybackController {
vlcResponse.getMediaSource().getVideoStream().getWidth() > 1200);
mApplication.getLogger().Info(useDeinterlacing ? "Explicit deinterlacing will be used" : "Explicit deinterlacing will NOT be used");
// TODO: Clean up this logic
// Now look at both responses and choose the one that direct plays or bitstreams - favor VLC
useVlc = !vlcErrorEncountered &&
!vlcResponse.getPlayMethod().equals(PlayMethod.Transcode) &&
(DeviceUtils.is60() ||
!mApplication.getPrefs().getBoolean("pref_bitstream_ac3", false) ||
vlcResponse.getMediaSource() == null ||
vlcResponse.getMediaSource().getDefaultAudioStream() == null ||
(!"ac3".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()) &&
!"truehd".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()))) &&
(Utils.downMixAudio() ||
!DeviceUtils.is60() ||
internalResponse.getPlayMethod().equals(PlayMethod.Transcode) ||
!mApplication.getPrefs().getBoolean("pref_bitstream_dts", false) ||
internalResponse.getMediaSource() == null ||
internalResponse.getMediaSource().getDefaultAudioStream() == null ||
(!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dca") &&
!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dts"))) &&
(!DeviceUtils.isFireTvStick() ||
(vlcResponse.getMediaSource().getVideoStream() != null && vlcResponse.getMediaSource().getVideoStream().getWidth() < 1000));
String preferredVideoPlayer = mApplication.getPrefs().getString("pref_video_player", "auto");
mApplication.getLogger().Info("User preferred player is: " + preferredVideoPlayer);
if (preferredVideoPlayer.equals("vlc")) {
// Force VLC
useVlc = true;
} else if (preferredVideoPlayer.equals("exoplayer")) {
// Make sure to not use VLC
useVlc = false;
} else if (preferredVideoPlayer.equals("auto")) {
// TODO: Clean up this logic
// Now look at both responses and choose the one that direct plays or bitstreams - favor VLC
useVlc = !vlcErrorEncountered &&
!vlcResponse.getPlayMethod().equals(PlayMethod.Transcode) &&
(DeviceUtils.is60() ||
!mApplication.getPrefs().getBoolean("pref_bitstream_ac3", false) ||
vlcResponse.getMediaSource() == null ||
vlcResponse.getMediaSource().getDefaultAudioStream() == null ||
(!"ac3".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()) &&
!"truehd".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()))) &&
(Utils.downMixAudio() ||
!DeviceUtils.is60() ||
internalResponse.getPlayMethod().equals(PlayMethod.Transcode) ||
!mApplication.getPrefs().getBoolean("pref_bitstream_dts", false) ||
internalResponse.getMediaSource() == null ||
internalResponse.getMediaSource().getDefaultAudioStream() == null ||
(!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dca") &&
!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dts"))) &&
(!DeviceUtils.isFireTvStick() ||
(vlcResponse.getMediaSource().getVideoStream() != null && vlcResponse.getMediaSource().getVideoStream().getWidth() < 1000));
}
mApplication.getLogger().Info(useVlc ? "Preferring VLC" : "Will use internal player");
mVideoManager.init(getBufferAmount(), useDeinterlacing);
@@ -524,7 +538,6 @@ public class PlaybackController {
@Override
public void onError(Exception exception) {
mApplication.getLogger().ErrorException("Unable to get internal stream info", exception);
useVlc = true;
mCurrentOptions = vlcOptions;
startItem(item, position, vlcResponse);
}
@@ -568,7 +581,10 @@ public class PlaybackController {
setPlaybackMethod(response.getPlayMethod());
if (useVlc && (!getPlaybackMethod().equals(PlayMethod.Transcode) || isLiveTv)) {
// Force VLC when media is not live TV and the preferred player is VLC
boolean forceVlc = !isLiveTv && mApplication.getPrefs().getString("pref_video_player", "auto").equals("vlc");
if (forceVlc || (useVlc && (!getPlaybackMethod().equals(PlayMethod.Transcode) || isLiveTv))) {
TvApp.getApplication().getLogger().Info("Playing back in VLC.");
mVideoManager.setNativeMode(false);
} else {
@@ -3,7 +3,6 @@ package org.jellyfin.androidtv.playback;
import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.view.SurfaceHolder;
@@ -14,12 +13,22 @@ import android.widget.FrameLayout;
import androidx.leanback.media.PlayerAdapter;
import com.devbrackets.android.exomedia.EMVideoView;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.util.DeviceUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.MediaStreamType;
import org.videolan.libvlc.IVLCVout;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
@@ -27,10 +36,6 @@ import org.videolan.libvlc.Media;
import java.util.ArrayList;
import java.util.List;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.MediaStreamType;
public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public final static int ZOOM_NORMAL = 0;
@@ -45,7 +50,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private SurfaceView mSurfaceView;
private SurfaceView mSubtitlesSurface;
private FrameLayout mSurfaceFrame;
private EMVideoView mVideoView;
private SimpleExoPlayer mExoPlayer;
private PlayerView mExoPlayerView;
private LibVLC mLibVLC;
private org.videolan.libvlc.MediaPlayer mVlcPlayer;
private String mCurrentVideoPath;
@@ -70,6 +76,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private boolean mSurfaceReady = false;
public boolean isContracted = false;
private boolean hasSubtitlesSurface = false;
private PlaybackListener errorListener;
private PlaybackListener completionListener;
private PlaybackListener preparedListener;
public VideoManager(PlaybackOverlayActivity activity, View view) {
mActivity = activity;
@@ -85,7 +94,29 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
} else {
mSubtitlesSurface.setVisibility(View.GONE);
}
mVideoView = view.findViewById(R.id.videoView);
mExoPlayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication());
mExoPlayerView = view.findViewById(R.id.exoPlayerView);
mExoPlayerView.setPlayer(mExoPlayer);
mExoPlayer.addListener(new Player.EventListener() {
@Override
public void onPlayerError(ExoPlaybackException error) {
TvApp.getApplication().getLogger().Error("***** Got error from player");
if (errorListener != null) errorListener.onEvent();
stopProgressLoop();
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
// Do not call listener when paused
if (playbackState == Player.STATE_READY && playWhenReady) {
if (preparedListener != null) preparedListener.onEvent();
startProgressLoop();
} else if (playbackState == Player.STATE_ENDED) {
if (completionListener != null) completionListener.onEvent();
stopProgressLoop();
}
}
});
}
public void init(int buffer, boolean isInterlaced) {
@@ -95,9 +126,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void setNativeMode(boolean value) {
nativeMode = value;
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
mExoPlayerView.setVisibility(View.VISIBLE);
} else {
mVideoView.setVisibility(View.GONE);
mExoPlayerView.setVisibility(View.GONE);
}
}
@@ -108,20 +139,20 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
mZoomMode = mode;
switch (mode) {
case ZOOM_NORMAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1);
mExoPlayerView.setScaleY(1);
mExoPlayerView.setScaleX(1);
break;
case ZOOM_VERTICAL:
mVideoView.setScaleX(1);
mVideoView.setScaleY(1.33f);
mExoPlayerView.setScaleX(1);
mExoPlayerView.setScaleY(1.33f);
break;
case ZOOM_HORIZONTAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1.33f);
mExoPlayerView.setScaleY(1);
mExoPlayerView.setScaleX(1.33f);
break;
case ZOOM_FULL:
mVideoView.setScaleX(1.33f);
mVideoView.setScaleY(1.33f);
mExoPlayerView.setScaleX(1.33f);
mExoPlayerView.setScaleY(1.33f);
break;
}
@@ -133,14 +164,14 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public long getDuration() {
if (nativeMode){
return mVideoView.getDuration() > 0 ? mVideoView.getDuration() : mMetaDuration;
return mExoPlayer.getDuration() > 0 ? mExoPlayer.getDuration() : mMetaDuration;
} else {
return mVlcPlayer.getLength() > 0 ? mVlcPlayer.getLength() : mMetaDuration;
}
}
public long getCurrentPosition() {
if (nativeMode) return mVideoView.getCurrentPosition();
if (nativeMode) return mExoPlayer.getCurrentPosition();
if (mVlcPlayer == null) return 0;
@@ -164,17 +195,17 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
public boolean isPlaying() {
return nativeMode ? mVideoView.isPlaying() : mVlcPlayer != null && mVlcPlayer.isPlaying();
return nativeMode ? mExoPlayer.isPlaying() : mVlcPlayer != null && mVlcPlayer.isPlaying();
}
public boolean canSeek() { return nativeMode || mVlcPlayer.isSeekable(); }
public void start() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
normalWidth = mVideoView.getLayoutParams().width;
normalHeight = mVideoView.getLayoutParams().height;
mExoPlayer.setPlayWhenReady(true);
mExoPlayerView.setKeepScreenOn(true);
normalWidth = mExoPlayerView.getLayoutParams().width;
normalHeight = mExoPlayerView.getLayoutParams().height;
} else {
if (!mSurfaceReady) {
TvApp.getApplication().getLogger().Error("Attempt to play before surface ready");
@@ -190,8 +221,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void play() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
mExoPlayer.setPlayWhenReady(true);
mExoPlayerView.setKeepScreenOn(true);
} else {
mVlcPlayer.play();
mSurfaceView.setKeepScreenOn(true);
@@ -200,8 +231,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void pause() {
if (nativeMode) {
mVideoView.pause();
mVideoView.setKeepScreenOn(false);
mExoPlayer.setPlayWhenReady(false);
mExoPlayerView.setKeepScreenOn(false);
} else {
mVlcPlayer.pause();
mSurfaceView.setKeepScreenOn(false);
@@ -215,7 +246,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void stopPlayback() {
if (nativeMode) {
mVideoView.stopPlayback();
mExoPlayer.stop();
} else {
mVlcPlayer.stop();
}
@@ -225,8 +256,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public long seekTo(long pos) {
if (nativeMode) {
Long intPos = pos;
TvApp.getApplication().getLogger().Info("Exo length in seek is: " + mVideoView.getDuration());
mVideoView.seekTo(intPos.intValue());
TvApp.getApplication().getLogger().Info("Exo length in seek is: " + mExoPlayer.getDuration());
mExoPlayer.seekTo(intPos.intValue());
return pos;
} else {
if (mVlcPlayer == null || !mVlcPlayer.isSeekable()) return -1;
@@ -257,7 +288,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
if (nativeMode) {
try {
mVideoView.setVideoPath(path);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "ATV/ExoPlayer");
mExoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(path)));
} catch (IllegalStateException e) {
TvApp.getApplication().getLogger().ErrorException("Unable to set video path. Probably backing out.", e);
}
@@ -275,7 +308,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void hideSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.INVISIBLE);
mExoPlayerView.setVisibility(View.INVISIBLE);
} else {
mSurfaceView.setVisibility(View.INVISIBLE);
}
@@ -283,7 +316,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void showSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
mExoPlayerView.setVisibility(View.VISIBLE);
} else {
mSurfaceView.setVisibility(View.VISIBLE);
}
@@ -472,15 +505,22 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
private void releasePlayer() {
if (mVlcPlayer == null) return;
if (mVlcPlayer != null) {
mVlcPlayer.setEventListener(null);
mVlcPlayer.stop();
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.release();
mLibVLC.release();
mLibVLC = null;
mVlcPlayer = null;
}
if (mExoPlayer != null) {
mExoPlayerView.setPlayer(null);
mExoPlayer.release();
mExoPlayer = null;
}
mVlcPlayer.setEventListener(null);
mVlcPlayer.stop();
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.release();
mLibVLC.release();
mLibVLC = null;
mVlcPlayer = null;
mSurfaceView.setKeepScreenOn(false);
}
@@ -488,7 +528,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
int normalHeight;
public void contractVideo(int height) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mExoPlayerView.getLayoutParams() : mSurfaceView.getLayoutParams());
if (isContracted) return;
Activity activity = TvApp.getApplication().getCurrentActivity();
@@ -501,8 +541,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
lp.bottomMargin = ((lp.height - normalHeight) / 2) - 50;
if (nativeMode) {
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
mExoPlayerView.setLayoutParams(lp);
mExoPlayerView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = true;
@@ -511,14 +551,14 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void setVideoFullSize() {
if (normalHeight == 0) return;
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mExoPlayerView.getLayoutParams() : mSurfaceView.getLayoutParams());
lp.height = normalHeight;
lp.width = normalWidth;
if (nativeMode) {
lp.rightMargin = 0;
lp.bottomMargin = 0;
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
mExoPlayerView.setLayoutParams(lp);
mExoPlayerView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = false;
@@ -594,19 +634,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
if (hasSubtitlesSurface) mSubtitlesSurface.invalidate();
}
PlaybackListener errorListener;
public void setOnErrorListener(final PlaybackListener listener) {
mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
TvApp.getApplication().getLogger().Error("***** Got error from player");
listener.onEvent();
stopProgressLoop();
return true;
}
});
mVlcHandler.setOnErrorListener(listener);
errorListener = listener;
}
@@ -616,28 +644,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
public void setOnCompletionListener(final PlaybackListener listener) {
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
listener.onEvent();
stopProgressLoop();
}
});
completionListener = listener;
mVlcHandler.setOnCompletionListener(listener);
}
private MediaPlayer mNativeMediaPlayer;
public void setOnPreparedListener(final PlaybackListener listener) {
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mNativeMediaPlayer = mp;
listener.onEvent();
startProgressLoop();
}
});
preparedListener = listener;
mVlcHandler.setOnPreparedListener(listener);
}
@@ -0,0 +1,29 @@
package org.jellyfin.androidtv.search;
import android.os.Bundle;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import androidx.leanback.app.SearchSupportFragment;
import androidx.leanback.widget.ListRow;
public class LeanbackSearchFragment extends SearchSupportFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create provider
SearchProvider searchProvider = new SearchProvider(getContext(), getActivity().getIntent().getBooleanExtra("MusicOnly", false));
setSearchResultProvider(searchProvider);
// Create click listener
setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, row) -> {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
});
}
}
@@ -1,113 +0,0 @@
package org.jellyfin.androidtv.search;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.SearchFragment;
import androidx.leanback.app.SearchSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.util.DisplayMetrics;
import com.squareup.picasso.Target;
import org.jellyfin.androidtv.imagehandling.PicassoBackgroundManagerTarget;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.util.Utils;
import java.util.Timer;
/**
* Created by Eric on 1/26/2015.
*/
public class MySearchFragment extends SearchSupportFragment
implements SearchSupportFragment.SearchResultProvider {
private static final int SEARCH_DELAY_MS = 1500;
private final Handler mHandler = new Handler();
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
private Drawable mDefaultBackground;
private Target mBackgroundTarget;
private DisplayMetrics mMetrics;
private Timer mBackgroundTimer;
private String mBackgroundUrl;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setSearchResultProvider(this);
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(),((BaseRowItem) item).getIndex(), getActivity());
}
});
mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly",false));
prepareBackgroundManager();
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
private boolean textEdited = false;
@Override
public boolean onQueryTextChange(String newQuery) {
textEdited = true;
if (Utils.isNonEmpty(newQuery)) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(newQuery);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
if (!textEdited) {
if (Utils.isNonEmpty(query)) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(query);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
}
return true;
}
private void prepareBackgroundManager() {
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mDefaultBackground = null;
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
}
@@ -1,108 +0,0 @@
package org.jellyfin.androidtv.search;
import androidx.fragment.app.Fragment;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
/**
* Created by Eric on 4/8/2015.
*/
public class MySearchFragmentNoSpeech extends Fragment {
private static final int SEARCH_DELAY_MS = 1500;
private RowsSupportFragment mRowsFragment;
private EditText mSearchField;
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
private Handler mHandler = new Handler();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_empty_frame, container, false);
// Inject the RowsSupportFragment in the results container
if (getChildFragmentManager().findFragmentById(R.id.emptyFrame) == null) {
mRowsFragment = new RowsSupportFragment();
getChildFragmentManager().beginTransaction()
.replace(R.id.emptyFrame, mRowsFragment).commit();
} else {
mRowsFragment = (RowsSupportFragment) getChildFragmentManager()
.findFragmentById(R.id.emptyFrame);
}
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
mRowsFragment.setAdapter(mRowsAdapter);
// Setup item selection
mRowsFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
}
});
mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly",false));
return root;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Setup search field listening
mSearchField = (EditText) getActivity().findViewById(R.id.searchField);
mSearchField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() > 0) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(s.toString());
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
}
});
}
}
@@ -3,26 +3,34 @@ package org.jellyfin.androidtv.search;
import android.os.Bundle;
import android.speech.SpeechRecognizer;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.BaseActivity;
import androidx.fragment.app.Fragment;
public class SearchActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (TvApp.getApplication().isVoiceSearchAllowed() &&
SpeechRecognizer.isRecognitionAvailable(TvApp.getApplication())) {
setContentView(R.layout.activity_search);
} else {
setContentView(R.layout.activity_search_no_speech);
}
boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(this);
// Determine fragment to use
Fragment searchFragment = isSpeechEnabled
? new LeanbackSearchFragment()
: new TextSearchFragment();
// Add fragment
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, searchFragment)
.commit();
}
@Override
public boolean onSearchRequested() {
//re-start us
this.recreate();
// Reset layout
recreate();
return true;
}
}
@@ -0,0 +1,67 @@
package org.jellyfin.androidtv.search;
import android.content.Context;
import android.os.Handler;
import org.jellyfin.androidtv.util.Utils;
import androidx.leanback.app.SearchSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.ObjectAdapter;
public class SearchProvider implements SearchSupportFragment.SearchResultProvider {
private static final int SEARCH_DELAY_MS = 1500;
private final Handler mHandler = new Handler();
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
SearchProvider(Context context, boolean musicOnly) {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
mDelayedLoad = new SearchRunnable(context, mRowsAdapter, musicOnly);
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
@Override
public boolean onQueryTextChange(String query) {
search(query, true);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
search(query, false);
return true;
}
/**
* Update search results
*
* @param query String to search for
* @param delayed When true the search is delayed by [SEARCH_DELAY_MS] milliseconds
*/
private void search(String query, boolean delayed) {
// Clear results when query is empty
if (Utils.isEmpty(query)) {
mRowsAdapter.clear();
return;
}
// Remove current delayed search (if any)
mHandler.removeCallbacks(mDelayedLoad);
// Update search string
mDelayedLoad.setQueryString(query);
// Schedule search depending on [delayed]
if (delayed) mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
else mHandler.post(mDelayedLoad);
}
}
@@ -1,22 +1,22 @@
package org.jellyfin.androidtv.search;
import android.app.Activity;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import android.content.Context;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.apiclient.model.search.SearchQuery;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
/**
* Created by Eric on 4/8/2015.
*/
public class SearchRunnable implements Runnable {
private String searchString;
private Activity activity;
private Context context;
private ArrayObjectAdapter mRowsAdapter;
private boolean musicOnly;
@@ -24,8 +24,8 @@ public class SearchRunnable implements Runnable {
searchString = value;
}
public SearchRunnable(Activity activity, ArrayObjectAdapter adapter, boolean musicOnly) {
this.activity = activity;
public SearchRunnable(Context context, ArrayObjectAdapter adapter, boolean musicOnly) {
this.context = context;
this.mRowsAdapter = adapter;
this.musicOnly = musicOnly;
}
@@ -38,56 +38,56 @@ public class SearchRunnable implements Runnable {
//Get search results by type
SearchQuery people = getSearchQuery(new String[] {"Person","People"});
ItemRowAdapter peopleAdapter = new ItemRowAdapter(people, new CardPresenter(), mRowsAdapter);
ListRow peopleRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_people)), peopleAdapter);
ListRow peopleRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_people)), peopleAdapter);
peopleAdapter.setRow(peopleRow);
peopleAdapter.Retrieve();
SearchQuery movies = getSearchQuery(new String[] {"Movie", "BoxSet"});
ItemRowAdapter movieAdapter = new ItemRowAdapter(movies, new CardPresenter(), mRowsAdapter);
ListRow movieRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_movies)), movieAdapter);
ListRow movieRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_movies)), movieAdapter);
movieAdapter.setRow(movieRow);
movieAdapter.Retrieve();
SearchQuery tv = getSearchQuery(new String[] {"Series","Episode"});
ItemRowAdapter tvAdapter = new ItemRowAdapter(tv, new CardPresenter(), mRowsAdapter);
ListRow tvRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_tv)), tvAdapter);
ListRow tvRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_tv)), tvAdapter);
tvAdapter.setRow(tvRow);
tvAdapter.Retrieve();
SearchQuery videos = getSearchQuery(new String[] {"Video"});
ItemRowAdapter videoAdapter = new ItemRowAdapter(videos, new CardPresenter(), mRowsAdapter);
ListRow videoRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_videos)), videoAdapter);
ListRow videoRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_videos)), videoAdapter);
videoAdapter.setRow(videoRow);
videoAdapter.Retrieve();
SearchQuery recordings = getSearchQuery(new String[] {"Recording"});
ItemRowAdapter recordingAdapter = new ItemRowAdapter(recordings, new CardPresenter(), mRowsAdapter);
ListRow recordingRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_recordings)), recordingAdapter);
ListRow recordingRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_recordings)), recordingAdapter);
recordingAdapter.setRow(recordingRow);
recordingAdapter.Retrieve();
SearchQuery programs = getSearchQuery(new String[] {"Program"});
ItemRowAdapter programAdapter = new ItemRowAdapter(programs, new CardPresenter(), mRowsAdapter);
ListRow programRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_programs)), programAdapter);
ListRow programRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_programs)), programAdapter);
programAdapter.setRow(programRow);
programAdapter.Retrieve();
}
SearchQuery artists = getSearchQuery(new String[] {"MusicArtist"});
ItemRowAdapter artistAdapter = new ItemRowAdapter(artists, new CardPresenter(), mRowsAdapter);
ListRow artistRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_artists)), artistAdapter);
ListRow artistRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_artists)), artistAdapter);
artistAdapter.setRow(artistRow);
artistAdapter.Retrieve();
SearchQuery albums = getSearchQuery(new String[] {"MusicAlbum"});
ItemRowAdapter albumAdapter = new ItemRowAdapter(albums, new CardPresenter(), mRowsAdapter);
ListRow albumRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_albums)), albumAdapter);
ListRow albumRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_albums)), albumAdapter);
albumAdapter.setRow(albumRow);
albumAdapter.Retrieve();
SearchQuery songs = getSearchQuery(new String[] {"Audio"});
ItemRowAdapter songAdapter = new ItemRowAdapter(songs, new CardPresenter(), mRowsAdapter);
ListRow songRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_songs)), songAdapter);
ListRow songRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_songs)), songAdapter);
songAdapter.setRow(songRow);
songAdapter.Retrieve();
@@ -0,0 +1,84 @@
package org.jellyfin.androidtv.search;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import androidx.fragment.app.Fragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ListRow;
public class TextSearchFragment extends Fragment implements TextWatcher, TextView.OnEditorActionListener {
private SearchProvider searchProvider;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
searchProvider = new SearchProvider(getContext(), getActivity().getIntent().getBooleanExtra("MusicOnly", false));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search_text, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Add event listeners
EditText searchBar = getActivity().findViewById(R.id.search_bar);
searchBar.addTextChangedListener(this);
searchBar.setOnEditorActionListener(this);
// Set up result fragment
RowsSupportFragment rowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(R.id.results_frame);
rowsSupportFragment.setAdapter(searchProvider.getResultsAdapter());
// Create click listener
rowsSupportFragment.setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, row) -> {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
});
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// No implementation
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// No implementation
}
@Override
public void afterTextChanged(Editable s) {
searchProvider.onQueryTextChange(s.toString());
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// Detect keyboard "submit" actions
if (actionId == EditorInfo.IME_ACTION_SEARCH)
searchProvider.onQueryTextSubmit(v.getText().toString());
// Return "false" to automatically close keyboard
return false;
}
}
@@ -113,7 +113,7 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
if (!TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getId().equals(TvApp.getApplication().getCurrentUser().getId())) {
listPreference.setEnabled(false);
pwPrompt.setEnabled(false);
extra = getActivity().getString(R.string.lbl_paren_login_as)+TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName()+getActivity().getString(R.string.lbl_to_change_paren);
extra = getActivity().getString(R.string.lbl_paren_login_as) + TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName() + getActivity().getString(R.string.lbl_to_change_paren);
}
listPreference.setSummary(getActivity().getString(R.string.lbl_login_as) + TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName() + extra);
} else {
@@ -121,6 +121,18 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
pwPrompt.setEnabled(false);
}
} else {
if (listPreference.getKey().equals("pref_video_player")) {
boolean isExternal = listPreference.getValue().equals("external");
// enable/disable other related items
Preference direct = findPreference("pref_send_path_external");
if (direct != null) direct.setEnabled(isExternal);
for (String key : extPlayerVideoDep) {
Preference pref = findPreference(key);
if (pref != null) pref.setEnabled(!isExternal);
}
}
listPreference.setSummary(listPreference.getEntry());
}
}
@@ -131,14 +143,6 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
// enable other live tv direct only options
Preference live = findPreference("pref_enable_vlc_livetv");
if (live != null) live.setEnabled(cb.isChecked());
} else if (cb.getKey().equals("pref_video_use_external")) {
// enable/disable other related items
Preference direct = findPreference("pref_send_path_external");
if (direct != null) direct.setEnabled(cb.isChecked());
for (String key: extPlayerVideoDep) {
Preference pref = findPreference(key);
if (pref != null) pref.setEnabled(!cb.isChecked());
}
} else if (cb.getKey().equals("pref_live_tv_use_external")) {
// enable/disable other related items
for (String key: extPlayerLiveTvDep) {
@@ -70,6 +70,12 @@ public class StartupActivity extends FragmentActivity {
application.getPrefs().edit().putBoolean("pref_enable_info_panel", false).apply();
application.getSystemPrefs().edit().putString("sys_pref_config_version", "4").apply();
}
if (Integer.parseInt(application.getConfigVersion()) < 5) {
boolean useExternal = application.getPrefs().getBoolean("pref_video_use_external", false);
application.getPrefs().edit().putString("pref_video_player", useExternal ? "external" : "auto").apply();
application.getSystemPrefs().edit().putString("sys_pref_config_version", "5").apply();
}
//Ensure we have prefs
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
@@ -0,0 +1,18 @@
package org.jellyfin.androidtv.util.apiclient
import org.jellyfin.apiclient.interaction.ApiClient
import org.jellyfin.apiclient.interaction.Response
import org.jellyfin.apiclient.model.querying.ItemsResult
import org.jellyfin.apiclient.model.querying.NextUpQuery
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Adds a coroutine capable version of the "getNextUpEpisodes" function
*/
suspend fun ApiClient.getNextUpEpisodes(query: NextUpQuery): ItemsResult? = suspendCoroutine { continuation ->
GetNextUpEpisodesAsync(query, object : Response<ItemsResult>() {
override fun onResponse(response: ItemsResult?) = continuation.resume(response!!)
override fun onError(exception: Exception?) = continuation.resume(null)
})
}
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/search_fragment"
android:name="org.jellyfin.androidtv.search.MySearchFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ByLetterActivity"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame"
tools:layout="@layout/activity_search" />
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/searchField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="45dp"
android:enabled="true"
android:focusable="true"
android:gravity="center_vertical"
android:hint="@string/lbl_search_hint"
android:minWidth="400sp"
android:minHeight="30sp"
android:textSize="18sp" />
<fragment
android:id="@+id/fragment_search_no_speech"
android:name="org.jellyfin.androidtv.search.MySearchFragmentNoSpeech"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="120sp"
tools:layout="@layout/fragment_empty_frame" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/searchField"
android:layout_alignBottom="@+id/searchField"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_toStartOf="@+id/searchField"
android:src="@drawable/lb_ic_in_app_search" />
</RelativeLayout>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/emptyFrame"/>
</FrameLayout>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Search bar + icon -->
<EditText
android:id="@+id/search_bar"
android:layout_width="600dp"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:hint="@string/lbl_search_hint"
android:imeOptions="normal|flagNoExtractUi|actionSearch"
android:inputType="text"
android:singleLine="true"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:src="@drawable/lb_ic_in_app_search"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar" />
<!-- Fragment showing search results -->
<fragment
android:id="@+id/results_frame"
android:name="androidx.leanback.app.RowsSupportFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/black">
<FrameLayout
@@ -30,12 +31,11 @@
</FrameLayout>
<com.devbrackets.android.exomedia.EMVideoView
android:id="@+id/videoView"
android:layout_width="fill_parent"
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/exoPlayerView"
app:use_controller="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
<fragment
@@ -160,8 +160,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/nextButton"
android:layout_alignStart="@+id/nextButton"
android:layout_alignEnd="@+id/nextButton"
android:layout_alignStart="@+id/nextButton"
android:layout_marginTop="5sp"
android:background="@drawable/jellyfin_button"
android:minWidth="120sp"
@@ -179,9 +179,9 @@
<LinearLayout
android:id="@+id/rows_area"
android:layout_marginTop="16dp"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginTop="16dp"
android:layout_gravity="start|bottom"
android:orientation="horizontal" />
</FrameLayout>
-2
View File
@@ -274,8 +274,6 @@
<string name="pref_use_direct_path_title">Přímý průchod</string>
<string name="pref_external_live_tv_summary">Vyžaduje vhodnou aplikaci pro přehrávání live streamů</string>
<string name="pref_external_live_tv_title">Použít externí přehrávač pro Live TV</string>
<string name="pref_use_external_player_summary">Vyžaduje vhodnou aplikaci pro přehrávání videa</string>
<string name="pref_use_external_player_title">Použít externí přehrávač pro videa</string>
<string name="lbl_song_title">Název skladby</string>
<string name="lbl_album_name">Název alba</string>
<string name="lbl_next_episode">Další epizoda</string>
-2
View File
@@ -259,8 +259,6 @@
<string name="pref_use_direct_path_title">Direkten Pfad übergeben</string>
<string name="pref_external_live_tv_summary">Benötigt eine Anwendung, welche Livestreams wiedergeben kann</string>
<string name="pref_external_live_tv_title">Externen Player für Live TV verwenden</string>
<string name="pref_use_external_player_summary">Benötigt eine Anwendung, welche die Videodateien wiedergeben kann</string>
<string name="pref_use_external_player_title">Externen Player für Videos verwenden</string>
<string name="lbl_song_title">Songtitel</string>
<string name="lbl_album_name">Album Name</string>
<string name="lbl_next_episode">Nächste Episode</string>
@@ -19,8 +19,6 @@
<string name="pref_use_direct_path_title">Pass Direct Path</string>
<string name="pref_external_live_tv_summary">Requires a capable app to play live streams</string>
<string name="pref_external_live_tv_title">Use External Player for Live TV</string>
<string name="pref_use_external_player_summary">Requires a capable app to play video types</string>
<string name="pref_use_external_player_title">Use External Player for Videos</string>
<string name="lbl_song_title">Song Title</string>
<string name="lbl_album_name">Album name</string>
<string name="lbl_next_episode">Next Episode</string>
@@ -264,8 +264,6 @@
<string name="pref_use_direct_path_title">Pasar ruta directa</string>
<string name="pref_external_live_tv_summary">Requiere una aplicación capaz para reproducir transmisiones en vivo</string>
<string name="pref_external_live_tv_title">Usar reproductor externo para TV en vivo</string>
<string name="pref_use_external_player_summary">Requiere una aplicación capaz para reproducir tipos de vídeo</string>
<string name="pref_use_external_player_title">Usar un reproductor externo para vídeos</string>
<string name="lbl_song_title">Titulo de Canción</string>
<string name="lbl_album_name">Nombre de álbum</string>
<string name="lbl_next_episode">Siguiente Episodio</string>
-1
View File
@@ -189,7 +189,6 @@
<string name="pref_use_direct_path_title">Pasar ruta directa</string>
<string name="pref_external_live_tv_summary">Requiere una aplicación capaz para reproducir transmisiones en vivo</string>
<string name="pref_external_live_tv_title">Usar reproductor externo para la televisión en vivo</string>
<string name="pref_use_external_player_title">Usar un reproductor externo para vídeos</string>
<string name="lbl_song_title">Título de la canción</string>
<string name="lbl_album_name">Nombre del álbum</string>
<string name="lbl_next_episode">Próximo episodio</string>
-2
View File
@@ -292,8 +292,6 @@
<string name="lbl_next_episode">Épisode suivant</string>
<string name="lbl_album_name">Nom de l\'album</string>
<string name="lbl_song_title">titre de la chanson</string>
<string name="pref_use_external_player_title">Utiliser un lecteur externe pour les vidéos</string>
<string name="pref_use_external_player_summary">Nécessite une application capable de lire les vidéos</string>
<string name="pref_use_direct_path_title">Utiliser le chemin directe</string>
<string name="pref_use_direct_path_summary">Utiliser le chemin du fichier de l\'url de streaming</string>
<string name="lbl_now_playing_album">de %s</string>
-2
View File
@@ -288,7 +288,6 @@
<string name="pref_live_tv_mode_desc">Eń keıingi qaralǵan efırlik arnaǵa dereý ótý</string>
<string name="lbl_image_size">Sýret ólshemi</string>
<string name="pref_external_live_tv_summary">Tikeleı aǵymdardy oınatý úshin jaramdy qoldanbany qajet etedi</string>
<string name="pref_use_external_player_summary">Beıne túrlerin oınatý úshin jaramdy qoldanbany qajet etedi</string>
<string name="msg_error_server_version">Qoldaý kórsetilmeıtin server nusqasy. Server nusqasy %1$s nemese odan keıingi bolýy qajet.</string>
<string name="lbl_image_type">Sýret túri</string>
<string name="lbl_default_view">Ádepki kórinis</string>
@@ -312,7 +311,6 @@
<string name="lbl_suggestions">Usynystar</string>
<string name="pref_use_direct_path_title">Tikeleı shyǵarý</string>
<string name="pref_external_live_tv_title">Efır úshin syrtqy oınatqyshty paıdalaný</string>
<string name="pref_use_external_player_title">Beıneler úshin syrtqy oınatqyshty paıdalaný</string>
<string name="lbl_song_title">Áýen ataýy</string>
<string name="lbl_next_episode">Kelesi bólimge</string>
<string name="lbl_previous_episode">Aldyńǵy bólimge</string>
-2
View File
@@ -212,8 +212,6 @@
<string name="lbl_next_episode">다음 에피소드</string>
<string name="lbl_album_name">앨범 이름</string>
<string name="lbl_song_title">노래 제목</string>
<string name="pref_use_external_player_title">비디오에 외부 재생기 사용</string>
<string name="pref_use_external_player_summary">비디오 타입을 재생하려면 호환되는 앱이 필요합니다</string>
<string name="pref_external_live_tv_title">실시간 TV에 외부 재생기 사용</string>
<string name="pref_external_live_tv_summary">실시간 스트림을 재생하려면 호환되는 앱이 필요합니다</string>
</resources>
-2
View File
@@ -228,8 +228,6 @@
<string name="pref_use_direct_path_title">Send filbanen direkte</string>
<string name="pref_external_live_tv_summary">Krever en støttet app for å spille av direktestrømmer</string>
<string name="pref_external_live_tv_title">Bruk eksterm spiller for direkte-TV</string>
<string name="pref_use_external_player_summary">Krever en støttet app for å spille av videotyper</string>
<string name="pref_use_external_player_title">Bruk ekstern spiller for videoer</string>
<string name="lbl_song_title">Låtnavn</string>
<string name="lbl_album_name">Albumnavn</string>
<string name="lbl_next_episode">Neste episode</string>
-2
View File
@@ -267,7 +267,6 @@
<string name="title_suggestions_holiday">Fijne feestdagen!</string>
<string name="lbl_suggestions">Suggesties</string>
<string name="pref_external_live_tv_title">Gebruik externe speler voor Live TV</string>
<string name="pref_use_external_player_title">Gebruik externe speler voor video\'s</string>
<string name="lbl_album_name">Album naam</string>
<string name="lbl_next_episode">Volgende Aflevering</string>
<string name="lbl_previous_episode">Vorige Aflevering</string>
@@ -301,7 +300,6 @@
<string name="title_suggestions_halloween">Fijne Halloween!</string>
<string name="lbl_suggestions_halloween">Griezelige suggesties</string>
<string name="pref_external_live_tv_summary">Heeft een bruikbare app nodig om live streams af te spelen</string>
<string name="pref_use_external_player_summary">Heeft een bruikbare app nodig om video types af te spelen</string>
<string name="lbl_song_title">Titel van het nummer</string>
<string name="lbl_more_like_this">Meer als dit</string>
<string name="lbl_resume_from">Hervat vanaf %s</string>
@@ -250,7 +250,6 @@
<string name="pref_use_direct_path_summary">Enviar o local do sistema de arquivos invés da URL de streaming</string>
<string name="pref_external_live_tv_summary">Requer um app compatível de reproduzir streams ao vivo</string>
<string name="pref_external_live_tv_title">Usar Reprodutor Externo para TV ao Vivo</string>
<string name="pref_use_external_player_summary">Requer um app compatível para reproduzir os tipos de vídeo</string>
<string name="lbl_song_title">Título da Música</string>
<string name="lbl_album_name">Nome do álbum</string>
<string name="lbl_next_episode">Próximo Episódio</string>
@@ -311,7 +310,6 @@
<string name="title_suggestions_halloween">Feliz Halloween!</string>
<string name="lbl_suggestions_halloween">Sugestões Assustadoras</string>
<string name="lbl_suggestions">Sugestões</string>
<string name="pref_use_external_player_title">Usar Player Externo para Vídeos</string>
<string name="lbl_series_recordings">Séries Gravadas</string>
<string name="msg_error_signin">Ocorreu um erro ao fazer login com suas credenciais salvas.</string>
<string name="msg_error_server_version">Versão do servidor não suportada. Versão do servidor deve ser %1$s ou posterior.</string>
-2
View File
@@ -251,7 +251,6 @@
<string name="lbl_bitstream_dts">Транслировать DTS аудио</string>
<string name="lbl_favorite">Избранное</string>
<string name="pref_external_live_tv_title">Использовать для эфира внешний проигрыватель</string>
<string name="pref_use_external_player_title">Использовать для видео внешний проигрыватель</string>
<string name="lbl_song_title">Название композиции</string>
<string name="lbl_album_name">Название альбома</string>
<string name="lbl_next_episode">Следующий эпизод</string>
@@ -290,7 +289,6 @@
<string name="pref_use_direct_path_summary">Выводить путь в файловой системе вместо URL потока</string>
<string name="pref_use_direct_path_title">Прямой вывод</string>
<string name="pref_external_live_tv_summary">Требуется совместимое приложение для воспроизведения эфирных трансляций</string>
<string name="pref_use_external_player_summary">Требуется совместимое приложение для воспроизведения видеотипов</string>
<string name="lbl_more_like_this">Ещё подобное этому</string>
<string name="lbl_resume_preroll">Откат для возобновления</string>
<string name="lbl_guide_option_played">Последнее воспроизведение</string>
@@ -151,8 +151,6 @@
<string name="pref_use_direct_path_title">通过直接路径</string>
<string name="pref_external_live_tv_summary">需要一个有能力的应用程序播放直播流</string>
<string name="pref_external_live_tv_title">使用外部播放器播放电视直播</string>
<string name="pref_use_external_player_summary">需要一个有能力的应用程序来播放视频类型</string>
<string name="pref_use_external_player_title">使用外部播放器播放视频</string>
<string name="lbl_song_title">歌曲标题</string>
<string name="lbl_album_name">专辑名</string>
<string name="lbl_next_episode">下一个事件</string>
+12
View File
@@ -101,4 +101,16 @@
<item>0</item>
<item>1</item>
</string-array>
<string-array name="pref_video_player_entries" >
<item>@string/pref_video_player_auto</item>
<item>@string/pref_video_player_exoplayer</item>
<item>@string/pref_video_player_vlc</item>
<item>@string/pref_video_player_external</item>
</string-array>
<string-array name="pref_video_player_values" >
<item>auto</item>
<item>exoplayer</item>
<item>vlc</item>
<item>external</item>
</string-array>
</resources>
+5 -3
View File
@@ -305,11 +305,9 @@
<string name="lbl_next_episode">Next Episode</string>
<string name="lbl_album_name">Album name</string>
<string name="lbl_song_title">Song Title</string>
<string name="pref_use_external_player_title">Use External Player for Videos</string>
<string name="pref_use_external_player_summary">Requires a capable app to play video types</string>
<string name="pref_external_live_tv_title">Use External Player for Live TV</string>
<string name="pref_external_live_tv_summary">Requires a capable app to play live streams</string>
<string name="pref_use_direct_path_title">Pass Direct Path</string>
<string name="pref_use_direct_path_title">Pass Direct Path for external player</string>
<string name="pref_use_direct_path_summary">Pass the file system path instead of streaming URL</string>
<string name="pref_acra_category">Crash Reporting</string>
<string name="pref_enable_acra">Send crash reports to developers</string>
@@ -345,4 +343,8 @@
<string name="lbl_image_type">Image Type</string>
<string name="lbl_image_size">Image Size</string>
<string name="lbl_my_media">My Media</string>
<string name="pref_video_player_auto">Automatically choose</string>
<string name="pref_video_player_exoplayer">ExoPlayer</string>
<string name="pref_video_player_vlc">libVLC</string>
<string name="pref_video_player_external">External app</string>
</resources>
+5 -5
View File
@@ -75,6 +75,11 @@
android:entries="@array/pref_preroll_options"
android:entryValues="@array/pref_preroll_values"
/>
<ListPreference android:key="pref_video_player"
android:title="Preferred media player"
android:defaultValue="auto"
android:entries="@array/pref_video_player_entries"
android:entryValues="@array/pref_video_player_values" />
<CheckBoxPreference android:key="pref_enable_cinema_mode"
android:title="@string/lbl_enable_cinema_mode"
android:defaultValue="true"
@@ -100,11 +105,6 @@
android:title="@string/lbl_refresh_switching"
android:defaultValue="true"
/>
<CheckBoxPreference android:key="pref_video_use_external"
android:title="@string/pref_use_external_player_title"
android:defaultValue="false"
android:summary="@string/pref_use_external_player_summary"
/>
<CheckBoxPreference android:key="pref_send_path_external"
android:title="@string/pref_use_direct_path_title"
android:defaultValue="false"
+1 -14
View File
@@ -1,16 +1,3 @@
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Fri Jan 03 01:05:56 CET 2020
org.gradle.jvmargs=-Xmx768m
android.enableJetifier=true
org.gradle.jvmargs=-Xmx1024M
android.useAndroidX=true