From ec83261b4313de5486659860066e880fc86e9120 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 29 Dec 2019 17:43:09 +0100 Subject: [PATCH 01/27] Remove MySearchFragmentNoSpeech and custom speech permission logic (is now handled by androidx SearchSupportFragment) --- .../java/org/jellyfin/androidtv/TvApp.java | 52 ++------- .../jellyfin/androidtv/base/BaseActivity.java | 20 +--- .../search/MySearchFragmentNoSpeech.java | 108 ------------------ .../androidtv/search/SearchActivity.java | 15 +-- 4 files changed, 15 insertions(+), 180 deletions(-) delete mode 100644 app/src/main/java/org/jellyfin/androidtv/search/MySearchFragmentNoSpeech.java diff --git a/app/src/main/java/org/jellyfin/androidtv/TvApp.java b/app/src/main/java/org/jellyfin/androidtv/TvApp.java index ff3a4bb7f..ff0b7df0d 100644 --- a/app/src/main/java/org/jellyfin/androidtv/TvApp.java +++ b/app/src/main/java/org/jellyfin/androidtv/TvApp.java @@ -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,27 +227,7 @@ 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) { @@ -553,14 +523,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; } diff --git a/app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java b/app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java index 97a518088..7b74f9946 100644 --- a/app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java @@ -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); - } - } } diff --git a/app/src/main/java/org/jellyfin/androidtv/search/MySearchFragmentNoSpeech.java b/app/src/main/java/org/jellyfin/androidtv/search/MySearchFragmentNoSpeech.java deleted file mode 100644 index 028f639a8..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/search/MySearchFragmentNoSpeech.java +++ /dev/null @@ -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(); - } - } - }); - - } -} diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java index b9e2aae28..5bdac16b7 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java @@ -1,28 +1,23 @@ 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; 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); - } + + setContentView(R.layout.activity_search); } @Override public boolean onSearchRequested() { - //re-start us - this.recreate(); + // Reset layout + recreate(); + return true; } } From 91bef5afc602a31e637d21824e6c9692a22f2449 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 29 Dec 2019 18:07:09 +0100 Subject: [PATCH 02/27] Refactor SearchFragment --- ...earchFragment.java => SearchFragment.java} | 88 +++++++------------ app/src/main/res/layout/activity_search.xml | 4 +- 2 files changed, 35 insertions(+), 57 deletions(-) rename app/src/main/java/org/jellyfin/androidtv/search/{MySearchFragment.java => SearchFragment.java} (52%) diff --git a/app/src/main/java/org/jellyfin/androidtv/search/MySearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java similarity index 52% rename from app/src/main/java/org/jellyfin/androidtv/search/MySearchFragment.java rename to app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java index 120f598bc..eb55b18fd 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/MySearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java @@ -1,10 +1,13 @@ 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 org.jellyfin.androidtv.itemhandling.BaseRowItem; +import org.jellyfin.androidtv.itemhandling.ItemLauncher; +import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; +import org.jellyfin.androidtv.util.Utils; + import androidx.leanback.app.SearchSupportFragment; import androidx.leanback.widget.ArrayObjectAdapter; import androidx.leanback.widget.ListRow; @@ -14,22 +17,11 @@ 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 +public class SearchFragment extends SearchSupportFragment implements SearchSupportFragment.SearchResultProvider { private static final int SEARCH_DELAY_MS = 1500; @@ -37,13 +29,6 @@ public class MySearchFragment extends SearchSupportFragment 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); @@ -54,12 +39,10 @@ public class MySearchFragment extends SearchSupportFragment @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()); + ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity()); } }); - mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly",false)); - - prepareBackgroundManager(); + mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly", false)); } @Override @@ -67,47 +50,42 @@ public class MySearchFragment extends SearchSupportFragment 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(); - } + public boolean onQueryTextChange(String query) { + search(query, true); + 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(); - } - } + search(query, false); + return true; } - private void prepareBackgroundManager() { + /** + * 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; + } - BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); - backgroundManager.attach(getActivity().getWindow()); - mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager); + // Remove current delayed search (if any) + mHandler.removeCallbacks(mDelayedLoad); - mDefaultBackground = null; + // Update search string + mDelayedLoad.setQueryString(query); - mMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics); + // Schedule search depending on [delayed] + if (delayed) mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS); + else mHandler.post(mDelayedLoad); } - - } diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index 46b4af9e7..df5d27e28 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -2,10 +2,10 @@ From 2c2e738222056c3ee7537242e41c6437d8928884 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 29 Dec 2019 18:38:55 +0100 Subject: [PATCH 03/27] Remove no-speech search xml files --- .../res/layout/activity_search_no_speech.xml | 38 ------------------- .../main/res/layout/fragment_empty_frame.xml | 9 ----- 2 files changed, 47 deletions(-) delete mode 100644 app/src/main/res/layout/activity_search_no_speech.xml delete mode 100644 app/src/main/res/layout/fragment_empty_frame.xml diff --git a/app/src/main/res/layout/activity_search_no_speech.xml b/app/src/main/res/layout/activity_search_no_speech.xml deleted file mode 100644 index 42ea1fd4d..000000000 --- a/app/src/main/res/layout/activity_search_no_speech.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_empty_frame.xml b/app/src/main/res/layout/fragment_empty_frame.xml deleted file mode 100644 index 2b9f7a2e6..000000000 --- a/app/src/main/res/layout/fragment_empty_frame.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file From 71e434517b420cd4440823c5eefe773eed293a67 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 31 Dec 2019 11:24:17 +0100 Subject: [PATCH 04/27] Disable speech when not supported --- .../androidtv/search/SearchFragment.java | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java index eb55b18fd..4ad0c7cba 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java @@ -2,7 +2,12 @@ package org.jellyfin.androidtv.search; import android.os.Bundle; import android.os.Handler; +import android.speech.SpeechRecognizer; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import org.jellyfin.androidtv.R; import org.jellyfin.androidtv.itemhandling.BaseRowItem; import org.jellyfin.androidtv.itemhandling.ItemLauncher; import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; @@ -13,10 +18,7 @@ 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 androidx.leanback.widget.SpeechOrbView; /** * Created by Eric on 1/26/2015. @@ -28,21 +30,44 @@ public class SearchFragment extends SearchSupportFragment private final Handler mHandler = new Handler(); private ArrayObjectAdapter mRowsAdapter; private SearchRunnable mDelayedLoad; + private boolean isSpeechEnabled = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(getContext()); 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()); - } + setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, 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)); + + // Disable speech functionality + if (!isSpeechEnabled) { + // This function is deprecated but it still disabled the automatic speech functionality + // when a callback is set. We don't actually implement it. + setSpeechRecognitionCallback(() -> { + }); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view == null) return null; + + // Hide speech orb when speech is disabled + if (!isSpeechEnabled) { + // Set to invisible instead of gone to keep the alignment of the input + SpeechOrbView speechOrbView = view.findViewById(R.id.lb_search_bar_speech_orb); + speechOrbView.setVisibility(View.INVISIBLE); + } + + return view; } @Override @@ -67,7 +92,7 @@ public class SearchFragment extends SearchSupportFragment /** * Update search results * - * @param query String to search for + * @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) { From 900032efa65b15c4051a347687e3ff2385f75e19 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 31 Dec 2019 11:30:59 +0100 Subject: [PATCH 05/27] Separate SearchFragment and SearchProvider --- .../androidtv/search/SearchFragment.java | 64 ++---------------- .../androidtv/search/SearchProvider.java | 67 +++++++++++++++++++ .../androidtv/search/SearchRunnable.java | 34 +++++----- 3 files changed, 88 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/org/jellyfin/androidtv/search/SearchProvider.java diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java index 4ad0c7cba..529b6fb4f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java @@ -1,7 +1,6 @@ package org.jellyfin.androidtv.search; import android.os.Bundle; -import android.os.Handler; import android.speech.SpeechRecognizer; import android.view.LayoutInflater; import android.view.View; @@ -11,25 +10,13 @@ import org.jellyfin.androidtv.R; 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 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.SpeechOrbView; -/** - * Created by Eric on 1/26/2015. - */ -public class SearchFragment extends SearchSupportFragment - implements SearchSupportFragment.SearchResultProvider { +public class SearchFragment extends SearchSupportFragment { - private static final int SEARCH_DELAY_MS = 1500; - private final Handler mHandler = new Handler(); - private ArrayObjectAdapter mRowsAdapter; - private SearchRunnable mDelayedLoad; private boolean isSpeechEnabled = false; @Override @@ -37,14 +24,14 @@ public class SearchFragment extends SearchSupportFragment super.onCreate(savedInstanceState); isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(getContext()); - mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - setSearchResultProvider(this); + + SearchProvider searchProvider = new SearchProvider(getContext(), getActivity().getIntent().getBooleanExtra("MusicOnly", false)); + setSearchResultProvider(searchProvider); setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, 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)); // Disable speech functionality if (!isSpeechEnabled) { @@ -69,48 +56,5 @@ public class SearchFragment extends SearchSupportFragment return view; } - - @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); - } } diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchProvider.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchProvider.java new file mode 100644 index 000000000..a6e63d847 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchProvider.java @@ -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); + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchRunnable.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchRunnable.java index 18f099006..88b599362 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchRunnable.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchRunnable.java @@ -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(); From de2081bec2e3634b4fbab69064578ca8d9089314 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 31 Dec 2019 12:04:31 +0100 Subject: [PATCH 06/27] Use more simple approach for disabling speech --- .../androidtv/search/SearchFragment.java | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java index 529b6fb4f..474d8ece4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java @@ -2,31 +2,25 @@ package org.jellyfin.androidtv.search; import android.os.Bundle; import android.speech.SpeechRecognizer; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import org.jellyfin.androidtv.R; +import org.jellyfin.androidtv.TvApp; 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; -import androidx.leanback.widget.SpeechOrbView; public class SearchFragment extends SearchSupportFragment { - - private boolean isSpeechEnabled = false; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(getContext()); - + // 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; @@ -34,27 +28,14 @@ public class SearchFragment extends SearchSupportFragment { }); // Disable speech functionality + boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(getContext()); if (!isSpeechEnabled) { // This function is deprecated but it still disabled the automatic speech functionality // when a callback is set. We don't actually implement it. setSpeechRecognitionCallback(() -> { + TvApp.getApplication().getLogger().Debug("setSpeechRecognitionCallback call ignored"); }); } } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - if (view == null) return null; - - // Hide speech orb when speech is disabled - if (!isSpeechEnabled) { - // Set to invisible instead of gone to keep the alignment of the input - SpeechOrbView speechOrbView = view.findViewById(R.id.lb_search_bar_speech_orb); - speechOrbView.setVisibility(View.INVISIBLE); - } - - return view; - } } From 229990b50bc63839b17667a3409c4c1930e7a945 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 1 Jan 2020 10:53:48 +0100 Subject: [PATCH 07/27] Add separate search fragment for speech incapable devices (like Fire TV) --- app/build.gradle | 1 + ...gment.java => LeanbackSearchFragment.java} | 14 +--- .../androidtv/search/SearchActivity.java | 17 ++++- .../androidtv/search/TextSearchFragment.java | 73 +++++++++++++++++++ app/src/main/res/layout/activity_search.xml | 11 --- .../main/res/layout/fragment_search_text.xml | 44 +++++++++++ 6 files changed, 134 insertions(+), 26 deletions(-) rename app/src/main/java/org/jellyfin/androidtv/search/{SearchFragment.java => LeanbackSearchFragment.java} (60%) create mode 100644 app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java delete mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/fragment_search_text.xml diff --git a/app/build.gradle b/app/build.gradle index 161dfd4c0..19f8e04da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,4 +72,5 @@ dependencies { implementation "ch.acra:acra-dialog:$acraVersion" implementation "ch.acra:acra-limiter:$acraVersion" testImplementation 'junit:junit:4.12' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' } diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/LeanbackSearchFragment.java similarity index 60% rename from app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java rename to app/src/main/java/org/jellyfin/androidtv/search/LeanbackSearchFragment.java index 474d8ece4..ad19ec7f0 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/LeanbackSearchFragment.java @@ -1,9 +1,7 @@ package org.jellyfin.androidtv.search; import android.os.Bundle; -import android.speech.SpeechRecognizer; -import org.jellyfin.androidtv.TvApp; import org.jellyfin.androidtv.itemhandling.BaseRowItem; import org.jellyfin.androidtv.itemhandling.ItemLauncher; import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; @@ -11,7 +9,7 @@ import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; import androidx.leanback.app.SearchSupportFragment; import androidx.leanback.widget.ListRow; -public class SearchFragment extends SearchSupportFragment { +public class LeanbackSearchFragment extends SearchSupportFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -26,16 +24,6 @@ public class SearchFragment extends SearchSupportFragment { ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity()); }); - - // Disable speech functionality - boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(getContext()); - if (!isSpeechEnabled) { - // This function is deprecated but it still disabled the automatic speech functionality - // when a callback is set. We don't actually implement it. - setSpeechRecognitionCallback(() -> { - TvApp.getApplication().getLogger().Debug("setSpeechRecognitionCallback call ignored"); - }); - } } } diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java index 5bdac16b7..41c91ffea 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java @@ -1,16 +1,29 @@ package org.jellyfin.androidtv.search; import android.os.Bundle; +import android.speech.SpeechRecognizer; -import org.jellyfin.androidtv.R; 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); - setContentView(R.layout.activity_search); + boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(this); + + // Determine fragment to use + Fragment searchFragment = isSpeechEnabled && false + ? new LeanbackSearchFragment() + : new TextSearchFragment(); + + // Add fragment + getSupportFragmentManager() + .beginTransaction() + .replace(android.R.id.content, searchFragment) + .commit(); } @Override diff --git a/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java new file mode 100644 index 000000000..4ffb9c29c --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java @@ -0,0 +1,73 @@ +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 androidx.fragment.app.Fragment; +import androidx.leanback.app.RowsSupportFragment; + +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()); + } + + @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; + } +} + diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml deleted file mode 100644 index df5d27e28..000000000 --- a/app/src/main/res/layout/activity_search.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/app/src/main/res/layout/fragment_search_text.xml b/app/src/main/res/layout/fragment_search_text.xml new file mode 100644 index 000000000..2a7efb93c --- /dev/null +++ b/app/src/main/res/layout/fragment_search_text.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file From 6771bd2d14b3eb7bce029c81cb5f133101bac2fd Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 1 Jan 2020 14:45:34 +0100 Subject: [PATCH 08/27] Remove exomedia and upgrade exoplayer to the latest amazon revision --- app/build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 161dfd4c0..9b9c50447 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,10 +45,8 @@ 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' + amazonImplementation 'com.amazon.android:exoplayer:2.10.6' + googleImplementation 'com.google.android.exoplayer:exoplayer:2.10.6' // This is split like this because this is a modular gradle project on jitpack // see https://github.com/jitpack/gradle-modular From 1b30d78b1a548c0059ddeb0b26ddaa7e103eda33 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 1 Jan 2020 15:20:37 +0100 Subject: [PATCH 09/27] Use ExoPlayer 2 in MediaManager (audio player) --- .../androidtv/playback/MediaManager.java | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java b/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java index eb965f125..e33af52cf 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java @@ -7,14 +7,17 @@ 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.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.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +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 +35,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,12 +50,7 @@ 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; +import androidx.annotation.Nullable; public class MediaManager { private static ItemRowAdapter mCurrentMediaAdapter; @@ -63,7 +67,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; @@ -199,19 +203,19 @@ 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) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + //todo doesnt work, need to write own timer to do progress reports reportProgress(); - return false; } - }); - mExoplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override - public void onCompletion(MediaPlayer mp) { - onComplete(); + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_ENDED) { + onComplete(); + } } }); } else { @@ -564,8 +568,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(), apiClient.getCurrentUserId()); //todo user agent + + 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())); @@ -689,7 +695,7 @@ public class MediaManager { } private static void stop() { - if (nativeMode) mExoplayer.stopPlayback(); + if (nativeMode) mExoplayer.stop(true); else mVlcPlayer.stop(); } @@ -708,7 +714,7 @@ public class MediaManager { } private static void pause() { - if (nativeMode) mExoplayer.pause(); + if (nativeMode) mExoplayer.setPlayWhenReady(false); else mVlcPlayer.pause(); } @@ -730,7 +736,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); From b9cec5294eb2c63dd17b1e65635b4946924ec51e Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 1 Jan 2020 15:34:26 +0100 Subject: [PATCH 10/27] Update VideoManager to exoplayer 2 --- .../androidtv/playback/VideoManager.java | 144 ++++++++++-------- .../main/res/layout/vlc_player_interface.xml | 8 +- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java index 1d8712db2..9e3bdb696 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java @@ -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; @@ -12,12 +11,22 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -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; @@ -25,10 +34,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; @@ -43,7 +48,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; @@ -83,7 +89,10 @@ 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.setUseController(false); + mExoPlayerView.setPlayer(mExoPlayer); } public void init(int buffer, boolean isInterlaced) { @@ -93,9 +102,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); } } @@ -103,26 +112,27 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { public int getZoomMode() { return mZoomMode; } public void setZoom(int mode) { + //todo mZoomMode = mode; - switch (mode) { - case ZOOM_NORMAL: - mVideoView.setScaleY(1); - mVideoView.setScaleX(1); - break; - case ZOOM_VERTICAL: - mVideoView.setScaleX(1); - mVideoView.setScaleY(1.33f); - break; - case ZOOM_HORIZONTAL: - mVideoView.setScaleY(1); - mVideoView.setScaleX(1.33f); - break; - case ZOOM_FULL: - mVideoView.setScaleX(1.33f); - mVideoView.setScaleY(1.33f); - break; - - } +// switch (mode) { +// case ZOOM_NORMAL: +// mVideoView.setScaleY(1); +// mVideoView.setScaleX(1); +// break; +// case ZOOM_VERTICAL: +// mVideoView.setScaleX(1); +// mVideoView.setScaleY(1.33f); +// break; +// case ZOOM_HORIZONTAL: +// mVideoView.setScaleY(1); +// mVideoView.setScaleX(1.33f); +// break; +// case ZOOM_FULL: +// mVideoView.setScaleX(1.33f); +// mVideoView.setScaleY(1.33f); +// break; +// +// } } public void setMetaDuration(long duration) { @@ -131,14 +141,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; @@ -162,17 +172,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"); @@ -188,8 +198,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); @@ -198,8 +208,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); @@ -213,7 +223,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { public void stopPlayback() { if (nativeMode) { - mVideoView.stopPlayback(); + mExoPlayer.stop(); } else { mVlcPlayer.stop(); } @@ -223,8 +233,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; @@ -255,7 +265,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { if (nativeMode) { try { - mVideoView.setVideoPath(path); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "user agent"); //todo user agent + + 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); } @@ -273,7 +285,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); } @@ -281,7 +293,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); } @@ -486,7 +498,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(); @@ -499,8 +511,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; @@ -509,14 +521,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; @@ -595,13 +607,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { PlaybackListener errorListener; public void setOnErrorListener(final PlaybackListener listener) { - mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { + mExoPlayer.addListener(new Player.EventListener() { @Override - public boolean onError(MediaPlayer mp, int what, int extra) { + public void onPlayerError(ExoPlaybackException error) { TvApp.getApplication().getLogger().Error("***** Got error from player"); listener.onEvent(); stopProgressLoop(); - return true; } }); @@ -614,26 +625,27 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { } public void setOnCompletionListener(final PlaybackListener listener) { - mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + mExoPlayer.addListener(new Player.EventListener() { @Override - public void onCompletion(MediaPlayer mp) { - listener.onEvent(); - stopProgressLoop(); + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_ENDED) { + listener.onEvent(); + stopProgressLoop(); + } } }); mVlcHandler.setOnCompletionListener(listener); } - private MediaPlayer mNativeMediaPlayer; - public void setOnPreparedListener(final PlaybackListener listener) { - mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + mExoPlayer.addListener(new Player.EventListener() { @Override - public void onPrepared(MediaPlayer mp) { - mNativeMediaPlayer = mp; - listener.onEvent(); - startProgressLoop(); + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_READY) { + listener.onEvent(); + startProgressLoop(); + } } }); diff --git a/app/src/main/res/layout/vlc_player_interface.xml b/app/src/main/res/layout/vlc_player_interface.xml index 63ef7a2a6..c836636bf 100644 --- a/app/src/main/res/layout/vlc_player_interface.xml +++ b/app/src/main/res/layout/vlc_player_interface.xml @@ -29,9 +29,11 @@ - + Date: Wed, 1 Jan 2020 15:56:53 +0100 Subject: [PATCH 11/27] Fix audio progress not working --- .../androidtv/playback/MediaManager.java | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java b/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java index e33af52cf..32261cd44 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/MediaManager.java @@ -8,13 +8,14 @@ import android.content.DialogInterface; import android.content.Intent; import android.media.AudioManager; import android.net.Uri; +import android.os.Handler; import android.text.InputType; import android.widget.EditText; +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.Timeline; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -50,8 +51,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; -import androidx.annotation.Nullable; - public class MediaManager { private static ItemRowAdapter mCurrentMediaAdapter; private static int mCurrentMediaPosition = -1; @@ -67,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 SimpleExoPlayer mExoplayer; + private static SimpleExoPlayer mExoPlayer; private static AudioManager mAudioManager; private static boolean audioInitialized; private static boolean nativeMode = false; @@ -161,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() { @@ -169,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) { @@ -203,19 +202,21 @@ public class MediaManager { // Create a new media player based on platform if (DeviceUtils.is60()) { nativeMode = true; - mExoplayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication()); - mExoplayer.addListener(new Player.EventListener() { + mExoPlayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication()); + mExoPlayer.addListener(new Player.EventListener() { @Override - public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { - //todo doesnt work, need to write own timer to do progress reports - reportProgress(); + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_READY) { + startProgressLoop(); + } else if (playbackState == Player.STATE_ENDED) { + onComplete(); + stopProgressLoop(); + } } @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - onComplete(); - } + public void onPlayerError(ExoPlaybackException error) { + stopProgressLoop(); } }); } else { @@ -257,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) { @@ -458,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) { @@ -568,10 +588,10 @@ public class MediaManager { mCurrentAudioQueuePosition = pos; mCurrentAudioPosition = 0; if (nativeMode) { - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), apiClient.getCurrentUserId()); //todo user agent + 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())))); + 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())); @@ -673,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; } @@ -695,7 +715,7 @@ public class MediaManager { } private static void stop() { - if (nativeMode) mExoplayer.stop(true); + if (nativeMode) mExoPlayer.stop(true); else mVlcPlayer.stop(); } @@ -714,7 +734,7 @@ public class MediaManager { } private static void pause() { - if (nativeMode) mExoplayer.setPlayWhenReady(false); + if (nativeMode) mExoPlayer.setPlayWhenReady(false); else mVlcPlayer.pause(); } @@ -736,7 +756,7 @@ public class MediaManager { public static void resumeAudio() { if (mCurrentAudioItem != null) { ensureAudioFocus(); - if (nativeMode) mExoplayer.setPlayWhenReady(true); + if (nativeMode) mExoPlayer.setPlayWhenReady(true); else mVlcPlayer.play(); updateCurrentAudioItemPlaying(true); ReportingHelper.reportStart(mCurrentAudioItem, mCurrentAudioPosition * 10000); From 3c7d9c86a9667395f61875d0620886a9c95b8ce4 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 1 Jan 2020 16:20:50 +0100 Subject: [PATCH 12/27] Single event listener, fix setZoom, disable controls in XML, fix play after pause --- .../androidtv/playback/VideoManager.java | 97 +++++++++---------- .../main/res/layout/vlc_player_interface.xml | 4 +- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java index 9e3bdb696..d89710f47 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java @@ -74,6 +74,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; @@ -91,8 +94,27 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { } mExoPlayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication()); mExoPlayerView = view.findViewById(R.id.exoPlayerView); - mExoPlayerView.setUseController(false); 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) { @@ -112,27 +134,26 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { public int getZoomMode() { return mZoomMode; } public void setZoom(int mode) { - //todo mZoomMode = mode; -// switch (mode) { -// case ZOOM_NORMAL: -// mVideoView.setScaleY(1); -// mVideoView.setScaleX(1); -// break; -// case ZOOM_VERTICAL: -// mVideoView.setScaleX(1); -// mVideoView.setScaleY(1.33f); -// break; -// case ZOOM_HORIZONTAL: -// mVideoView.setScaleY(1); -// mVideoView.setScaleX(1.33f); -// break; -// case ZOOM_FULL: -// mVideoView.setScaleX(1.33f); -// mVideoView.setScaleY(1.33f); -// break; -// -// } + switch (mode) { + case ZOOM_NORMAL: + mExoPlayerView.setScaleY(1); + mExoPlayerView.setScaleX(1); + break; + case ZOOM_VERTICAL: + mExoPlayerView.setScaleX(1); + mExoPlayerView.setScaleY(1.33f); + break; + case ZOOM_HORIZONTAL: + mExoPlayerView.setScaleY(1); + mExoPlayerView.setScaleX(1.33f); + break; + case ZOOM_FULL: + mExoPlayerView.setScaleX(1.33f); + mExoPlayerView.setScaleY(1.33f); + break; + + } } public void setMetaDuration(long duration) { @@ -265,7 +286,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { if (nativeMode) { try { - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "user agent"); //todo user agent + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "ATV/ExoPlayer"); mExoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(path))); } catch (IllegalStateException e) { @@ -604,18 +625,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { if (hasSubtitlesSurface) mSubtitlesSurface.invalidate(); } - PlaybackListener errorListener; - public void setOnErrorListener(final PlaybackListener listener) { - mExoPlayer.addListener(new Player.EventListener() { - @Override - public void onPlayerError(ExoPlaybackException error) { - TvApp.getApplication().getLogger().Error("***** Got error from player"); - listener.onEvent(); - stopProgressLoop(); - } - }); - mVlcHandler.setOnErrorListener(listener); errorListener = listener; } @@ -625,29 +635,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener { } public void setOnCompletionListener(final PlaybackListener listener) { - mExoPlayer.addListener(new Player.EventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - listener.onEvent(); - stopProgressLoop(); - } - } - }); - + completionListener = listener; mVlcHandler.setOnCompletionListener(listener); } public void setOnPreparedListener(final PlaybackListener listener) { - mExoPlayer.addListener(new Player.EventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_READY) { - listener.onEvent(); - startProgressLoop(); - } - } - }); + preparedListener = listener; mVlcHandler.setOnPreparedListener(listener); } diff --git a/app/src/main/res/layout/vlc_player_interface.xml b/app/src/main/res/layout/vlc_player_interface.xml index c836636bf..2a999bff0 100644 --- a/app/src/main/res/layout/vlc_player_interface.xml +++ b/app/src/main/res/layout/vlc_player_interface.xml @@ -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"> + android:visibility="gone" + app:use_controller="false" /> Date: Sun, 15 Dec 2019 17:25:49 +0100 Subject: [PATCH 13/27] Add support for Android Leanback "Watch Next" feature --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 3 + .../androidtv/browsing/HomeFragment.java | 10 ++ .../androidtv/channels/ChannelManager.kt | 124 ++++++++++++++++++ .../util/apiclient/ApiClientExtensions.kt | 18 +++ 5 files changed, 158 insertions(+) create mode 100644 app/src/main/java/org/jellyfin/androidtv/channels/ChannelManager.kt create mode 100644 app/src/main/java/org/jellyfin/androidtv/util/apiclient/ApiClientExtensions.kt diff --git a/app/build.gradle b/app/build.gradle index 411458f4e..7b5abdafd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,9 @@ 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.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() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33f9fceaa..d9009b9e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ + + + diff --git a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java index d4c00edf5..26efeace0 100644 --- a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java @@ -18,6 +18,7 @@ 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.integration.RecommendationManager; import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; import org.jellyfin.androidtv.livetv.LiveTvGuideActivity; @@ -55,6 +56,8 @@ public class HomeFragment extends StdBrowseFragment { private static final int LOGOUT = 0; private static final int SETTINGS = 1; + private ChannelManager channelManager; + @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -72,6 +75,10 @@ public class HomeFragment extends StdBrowseFragment { //Init recommendations RecommendationManager.init(); + // Init leanback home channels; + channelManager = new ChannelManager(); + channelManager.update(); + //Get auto bitrate TvApp.getApplication().determineAutoBitrate(); @@ -128,6 +135,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 diff --git a/app/src/main/java/org/jellyfin/androidtv/channels/ChannelManager.kt b/app/src/main/java/org/jellyfin/androidtv/channels/ChannelManager.kt new file mode 100644 index 000000000..04732c7b1 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/channels/ChannelManager.kt @@ -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() +} diff --git a/app/src/main/java/org/jellyfin/androidtv/util/apiclient/ApiClientExtensions.kt b/app/src/main/java/org/jellyfin/androidtv/util/apiclient/ApiClientExtensions.kt new file mode 100644 index 000000000..e7362efbf --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/util/apiclient/ApiClientExtensions.kt @@ -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() { + override fun onResponse(response: ItemsResult?) = continuation.resume(response!!) + override fun onError(exception: Exception?) = continuation.resume(null) + }) +} From 1704e3df3d7e452533c9c3129f3a5d6083a7233e Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 18 Dec 2019 21:50:22 +0100 Subject: [PATCH 14/27] Fix channels updating twice on app start --- .../main/java/org/jellyfin/androidtv/browsing/HomeFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java index 26efeace0..fe4038767 100644 --- a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java @@ -77,7 +77,6 @@ public class HomeFragment extends StdBrowseFragment { // Init leanback home channels; channelManager = new ChannelManager(); - channelManager.update(); //Get auto bitrate TvApp.getApplication().determineAutoBitrate(); From 038caab6807187e5d4e5b5ff47831db1218e2e2e Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Thu, 2 Jan 2020 17:50:28 +0100 Subject: [PATCH 15/27] Disable forced speech disabled activity --- .../main/java/org/jellyfin/androidtv/search/SearchActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java index 41c91ffea..77820b61a 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/SearchActivity.java @@ -15,7 +15,7 @@ public class SearchActivity extends BaseActivity { boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(this); // Determine fragment to use - Fragment searchFragment = isSpeechEnabled && false + Fragment searchFragment = isSpeechEnabled ? new LeanbackSearchFragment() : new TextSearchFragment(); From bf33dc60bd1be5bed738986a4a3f989a328054ad Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Thu, 2 Jan 2020 17:50:52 +0100 Subject: [PATCH 16/27] Remove showSearchWithPermission method --- app/src/main/java/org/jellyfin/androidtv/TvApp.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/TvApp.java b/app/src/main/java/org/jellyfin/androidtv/TvApp.java index ff0b7df0d..cfaa563db 100644 --- a/app/src/main/java/org/jellyfin/androidtv/TvApp.java +++ b/app/src/main/java/org/jellyfin/androidtv/TvApp.java @@ -227,12 +227,9 @@ public class TvApp extends Application { } public void showSearch(final Activity activity, boolean musicOnly) { - 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); } From 5c4f213383e33a7c2a00f2185a922f0e273ebfb2 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 2 Jan 2020 12:25:59 -0500 Subject: [PATCH 17/27] Increase max jvm heap size --- app/build.gradle | 2 +- gradle.properties | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7b5abdafd..81074000a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/gradle.properties b/gradle.properties index 5465fec0e..239fe4f17 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ +org.gradle.jvmargs=-Xmx768m + android.enableJetifier=true android.useAndroidX=true \ No newline at end of file From e36dd6c968539d85c8609e12f6a2e747581865a3 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Thu, 2 Jan 2020 19:13:00 +0100 Subject: [PATCH 18/27] Add option to choose the preferred media player in settings --- .../java/org/jellyfin/androidtv/TvApp.java | 19 +++---- .../playback/ExternalPlayerActivity.java | 2 +- .../playback/PlaybackController.java | 57 +++++++++++-------- .../androidtv/settings/SettingsFragment.java | 22 ++++--- .../androidtv/startup/StartupActivity.java | 6 ++ app/src/main/res/values/array.xml | 12 ++++ app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/preferences.xml | 10 ++-- 8 files changed, 85 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/TvApp.java b/app/src/main/java/org/jellyfin/androidtv/TvApp.java index ff3a4bb7f..bbdc671a0 100644 --- a/app/src/main/java/org/jellyfin/androidtv/TvApp.java +++ b/app/src/main/java/org/jellyfin/androidtv/TvApp.java @@ -17,10 +17,6 @@ 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 +26,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 +49,13 @@ 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.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.palette.graphics.Palette; + @AcraCore(buildConfigClass = BuildConfig.class) @AcraHttpSender( uri = "https://collector.tracepot.com/a2eda9d9", @@ -309,7 +308,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); diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/ExternalPlayerActivity.java b/app/src/main/java/org/jellyfin/androidtv/playback/ExternalPlayerActivity.java index c155db01c..feee6d721 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/ExternalPlayerActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/ExternalPlayerActivity.java @@ -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(); } }) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java index 9c7e7e348..a92328a60 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java @@ -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); @@ -474,26 +473,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); diff --git a/app/src/main/java/org/jellyfin/androidtv/settings/SettingsFragment.java b/app/src/main/java/org/jellyfin/androidtv/settings/SettingsFragment.java index 763e278f2..aa2f4dbb5 100644 --- a/app/src/main/java/org/jellyfin/androidtv/settings/SettingsFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/settings/SettingsFragment.java @@ -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) { diff --git a/app/src/main/java/org/jellyfin/androidtv/startup/StartupActivity.java b/app/src/main/java/org/jellyfin/androidtv/startup/StartupActivity.java index f38dc1f6f..cbe5553cb 100644 --- a/app/src/main/java/org/jellyfin/androidtv/startup/StartupActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/startup/StartupActivity.java @@ -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); diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index ab8141c0f..a5897a09d 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -101,4 +101,16 @@ 0 1 + + @string/pref_video_player_auto + @string/pref_video_player_exoplayer + @string/pref_video_player_vlc + @string/pref_video_player_external + + + auto + exoplayer + vlc + external + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca48ab8bf..accfb1b98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -309,7 +309,7 @@ Requires a capable app to play video types Use External Player for Live TV Requires a capable app to play live streams - Pass Direct Path + Pass Direct Path for external player Pass the file system path instead of streaming URL Crash Reporting Send crash reports to developers @@ -344,4 +344,8 @@ Default View Image Type Image Size + Automatically choose + ExoPlayer + libVLC + External app \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d769d5732..c7b386245 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -75,6 +75,11 @@ android:entries="@array/pref_preroll_options" android:entryValues="@array/pref_preroll_values" /> + - Date: Thu, 2 Jan 2020 19:14:08 +0100 Subject: [PATCH 19/27] Remove strings for external player option --- app/src/main/res/values/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index accfb1b98..22a1e0461 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -305,8 +305,6 @@ Next Episode Album name Song Title - Use External Player for Videos - Requires a capable app to play video types Use External Player for Live TV Requires a capable app to play live streams Pass Direct Path for external player From 5049e7d09fdb6d6eed10ef372c19bf679ac1d6be Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 3 Jan 2020 19:27:14 +0100 Subject: [PATCH 20/27] Add missing click listener to text search fragment --- .../jellyfin/androidtv/search/TextSearchFragment.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java b/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java index 4ffb9c29c..4fc5e7da4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/search/TextSearchFragment.java @@ -12,9 +12,13 @@ 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; @@ -43,6 +47,13 @@ public class TextSearchFragment extends Fragment implements TextWatcher, TextVie // 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 From 1ca6abdd1388489d920243d0ea2e9732d8343c23 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 3 Jan 2020 19:36:28 +0100 Subject: [PATCH 21/27] Remove build flavors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the amazon fork for exoplayer for all builds. This removes the need for a separate flavor for google/amazon devices 🎉 --- app/build.gradle | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9b9c50447..c6625c3cc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,26 +27,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.amazon.android:exoplayer:2.10.6' - googleImplementation 'com.google.android.exoplayer:exoplayer:2.10.6' // This is split like this because this is a modular gradle project on jitpack // see https://github.com/jitpack/gradle-modular @@ -55,6 +41,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' From 96c0dc0f87389fb117908354e8b4c942145fc8a1 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 3 Jan 2020 19:52:49 +0100 Subject: [PATCH 22/27] Remove recommendation code (was unused and not working) --- app/src/main/AndroidManifest.xml | 7 - .../androidtv/browsing/HomeFragment.java | 26 +- .../androidtv/integration/Recommendation.java | 51 --- .../integration/RecommendationBuilder.java | 127 ------- .../RecommendationContentProvider.java | 157 --------- .../integration/RecommendationManager.java | 332 ------------------ .../integration/RecommendationType.java | 6 - .../integration/Recommendations.java | 129 ------- .../CustomPlaybackOverlayFragment.java | 38 +- 9 files changed, 29 insertions(+), 844 deletions(-) delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/Recommendation.java delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/RecommendationBuilder.java delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/RecommendationContentProvider.java delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/RecommendationManager.java delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/RecommendationType.java delete mode 100644 app/src/main/java/org/jellyfin/androidtv/integration/Recommendations.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d9009b9e5..04670a60f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,13 +84,6 @@ - - diff --git a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java index fe4038767..e094a17fb 100644 --- a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java @@ -5,13 +5,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; -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; import android.widget.Toast; import org.jellyfin.androidtv.R; @@ -19,11 +12,11 @@ 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.integration.RecommendationManager; import org.jellyfin.androidtv.itemhandling.ItemRowAdapter; import org.jellyfin.androidtv.livetv.LiveTvGuideActivity; import org.jellyfin.androidtv.model.ChangeTriggerType; import org.jellyfin.androidtv.model.DisplayPriorityType; +import org.jellyfin.androidtv.model.LogonCredentials; import org.jellyfin.androidtv.playback.AudioEventListener; import org.jellyfin.androidtv.playback.MediaManager; import org.jellyfin.androidtv.presentation.GridButtonPresenter; @@ -32,13 +25,9 @@ import org.jellyfin.androidtv.querying.QueryType; import org.jellyfin.androidtv.querying.StdItemQuery; import org.jellyfin.androidtv.querying.ViewQuery; import org.jellyfin.androidtv.settings.SettingsActivity; -import org.jellyfin.androidtv.model.LogonCredentials; import org.jellyfin.androidtv.startup.SelectUserActivity; import org.jellyfin.androidtv.ui.GridButton; import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper; - -import java.io.IOException; - import org.jellyfin.apiclient.interaction.EmptyResponse; import org.jellyfin.apiclient.interaction.Response; import org.jellyfin.apiclient.model.entities.LocationType; @@ -52,6 +41,16 @@ import org.jellyfin.apiclient.model.querying.ItemsResult; import org.jellyfin.apiclient.model.querying.LatestItemsQuery; import org.jellyfin.apiclient.model.querying.NextUpQuery; +import java.io.IOException; + +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 HomeFragment extends StdBrowseFragment { private static final int LOGOUT = 0; private static final int SETTINGS = 1; @@ -72,9 +71,6 @@ public class HomeFragment extends StdBrowseFragment { TvApp.getApplication().getLogger().ErrorException("Unable to save login creds", e); } - //Init recommendations - RecommendationManager.init(); - // Init leanback home channels; channelManager = new ChannelManager(); diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/Recommendation.java b/app/src/main/java/org/jellyfin/androidtv/integration/Recommendation.java deleted file mode 100644 index 8d6cfa3db..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/Recommendation.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationBuilder.java b/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationBuilder.java deleted file mode 100644 index 61c4ebcb5..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationBuilder.java +++ /dev/null @@ -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 + - '}'; - }} diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationContentProvider.java b/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationContentProvider.java deleted file mode 100644 index 54b27cca7..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationContentProvider.java +++ /dev/null @@ -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); - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationManager.java b/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationManager.java deleted file mode 100644 index d3c6d0e2c..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationManager.java +++ /dev/null @@ -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() { - @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() { - @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() { - @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() { - @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() { - @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() { - @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 { - @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); - } - -} - diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationType.java b/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationType.java deleted file mode 100644 index ae0e0c9ab..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/RecommendationType.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.jellyfin.androidtv.integration; - -public enum RecommendationType { - Movie, - Tv -} diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/Recommendations.java b/app/src/main/java/org/jellyfin/androidtv/integration/Recommendations.java deleted file mode 100644 index d9dbadf59..000000000 --- a/app/src/main/java/org/jellyfin/androidtv/integration/Recommendations.java +++ /dev/null @@ -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 tvRecommendations = new ArrayList<>(); - private List movieRecommendations = new ArrayList<>(); - - public Recommendations(String serverId, String userId) { - this.userId = userId; - this.serverId = serverId; - } - - public List getTvRecommendations() { - return tvRecommendations; - } - - public void setTvRecommendations(List tvRecommendations) { - this.tvRecommendations = tvRecommendations; - } - - public List getMovieRecommendations() { - return movieRecommendations; - } - - public void setMovieRecommendations(List 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 list = type == RecommendationType.Movie ? movieRecommendations : tvRecommendations; - - Recommendation oldest = Collections.min(list, new Comparator() { - @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 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; - } -} - diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/CustomPlaybackOverlayFragment.java b/app/src/main/java/org/jellyfin/androidtv/playback/CustomPlaybackOverlayFragment.java index 6754c2bac..eea61483c 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/CustomPlaybackOverlayFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/CustomPlaybackOverlayFragment.java @@ -1,7 +1,6 @@ package org.jellyfin.androidtv.playback; import android.app.AlertDialog; -import androidx.fragment.app.Fragment; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -12,15 +11,6 @@ import android.media.AudioManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; -import androidx.annotation.Nullable; -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; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; @@ -53,7 +43,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; @@ -83,14 +72,6 @@ import org.jellyfin.androidtv.util.TimeUtils; import org.jellyfin.androidtv.util.Utils; import org.jellyfin.androidtv.util.apiclient.BaseItemUtils; import org.jellyfin.androidtv.util.apiclient.StreamHelper; - -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 org.jellyfin.apiclient.interaction.EmptyResponse; import org.jellyfin.apiclient.interaction.Response; import org.jellyfin.apiclient.model.dto.BaseItemDto; @@ -103,6 +84,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 mPoster; ImageView mStudioImage; @@ -754,7 +753,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(); } } From f5c87878f6285f29c6b6a8c01573a330d63ed6c1 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 3 Jan 2020 20:00:08 +0100 Subject: [PATCH 23/27] Fix bad import after merge --- .../main/java/org/jellyfin/androidtv/browsing/HomeFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java index c9bde9bbe..82b0bca45 100644 --- a/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java @@ -12,7 +12,6 @@ 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; From c411f58c3d21dc86c68a4d3856698d54058d04c9 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Fri, 3 Jan 2020 20:06:25 +0100 Subject: [PATCH 24/27] Remove old strings from other languages as well --- app/src/main/res/values-cs/strings.xml | 2 -- app/src/main/res/values-de/strings.xml | 2 -- app/src/main/res/values-en-rGB/strings.xml | 2 -- app/src/main/res/values-es-rMX/strings.xml | 2 -- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 2 -- app/src/main/res/values-kk/strings.xml | 2 -- app/src/main/res/values-ko/strings.xml | 2 -- app/src/main/res/values-nb/strings.xml | 2 -- app/src/main/res/values-nl/strings.xml | 2 -- app/src/main/res/values-pt-rBR/strings.xml | 2 -- app/src/main/res/values-ru/strings.xml | 2 -- app/src/main/res/values-zh-rCN/strings.xml | 2 -- 13 files changed, 25 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1466a7bdb..f4b136687 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -274,8 +274,6 @@ Přímý průchod Vyžaduje vhodnou aplikaci pro přehrávání live streamů Použít externí přehrávač pro Live TV - Vyžaduje vhodnou aplikaci pro přehrávání videa - Použít externí přehrávač pro videa Název skladby Název alba Další epizoda diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f04ed20d5..62eeda10b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -259,8 +259,6 @@ Direkten Pfad übergeben Benötigt eine Anwendung, welche Livestreams wiedergeben kann Externen Player für Live TV verwenden - Benötigt eine Anwendung, welche die Videodateien wiedergeben kann - Externen Player für Videos verwenden Songtitel Album Name Nächste Episode diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index f9095309f..369f0b62a 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -19,8 +19,6 @@ Pass Direct Path Requires a capable app to play live streams Use External Player for Live TV - Requires a capable app to play video types - Use External Player for Videos Song Title Album name Next Episode diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 131f0d0b2..e3faf592d 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -264,8 +264,6 @@ Pasar ruta directa Requiere una aplicación capaz para reproducir transmisiones en vivo Usar reproductor externo para TV en vivo - Requiere una aplicación capaz para reproducir tipos de vídeo - Usar un reproductor externo para vídeos Titulo de Canción Nombre de álbum Siguiente Episodio diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d3780bc11..d027eab32 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -189,7 +189,6 @@ Pasar ruta directa Requiere una aplicación capaz para reproducir transmisiones en vivo Usar reproductor externo para la televisión en vivo - Usar un reproductor externo para vídeos Título de la canción Nombre del álbum Próximo episodio diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d54b275aa..890259946 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -292,8 +292,6 @@ Épisode suivant Nom de l\'album titre de la chanson - Utiliser un lecteur externe pour les vidéos - Nécessite une application capable de lire les vidéos Utiliser le chemin directe Utiliser le chemin du fichier de l\'url de streaming de %s diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index ffb38d4e7..c8de08483 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -288,7 +288,6 @@ Eń keıingi qaralǵan efırlik arnaǵa dereý ótý Sýret ólshemi Tikeleı aǵymdardy oınatý úshin jaramdy qoldanbany qajet etedi - Beıne túrlerin oınatý úshin jaramdy qoldanbany qajet etedi Qoldaý kórsetilmeıtin server nusqasy. Server nusqasy %1$s nemese odan keıingi bolýy qajet. Sýret túri Ádepki kórinis @@ -312,7 +311,6 @@ Usynystar Tikeleı shyǵarý Efır úshin syrtqy oınatqyshty paıdalaný - Beıneler úshin syrtqy oınatqyshty paıdalaný Áýen ataýy Kelesi bólimge Aldyńǵy bólimge diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8079388d7..3507cec77 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -212,8 +212,6 @@ 다음 에피소드 앨범 이름 노래 제목 - 비디오에 외부 재생기 사용 - 비디오 타입을 재생하려면 호환되는 앱이 필요합니다 실시간 TV에 외부 재생기 사용 실시간 스트림을 재생하려면 호환되는 앱이 필요합니다 \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 6caac392d..596c2f354 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -228,8 +228,6 @@ Send filbanen direkte Krever en støttet app for å spille av direktestrømmer Bruk eksterm spiller for direkte-TV - Krever en støttet app for å spille av videotyper - Bruk ekstern spiller for videoer Låtnavn Albumnavn Neste episode diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1c527f34a..ecfb745ca 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -267,7 +267,6 @@ Fijne feestdagen! Suggesties Gebruik externe speler voor Live TV - Gebruik externe speler voor video\'s Album naam Volgende Aflevering Vorige Aflevering @@ -301,7 +300,6 @@ Fijne Halloween! Griezelige suggesties Heeft een bruikbare app nodig om live streams af te spelen - Heeft een bruikbare app nodig om video types af te spelen Titel van het nummer Meer als dit Hervat vanaf %s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 43a1c4718..c40850a7b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -250,7 +250,6 @@ Enviar o local do sistema de arquivos invés da URL de streaming Requer um app compatível de reproduzir streams ao vivo Usar Reprodutor Externo para TV ao Vivo - Requer um app compatível para reproduzir os tipos de vídeo Título da Música Nome do álbum Próximo Episódio @@ -311,7 +310,6 @@ Feliz Halloween! Sugestões Assustadoras Sugestões - Usar Player Externo para Vídeos Séries Gravadas Ocorreu um erro ao fazer login com suas credenciais salvas. Versão do servidor não suportada. Versão do servidor deve ser %1$s ou posterior. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b7f944bb9..ff060a7a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -251,7 +251,6 @@ Транслировать DTS аудио Избранное Использовать для эфира внешний проигрыватель - Использовать для видео внешний проигрыватель Название композиции Название альбома Следующий эпизод @@ -290,7 +289,6 @@ Выводить путь в файловой системе вместо URL потока Прямой вывод Требуется совместимое приложение для воспроизведения эфирных трансляций - Требуется совместимое приложение для воспроизведения видеотипов Ещё подобное этому Откат для возобновления Последнее воспроизведение diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 78196575e..35bd0826b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -151,8 +151,6 @@ 通过直接路径 需要一个有能力的应用程序播放直播流 使用外部播放器播放电视直播 - 需要一个有能力的应用程序来播放视频类型 - 使用外部播放器播放视频 歌曲标题 专辑名 下一个事件 From 859f8d107b9c04c27f0784e50380eb6a2452dac8 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 4 Jan 2020 15:06:13 +0100 Subject: [PATCH 25/27] Properly release exoplayer. fixes potential memory issue --- .../androidtv/playback/VideoManager.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java index d89710f47..ee5e2c30b 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/VideoManager.java @@ -503,15 +503,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); } From 91e7252025098816c53cae00afe198ec4098fe0d Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 7 Jan 2020 22:23:33 +0100 Subject: [PATCH 26/27] Make sure to use VLC when preferred, even when transcoding --- .../org/jellyfin/androidtv/playback/PlaybackController.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java index a92328a60..1bbc80a4f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java @@ -580,7 +580,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 { From 35c0801f0d4615aad4a31e6691764ccebd4e0607 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 7 Jan 2020 22:30:11 +0100 Subject: [PATCH 27/27] Set default value for useVlc to make sure it is always set according to preferences. Also removes the forced-enabling of VLC in the error handler for requesting stream info --- .../org/jellyfin/androidtv/playback/PlaybackController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java index 1bbc80a4f..81eb921c4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/playback/PlaybackController.java @@ -90,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; @@ -111,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) { @@ -536,7 +539,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); }