mirror of
https://github.com/jellyfin/jellyfin-androidtv.git
synced 2026-10-26 16:45:23 +00:00
Merge branch 'master' into player-overlay-redesign
This commit is contained in:
49 files changed
+690
-1405
No files matched your search
+6
-17
@@ -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()
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}}
|
||||
-157
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user