diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index df1c27ffa..28215e013 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,7 +11,10 @@
+
+
+
diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
index 78da9678b..09f9aea58 100644
--- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
+++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
@@ -1,10 +1,12 @@
package com.google.android.material.appbar;
import android.content.Context;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.OverScroller;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -12,11 +14,45 @@ import java.lang.reflect.Field;
// See https://stackoverflow.com/questions/56849221#57997489
public final class FlingBehavior extends AppBarLayout.Behavior {
+ private final Rect focusScrollRect = new Rect();
+
public FlingBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
+ public boolean onRequestChildRectangleOnScreen(
+ @NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
+ @NonNull final Rect rectangle, final boolean immediate) {
+
+ focusScrollRect.set(rectangle);
+
+ coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
+
+ int height = coordinatorLayout.getHeight();
+
+ if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
+ // the child is too big to fit inside ourselves completely, ignore request
+ return false;
+ }
+
+ int dy;
+
+ if (focusScrollRect.bottom > height) {
+ dy = focusScrollRect.top;
+ } else if (focusScrollRect.top < 0) {
+ // scrolling up
+ dy = -(height - focusScrollRect.bottom);
+ } else {
+ // nothing to do
+ return false;
+ }
+
+ int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
+
+ return consumed == dy;
+ }
+
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
final MotionEvent ev) {
switch (ev.getActionMasked()) {
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index e6269dd5f..164a2839a 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -65,6 +65,7 @@ import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants;
+import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
@@ -74,6 +75,7 @@ import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
@@ -142,6 +144,10 @@ public class MainActivity extends AppCompatActivity {
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}
+
+ if (AndroidTvUtils.isTv()) {
+ FocusOverlayView.setupFocusObserver(this);
+ }
}
private void setupDrawer() throws Exception {
@@ -526,6 +532,14 @@ public class MainActivity extends AppCompatActivity {
Log.d(TAG, "onBackPressed() called");
}
+ if (AndroidTvUtils.isTv()) {
+ View drawerPanel = findViewById(R.id.navigation);
+ if (drawer.isDrawerOpen(drawerPanel)) {
+ drawer.closeDrawers();
+ return;
+ }
+ }
+
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index bb24c9681..264644c74 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -45,10 +45,12 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
+import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.views.FocusOverlayView;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import java.io.Serializable;
@@ -341,6 +343,10 @@ public class RouterActivity extends AppCompatActivity {
selectedPreviously = selectedRadioPosition;
alertDialog.show();
+
+ if (AndroidTvUtils.isTv()) {
+ FocusOverlayView.setupFocusObserver(alertDialog);
+ }
}
private List getChoicesForService(final StreamingService service,
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
index 912d63e5a..c5a0c3a22 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java
@@ -13,7 +13,9 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.views.FocusOverlayView;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.MissionsFragment;
@@ -54,6 +56,10 @@ public class DownloadActivity extends AppCompatActivity {
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
+
+ if (AndroidTvUtils.isTv()) {
+ FocusOverlayView.setupFocusObserver(this);
+ }
}
private void updateFragments() {
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 0441e1ba1..eba77847b 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -11,7 +11,6 @@ import android.preference.PreferenceManager;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -73,6 +72,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
+import org.schabi.newpipe.util.AndroidTvUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
@@ -86,6 +86,7 @@ import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.views.AnimatedProgressBar;
+import org.schabi.newpipe.views.LargeTextMovementMethod;
import java.io.Serializable;
import java.util.Collection;
@@ -471,10 +472,13 @@ public class VideoDetailFragment extends BaseStateFragment
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
videoDescriptionRootLayout.setVisibility(View.GONE);
+ videoDescriptionView.setFocusable(false);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
} else {
videoTitleTextView.setMaxLines(10);
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
+ videoDescriptionView.setFocusable(true);
+ videoDescriptionView.setMovementMethod(new LargeTextMovementMethod());
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
}
}
@@ -511,7 +515,6 @@ public class VideoDetailFragment extends BaseStateFragment
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
- videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view);
thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view);
@@ -533,6 +536,18 @@ public class VideoDetailFragment extends BaseStateFragment
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
setHeightThumbnail();
+
+ thumbnailBackgroundButton.requestFocus();
+
+ if (AndroidTvUtils.isTv()) {
+ // remove ripple effects from detail controls
+ final int transparent = getResources().getColor(R.color.transparent_background_color);
+ detailControlsAddToPlaylist.setBackgroundColor(transparent);
+ detailControlsBackground.setBackgroundColor(transparent);
+ detailControlsPopup.setBackgroundColor(transparent);
+ detailControlsDownload.setBackgroundColor(transparent);
+ }
+
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
index 55301dd50..c87096712 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
@@ -16,7 +16,6 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
@@ -35,6 +34,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.StreamDialogEntry;
+import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.List;
import java.util.Queue;
@@ -56,6 +56,7 @@ public abstract class BaseListFragment extends BaseStateFragment
protected InfoListAdapter infoListAdapter;
protected RecyclerView itemsList;
+ private int focusedPosition = -1;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
@@ -129,20 +130,53 @@ public abstract class BaseListFragment extends BaseStateFragment
return "." + infoListAdapter.getItemsList().size() + ".list";
}
+ private int getFocusedPosition() {
+ View focusedItem = itemsList.getFocusedChild();
+ if (focusedItem != null) {
+ RecyclerView.ViewHolder itemHolder = itemsList.findContainingViewHolder(focusedItem);
+ if (itemHolder != null) {
+ return itemHolder.getAdapterPosition();
+ }
+ }
+
+ return -1;
+ }
+
@Override
public void writeTo(final Queue