From eaa1179572e68dc264a54677314d39e18808e628 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:02:16 +0700 Subject: [PATCH 01/49] Fix scrolling comments list AppBarLayout mostly gets it, but we still need to uphold our own part - expanding it back after focus returns to it --- .../material/appbar/FlingBehavior.java | 39 ++++++++++++++ .../fragments/list/BaseListFragment.java | 3 +- .../views/SuperScrollLayoutManager.java | 53 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java 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 4a2662f53..ea2857b03 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,13 @@ package com.google.android.material.appbar; +import android.annotation.SuppressLint; 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; @@ -13,10 +16,46 @@ import java.lang.reflect.Field; // check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489 public final class FlingBehavior extends AppBarLayout.Behavior { + private final Rect focusScrollRect = new Rect(); + public FlingBehavior(Context context, AttributeSet attrs) { super(context, attrs); } + @Override + public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull Rect rectangle, 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 offset = getTopAndBottomOffset(); + + 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 newOffset = offset + dy; + + int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); + + return consumed == dy; + } + @Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) { switch (ev.getActionMasked()) { 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 d6fd1dd00..a3844a92f 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 @@ -34,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; @@ -147,7 +148,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem } protected RecyclerView.LayoutManager getListLayoutManager() { - return new LinearLayoutManager(activity); + return new SuperScrollLayoutManager(activity); } protected RecyclerView.LayoutManager getGridLayoutManager() { diff --git a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java new file mode 100644 index 000000000..33fe7b9cc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) Eltex ltd 2019 + * SuperScrollLayoutManager.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.view.FocusFinder; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public final class SuperScrollLayoutManager extends LinearLayoutManager { + private final Rect handy = new Rect(); + + public SuperScrollLayoutManager(Context context) { + super(context); + } + + @Override + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate, boolean focusedChildVisible) { + if (!parent.isInTouchMode()) { + // only activate when in directional navigation mode (Android TV etc) — fine grained + // touch scrolling is better served by nested scroll system + + if (!focusedChildVisible || getFocusedChild() == child) { + handy.set(rect); + + parent.offsetDescendantRectToMyCoords(child, handy); + + parent.requestRectangleOnScreen(handy, immediate); + } + } + + return super.requestChildRectangleOnScreen(parent, child, rect, immediate, focusedChildVisible); + } +} From 4806ac62ee62719998aa1361df8f3ce7156e9c06 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:04:13 +0700 Subject: [PATCH 02/49] Correctly move focus from toolbar search bar to dropdown We don't hide MainFragment when search is show, so FocusFinder sometimes gives focus to (obscured) main content --- app/src/main/res/layout/toolbar_search_layout.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/toolbar_search_layout.xml b/app/src/main/res/layout/toolbar_search_layout.xml index fdc7e6d6b..bd5b2d5c7 100644 --- a/app/src/main/res/layout/toolbar_search_layout.xml +++ b/app/src/main/res/layout/toolbar_search_layout.xml @@ -19,6 +19,7 @@ android:drawablePadding="8dp" android:focusable="true" android:focusableInTouchMode="true" + android:nextFocusDown="@+id/suggestions_list" android:hint="@string/search" android:imeOptions="actionSearch|flagNoFullscreen" android:inputType="textFilter|textNoSuggestions" From 8952e2b0cdf362a9afb813f2c4ed20f998d474f5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:07:27 +0700 Subject: [PATCH 03/49] Close DrawerLayout on back button press --- .../main/java/org/schabi/newpipe/MainActivity.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index c24d77d03..3c18c25f6 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,6 +20,7 @@ package org.schabi.newpipe; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -29,6 +30,8 @@ import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -59,6 +62,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.FireTvUtils; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -408,13 +412,20 @@ public class MainActivity extends AppCompatActivity { public void onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); + if (FireTvUtils.isFireTv()) { + View drawerPanel = findViewById(R.id.navigation_layout); + 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 if (fragment instanceof BackPressable) { if (((BackPressable) fragment).onBackPressed()) return; } - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { finish(); } else super.onBackPressed(); From 2b39438eba2f4a78c288fd8a5121ce0672c544d0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:10:57 +0700 Subject: [PATCH 04/49] Fix scrolling in main screen grid GridLayoutManager is buggy - https://issuetracker.google.com/issues/37067220: it randomly loses or incorrectly assigns focus when being scrolled via direction-based navigation. This commit reimplements onFocusSearchFailed() on top of scrollBy() to work around that problem. Ordinary touch-based navigation should not be affected. --- .../fragments/list/BaseListFragment.java | 3 +- .../newpipe/local/BaseLocalListFragment.java | 3 +- .../subscription/SubscriptionFragment.java | 3 +- .../newpipe/views/FixedGridLayoutManager.java | 59 +++++++++++++++ .../newpipe/views/NewPipeRecyclerView.java | 72 +++++++++++++++++++ .../giga/ui/fragment/MissionsFragment.java | 3 +- app/src/main/res/layout/fragment_kiosk.xml | 2 +- 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java 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 a3844a92f..88684f2e7 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 @@ -35,6 +35,7 @@ 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 org.schabi.newpipe.views.FixedGridLayoutManager; import java.util.List; import java.util.Queue; @@ -156,7 +157,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); - final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); + final GridLayoutManager lm = new FixedGridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount)); return lm; } diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 414a9b6b5..c1293e240 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -18,6 +18,7 @@ import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.views.FixedGridLayoutManager; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -95,7 +96,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); - final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); + final GridLayoutManager lm = new FixedGridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(itemListAdapter.getSpanSizeLookup(spanCount)); return lm; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index bff6c1b3a..ea820b71e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -57,6 +57,7 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.CollapsibleView; +import org.schabi.newpipe.views.FixedGridLayoutManager; import java.io.File; import java.text.SimpleDateFormat; @@ -192,7 +193,7 @@ public class SubscriptionFragment extends BaseStateFragment + * FixedGridLayoutManager.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.View; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +// Version of GridLayoutManager that works around https://issuetracker.google.com/issues/37067220 +public class FixedGridLayoutManager extends GridLayoutManager { + public FixedGridLayoutManager(Context context, int spanCount) { + super(context, spanCount); + } + + public FixedGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public FixedGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) { + super(context, spanCount, orientation, reverseLayout); + } + + @Override + public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) { + FocusFinder ff = FocusFinder.getInstance(); + + View result = ff.findNextFocus((ViewGroup) focused.getParent(), focused, focusDirection); + if (result != null) { + return super.onFocusSearchFailed(focused, focusDirection, recycler, state); + } + + if (focusDirection == View.FOCUS_DOWN) { + scrollVerticallyBy(10, recycler, state); + return null; + } + + return super.onFocusSearchFailed(focused, focusDirection, recycler, state); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java new file mode 100644 index 000000000..76dee200f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) Eltex ltd 2019 + * NewPipeRecyclerView.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +public class NewPipeRecyclerView extends RecyclerView { + private static final String TAG = "FixedRecyclerView"; + + public NewPipeRecyclerView(@NonNull Context context) { + super(context); + } + + public NewPipeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public NewPipeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public View focusSearch(int direction) { + return null; + } + + @Override + public View focusSearch(View focused, int direction) { + return null; + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + View found = super.focusSearch(focused, direction); + if (found != null) { + found.requestFocus(direction); + return true; + } + + if (direction == View.FOCUS_UP) { + if (canScrollVertically(-1)) { + scrollBy(0, -10); + return true; + } + + return false; + } + + return super.dispatchUnhandledMove(focused, direction); + } +} diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 26da47b1f..3792f030a 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -30,6 +30,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FixedGridLayoutManager; import java.io.File; import java.io.IOException; @@ -108,7 +109,7 @@ public class MissionsFragment extends Fragment { mList = v.findViewById(R.id.mission_recycler); // Init layouts managers - mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); + mGridManager = new FixedGridLayoutManager(getActivity(), SPAN_SIZE); mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { diff --git a/app/src/main/res/layout/fragment_kiosk.xml b/app/src/main/res/layout/fragment_kiosk.xml index 01eeb0855..643d7d4f0 100644 --- a/app/src/main/res/layout/fragment_kiosk.xml +++ b/app/src/main/res/layout/fragment_kiosk.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - Date: Fri, 20 Sep 2019 16:13:13 +0700 Subject: [PATCH 05/49] MainPlayer: make title and subtitle non-focusable Focus isn't needed for marquee, only selection --- app/src/main/res/layout-large-land/activity_main_player.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index b535db2b8..902e81f1f 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -178,7 +178,6 @@ android:textSize="15sp" android:textStyle="bold" android:clickable="true" - android:focusable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -194,7 +193,6 @@ android:textColor="@android:color/white" android:textSize="12sp" android:clickable="true" - android:focusable="true" tools:text="The Video Artist LONG very LONG very Long"/> From 1bb96ef4058994fe2eabaa93cc5fdcf819acfa44 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:16:21 +0700 Subject: [PATCH 06/49] When child of CoordinatorLayout wants focus, show it! The same logic is present in RecyclerView, ScrollView etc. Android really should default to this behavior for all Views with isScrollContainer = true --- .../newpipe/views/FocusAwareCoordinator.java | 63 +++++++++++++++++++ .../fragment_video_detail.xml | 4 +- 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java new file mode 100644 index 000000000..778e50e52 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareCoordinator.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareCoordinator.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +public final class FocusAwareCoordinator extends CoordinatorLayout { + private final Rect childFocus = new Rect(); + + public FocusAwareCoordinator(@NonNull Context context) { + super(context); + } + + public FocusAwareCoordinator(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareCoordinator(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + + if (!isInTouchMode()) { + if (focused.getHeight() >= getHeight()) { + focused.getFocusedRect(childFocus); + + ((ViewGroup) child).offsetDescendantRectToMyCoords(focused, childFocus); + } else { + focused.getHitRect(childFocus); + + ((ViewGroup) child).offsetDescendantRectToMyCoords((View) focused.getParent(), childFocus); + } + + requestChildRectangleOnScreen(child, childFocus, false); + } + } +} diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 02b0a7b86..186e184f3 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -10,7 +10,7 @@ android:orientation="horizontal" android:baselineAligned="false"> - - + Date: Fri, 20 Sep 2019 16:23:17 +0700 Subject: [PATCH 07/49] Do not discriminate against non-Amazon TV boxes --- .../main/java/org/schabi/newpipe/util/FireTvUtils.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java index 69666463e..879b54e1f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java @@ -1,10 +1,18 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; +import android.content.pm.PackageManager; + import org.schabi.newpipe.App; public class FireTvUtils { + @SuppressLint("InlinedApi") public static boolean isFireTv(){ final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; - return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); + + PackageManager pm = App.getApp().getPackageManager(); + + return pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV) + || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } } From 644ad110c06638c50b4bd3800d3a334b4f976c7d Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:25:30 +0700 Subject: [PATCH 08/49] Make description focusable, so TV users can scroll it --- app/src/main/res/layout-large-land/fragment_video_detail.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 186e184f3..02d330ade 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -490,6 +490,7 @@ android:textAppearance="?android:attr/textAppearanceMedium" android:textIsSelectable="true" android:textSize="@dimen/video_item_detail_description_text_size" + android:focusable="true" tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum." /> Date: Fri, 20 Sep 2019 16:36:57 +0700 Subject: [PATCH 09/49] Improve usability of MainVideoActivity with directional navigation * Hide player controls when back is pressed (only on TV devices) * Do not hide control after click unless in touch mode * Show player controls on dpad usage * Notably increase control hide timeout when not in touch mode --- .../newpipe/player/MainVideoPlayer.java | 53 ++++++++++++++++++- .../schabi/newpipe/player/VideoPlayer.java | 15 +++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 7a3e60c66..5663e1ea2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -1,5 +1,6 @@ /* * Copyright 2017 Mauricio Colli + * Copyright 2019 Eltex ltd * MainVideoPlayer.java is part of NewPipe * * License: GPL-3.0+ @@ -45,6 +46,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; @@ -75,6 +77,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.FireTvUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -89,6 +92,7 @@ import java.util.UUID; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.player.VideoPlayer.DPAD_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateRotation; @@ -187,6 +191,40 @@ public final class MainVideoPlayer extends AppCompatActivity } } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (event.getKeyCode()) { + default: + break; + case KeyEvent.KEYCODE_BACK: + if (FireTvUtils.isFireTv() && playerImpl.isControlsVisible()) { + playerImpl.hideControls(0, 0); + hideSystemUi(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (!playerImpl.isControlsVisible()) { + playerImpl.showControlsThenHide(); + showSystemUi(); + return true; + } else { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return super.onKeyDown(keyCode, event); + } + @Override protected void onResume() { if (DEBUG) Log.d(TAG, "onResume() called"); @@ -692,7 +730,7 @@ public final class MainVideoPlayer extends AppCompatActivity getControlsVisibilityHandler().removeCallbacksAndMessages(null); animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } }); } @@ -898,6 +936,18 @@ public final class MainVideoPlayer extends AppCompatActivity super.showControls(duration); } + @Override + public void safeHideControls(long duration, long delay) { + if (DEBUG) Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); + + View controlsRoot = getControlsRoot(); + if (controlsRoot.isInTouchMode()) { + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + getControlsVisibilityHandler().postDelayed( + () -> animateView(controlsRoot, false, duration, 0, MainVideoPlayer.this::hideSystemUi), delay); + } + } + @Override public void hideControls(final long duration, long delay) { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); @@ -1058,6 +1108,7 @@ public final class MainVideoPlayer extends AppCompatActivity playerImpl.showControlsThenHide(); showSystemUi(); } + return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 360475ba2..0d9c14058 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -97,6 +97,7 @@ public abstract class VideoPlayer extends BasePlayer protected static final int RENDERER_UNAVAILABLE = -1; public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds private List availableStreams; private int selectedStreamIndex; @@ -825,8 +826,11 @@ public abstract class VideoPlayer extends BasePlayer public void showControlsThenHide() { if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); + + final int hideTime = controlsRoot.isInTouchMode() ? DEFAULT_CONTROLS_HIDE_TIME : DPAD_CONTROLS_HIDE_TIME; + animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, - () -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME)); + () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); } public void showControls(long duration) { @@ -835,6 +839,15 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, true, duration); } + public void safeHideControls(final long duration, long delay) { + if (DEBUG) Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); + if (rootView.isInTouchMode()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed( + () -> animateView(controlsRoot, false, duration), delay); + } + } + public void hideControls(final long duration, long delay) { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); controlsVisibilityHandler.removeCallbacksAndMessages(null); From d8bd8d87ec6eb6ca7379740be27d7d7545ae31d3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:42:32 +0700 Subject: [PATCH 10/49] Make player screen controls into buttons Buttons are more likely to have "correct" styling and are focusable/clickable out of box --- .../activity_main_player.xml | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index 902e81f1f..2859b6c5d 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -196,8 +196,9 @@ tools:text="The Video Artist LONG very LONG very Long"/> - - @@ -268,8 +272,9 @@ tools:ignore="RtlHardcoded" tools:visibility="visible"> - - From 7db1ba40ebf530181984c99804b1ab6d392a1742 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:48:34 +0700 Subject: [PATCH 11/49] Do not allow focus to escape from open DrawerLayout Upstream DrawerLayout does override addFocusables, but incorrectly checks for isDrawerOpen instread of isDrawerVisible --- .../newpipe/views/FocusAwareDrawerLayout.java | 69 +++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 4 +- 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java new file mode 100644 index 000000000..0e8097dbe --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareDrawerLayout.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.drawerlayout.widget.DrawerLayout; + +import java.util.ArrayList; + +public final class FocusAwareDrawerLayout extends DrawerLayout { + public FocusAwareDrawerLayout(@NonNull Context context) { + super(context); + } + + public FocusAwareDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + boolean hasOpenPanels = false; + View content = null; + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + + DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + + if (lp.gravity == 0) { + content = child; + } else { + if (isDrawerVisible(child)) { + hasOpenPanels = true; + child.addFocusables(views, direction, focusableMode); + } + } + } + + if (content != null && !hasOpenPanels) { + content.addFocusables(views, direction, focusableMode); + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 92e73234f..a965f5f65 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + From a8a28294d3d43f9720141c561dc1e9fa3b10d421 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 17:42:56 +0700 Subject: [PATCH 12/49] Support for seeking videos in directional navigation mode --- .../newpipe/views/FocusAwareSeekBar.java | 138 ++++++++++++++++++ .../activity_main_player.xml | 2 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java new file mode 100644 index 000000000..3789ea344 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) Eltex ltd 2019 + * FocusAwareDrawerLayout.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.ViewTreeObserver; +import android.widget.SeekBar; + +import androidx.appcompat.widget.AppCompatSeekBar; + +/** + * SeekBar, adapted for directional navigation. It emulates touch-related callbacks + * (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to + * work with it. + */ +public final class FocusAwareSeekBar extends AppCompatSeekBar { + private NestedListener listener; + + private ViewTreeObserver treeObserver; + + public FocusAwareSeekBar(Context context) { + super(context); + } + + public FocusAwareSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FocusAwareSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { + this.listener = l == null ? null : new NestedListener(l); + + super.setOnSeekBarChangeListener(listener); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (!isInTouchMode() && keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + releaseTrack(); + } + + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + if (!isInTouchMode() && !gainFocus) { + releaseTrack(); + } + } + + private final ViewTreeObserver.OnTouchModeChangeListener touchModeListener = isInTouchMode -> { if (isInTouchMode) releaseTrack(); }; + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + treeObserver = getViewTreeObserver(); + treeObserver.addOnTouchModeChangeListener(touchModeListener); + } + + @Override + protected void onDetachedFromWindow() { + if (treeObserver == null || !treeObserver.isAlive()) { + treeObserver = getViewTreeObserver(); + } + + treeObserver.removeOnTouchModeChangeListener(touchModeListener); + treeObserver = null; + + super.onDetachedFromWindow(); + } + + private void releaseTrack() { + if (listener != null && listener.isSeeking) { + listener.onStopTrackingTouch(this); + } + } + + private final class NestedListener implements OnSeekBarChangeListener { + private final OnSeekBarChangeListener delegate; + + boolean isSeeking; + + private NestedListener(OnSeekBarChangeListener delegate) { + this.delegate = delegate; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!seekBar.isInTouchMode() && !isSeeking && fromUser) { + isSeeking = true; + + onStartTrackingTouch(seekBar); + } + + delegate.onProgressChanged(seekBar, progress, fromUser); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + isSeeking = true; + + delegate.onStartTrackingTouch(seekBar); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + isSeeking = false; + + delegate.onStopTrackingTouch(seekBar); + } + } +} diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index 2859b6c5d..c40931a1a 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -401,7 +401,7 @@ tools:text="1:06:29"/> - Date: Mon, 23 Sep 2019 13:50:51 +0700 Subject: [PATCH 13/49] Focus drawer when it opens It is still buggy because of NavigationView (why the hell is NavigationMenuView marked as focusable?) but at least initial opening works as intended --- .../newpipe/views/FocusAwareDrawerLayout.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java index 0e8097dbe..2354427a3 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -17,8 +17,10 @@ */ package org.schabi.newpipe.views; +import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; +import android.view.Gravity; import android.view.View; import androidx.annotation.NonNull; @@ -66,4 +68,35 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { content.addFocusables(views, direction, focusableMode); } } + + // this override isn't strictly necessary, but it is helpful when DrawerLayout isn't the topmost + // view in hierarchy (such as when system or builtin appcompat ActionBar is used) + @Override + @SuppressLint("RtlHardcoded") + public void openDrawer(@NonNull View drawerView, boolean animate) { + super.openDrawer(drawerView, animate); + + LayoutParams params = (LayoutParams) drawerView.getLayoutParams(); + + int gravity = GravityCompat.getAbsoluteGravity(params.gravity, ViewCompat.getLayoutDirection(this)); + + int direction = 0; + + switch (gravity) { + case Gravity.LEFT: + direction = FOCUS_LEFT; + break; + case Gravity.RIGHT: + direction = FOCUS_RIGHT; + break; + case Gravity.TOP: + direction = FOCUS_UP; + break; + case Gravity.BOTTOM: + direction = FOCUS_DOWN; + break; + } + + drawerView.requestFocus(direction); + } } From d23227d427831184f6ae12162593c6112bcd4b11 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 14:17:03 +0700 Subject: [PATCH 14/49] Implement global focus highlight --- .../java/org/schabi/newpipe/MainActivity.java | 5 + .../org/schabi/newpipe/RouterActivity.java | 6 + .../newpipe/download/DownloadActivity.java | 6 + .../newpipe/player/MainVideoPlayer.java | 6 + .../newpipe/views/FocusOverlayView.java | 248 ++++++++++++++++++ 5 files changed, 271 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 3c18c25f6..8d2702d0b 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -69,6 +69,7 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; @@ -121,6 +122,10 @@ public class MainActivity extends AppCompatActivity { } catch (Exception e) { ErrorActivity.reportUiError(this, e); } + + if (FireTvUtils.isFireTv()) { + FocusOverlayView.setupFocusObserver(this); + } } private void setupDrawer() throws Exception { diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 1be6e096a..c5b97f86f 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.FireTvUtils; 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 java.io.Serializable; import java.util.ArrayList; @@ -316,6 +318,10 @@ public class RouterActivity extends AppCompatActivity { selectedPreviously = selectedRadioPosition; alertDialog.show(); + + if (FireTvUtils.isFireTv()) { + FocusOverlayView.setupFocusObserver(alertDialog); + } } private List getChoicesForService(StreamingService service, LinkType linkType) { 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 449a790e8..56265d321 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 android.view.ViewTreeObserver; import org.schabi.newpipe.R; import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.FireTvUtils; 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; @@ -50,6 +52,10 @@ public class DownloadActivity extends AppCompatActivity { getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); + + if (FireTvUtils.isFireTv()) { + FocusOverlayView.setupFocusObserver(this); + } } private void updateFragments() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 5663e1ea2..38da4d8b2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -84,6 +84,7 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; import java.util.List; import java.util.Queue; @@ -141,6 +142,7 @@ public final class MainVideoPlayer extends AppCompatActivity hideSystemUi(); setContentView(R.layout.activity_main_player); + playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); @@ -172,6 +174,10 @@ public final class MainVideoPlayer extends AppCompatActivity getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); + + if (FireTvUtils.isFireTv()) { + FocusOverlayView.setupFocusObserver(this); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java new file mode 100644 index 000000000..b0b9cc421 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -0,0 +1,248 @@ +/* + * Copyright 2019 Alexander Rvachev + * FocusOverlayView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.views; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.WindowCallbackWrapper; + +import org.schabi.newpipe.R; + +import java.lang.ref.WeakReference; + +public final class FocusOverlayView extends Drawable implements + ViewTreeObserver.OnGlobalFocusChangeListener, + ViewTreeObserver.OnDrawListener, + ViewTreeObserver.OnGlobalLayoutListener, + ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnTouchModeChangeListener { + + private boolean isInTouchMode; + + private final Rect focusRect = new Rect(); + + private final Paint rectPaint = new Paint(); + + private final Handler animator = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + updateRect(); + } + }; + + private WeakReference focused; + + public FocusOverlayView(Context context) { + rectPaint.setStyle(Paint.Style.STROKE); + rectPaint.setStrokeWidth(2); + rectPaint.setColor(context.getResources().getColor(R.color.white)); + } + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (newFocus != null && newFocus.getWidth() > 0 && newFocus.getHeight() > 0) { + newFocus.getGlobalVisibleRect(focusRect); + + focused = new WeakReference<>(newFocus); + } else { + focusRect.setEmpty(); + + focused = null; + } + + if (l != focusRect.left || r != focusRect.right || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + + focused = new WeakReference<>(newFocus); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + private void updateRect() { + if (focused == null) { + return; + } + + View focused = this.focused.get(); + + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (focused != null) { + focused.getGlobalVisibleRect(focusRect); + } else { + focusRect.setEmpty(); + } + + if (l != focusRect.left || r != focusRect.right || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + } + + @Override + public void onDraw() { + updateRect(); + } + + @Override + public void onScrollChanged() { + updateRect(); + + animator.removeMessages(0); + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onGlobalLayout() { + updateRect(); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + this.isInTouchMode = isInTouchMode; + + if (isInTouchMode) { + updateRect(); + } else { + invalidateSelf(); + } + } + + public void setCurrentFocus(View focused) { + if (focused == null) { + return; + } + + this.isInTouchMode = focused.isInTouchMode(); + + onGlobalFocusChanged(null, focused); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (!isInTouchMode && focusRect.width() != 0) { + canvas.drawRect(focusRect, rectPaint); + } + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSPARENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + } + + public static void setupFocusObserver(Dialog dialog) { + Rect displayRect = new Rect(); + + Window window = dialog.getWindow(); + assert window != null; + + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(dialog.getContext()); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + public static void setupFocusObserver(Activity activity) { + Rect displayRect = new Rect(); + + Window window = activity.getWindow(); + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(activity); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + private static void setupOverlay(Window window, FocusOverlayView overlay) { + ViewGroup decor = (ViewGroup) window.getDecorView(); + decor.getOverlay().add(overlay); + + ViewTreeObserver observer = decor.getViewTreeObserver(); + observer.addOnScrollChangedListener(overlay); + observer.addOnGlobalFocusChangeListener(overlay); + observer.addOnGlobalLayoutListener(overlay); + observer.addOnTouchModeChangeListener(overlay); + + overlay.setCurrentFocus(decor.getFocusedChild()); + + // Some key presses don't actually move focus, but still result in movement on screen. + // For example, MovementMethod of TextView may cause requestRectangleOnScreen() due to + // some "focusable" spans, which in turn causes CoordinatorLayout to "scroll" it's children. + // Unfortunately many such forms of "scrolling" do not count as scrolling for purpose + // of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly + // receiving keys from Window. + window.setCallback(new WindowCallbackWrapper(window.getCallback()) { + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + boolean res = super.dispatchKeyEvent(event); + overlay.onKey(event); + return res; + } + }); + } + + private void onKey(KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + + updateRect(); + + animator.sendEmptyMessageDelayed(0, 100); + } +} From 28fb864ed01bf0af2989527d984e500f91717fe1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 17:20:15 +0700 Subject: [PATCH 15/49] Focus video view thumbnail after it is loaded --- .../schabi/newpipe/fragments/detail/VideoDetailFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 14e989625..fd2a3285d 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 @@ -505,7 +505,7 @@ public class VideoDetailFragment setHeightThumbnail(); - + thumbnailBackgroundButton.requestFocus(); } @Override From 79c962fc8805183fdbe44b5dfe0ab7b2e32cd141 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 30 Sep 2019 12:02:07 +0700 Subject: [PATCH 16/49] More robust focus search in SuperScrollLayoutManager FocusFinder has glitches when some of target Views have different size. Fortunately LayoutManager can redefine focus search strategy to override the default behavior. --- .../views/SuperScrollLayoutManager.java | 110 +++++++++++++++++- app/src/main/res/layout/fragment_comments.xml | 2 +- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java index 33fe7b9cc..3946b8435 100644 --- a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -19,16 +19,21 @@ package org.schabi.newpipe.views; import android.content.Context; import android.graphics.Rect; -import android.view.FocusFinder; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; + public final class SuperScrollLayoutManager extends LinearLayoutManager { private final Rect handy = new Rect(); + private final ArrayList focusables = new ArrayList<>(); + public SuperScrollLayoutManager(Context context) { super(context); } @@ -50,4 +55,107 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { return super.requestChildRectangleOnScreen(parent, child, rect, immediate, focusedChildVisible); } + + @Nullable + @Override + public View onInterceptFocusSearch(@NonNull View focused, int direction) { + View focusedItem = findContainingItemView(focused); + if (focusedItem == null) { + return super.onInterceptFocusSearch(focused, direction); + } + + int listDirection = getAbsoluteDirection(direction); + if (listDirection == 0) { + return super.onInterceptFocusSearch(focused, direction); + } + + // FocusFinder has an oddity: it considers size of Views more important + // than closeness to source View. This means, that big Views far away from current item + // are preferred to smaller sub-View of closer item. Setting focusability of closer item + // to FOCUS_AFTER_DESCENDANTS does not solve this, because ViewGroup#addFocusables omits + // such parent itself from list, if any of children are focusable. + // Fortunately we can intercept focus search and implement our own logic, based purely + // on position along the LinearLayoutManager axis + + ViewGroup recycler = (ViewGroup) focusedItem.getParent(); + + int sourcePosition = getPosition(focusedItem); + if (sourcePosition == 0 && listDirection < 0) { + return super.onInterceptFocusSearch(focused, direction); + } + + View preferred = null; + + int distance = Integer.MAX_VALUE; + + focusables.clear(); + + recycler.addFocusables(focusables, direction, recycler.isInTouchMode() ? View.FOCUSABLES_TOUCH_MODE : View.FOCUSABLES_ALL); + + try { + for (View view : focusables) { + if (view == focused || view == recycler) { + continue; + } + + int candidate = getDistance(sourcePosition, view, listDirection); + if (candidate < 0) { + continue; + } + + if (candidate < distance) { + distance = candidate; + preferred = view; + } + } + } finally { + focusables.clear(); + } + + return preferred; + } + + private int getAbsoluteDirection(int direction) { + switch (direction) { + default: + break; + case View.FOCUS_FORWARD: + return 1; + case View.FOCUS_BACKWARD: + return -1; + } + + if (getOrientation() == RecyclerView.HORIZONTAL) { + switch (direction) { + default: + break; + case View.FOCUS_LEFT: + return getReverseLayout() ? 1 : -1; + case View.FOCUS_RIGHT: + return getReverseLayout() ? -1 : 1; + } + } else { + switch (direction) { + default: + break; + case View.FOCUS_UP: + return getReverseLayout() ? 1 : -1; + case View.FOCUS_DOWN: + return getReverseLayout() ? -1 : 1; + } + } + + return 0; + } + + private int getDistance(int sourcePosition, View candidate, int direction) { + View itemView = findContainingItemView(candidate); + if (itemView == null) { + return -1; + } + + int position = getPosition(itemView); + + return direction * (position - sourcePosition); + } } diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml index 0ee62c05d..4ced11d35 100644 --- a/app/src/main/res/layout/fragment_comments.xml +++ b/app/src/main/res/layout/fragment_comments.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - Date: Wed, 9 Oct 2019 17:09:07 +0700 Subject: [PATCH 17/49] Allow comment links (if any) to gain focus --- .../holder/CommentsMiniInfoItemHolder.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 4d94ec392..e7b09f3e2 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -1,6 +1,9 @@ package org.schabi.newpipe.info_list.holder; import androidx.appcompat.app.AppCompatActivity; + +import android.text.method.LinkMovementMethod; +import android.text.style.URLSpan; import android.text.util.Linkify; import android.view.ViewGroup; import android.widget.TextView; @@ -122,15 +125,35 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { }); } + private void allowLinkFocus() { + if (itemView.isInTouchMode()) { + return; + } + + URLSpan[] urls = itemContentView.getUrls(); + + if (urls != null && urls.length != 0) { + itemContentView.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + private void ellipsize() { + boolean hasEllipsis = false; + if (itemContentView.getLineCount() > commentDefaultLines){ int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1); int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2); if(end == -1) end = Math.max(endOfLastLine -2, 0); String newVal = itemContentView.getText().subSequence(0, end) + " …"; itemContentView.setText(newVal); + hasEllipsis = true; } + linkify(); + + if (!hasEllipsis) { + allowLinkFocus(); + } } private void toggleEllipsize() { @@ -145,11 +168,13 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { itemContentView.setMaxLines(commentExpandedLines); itemContentView.setText(commentText); linkify(); + allowLinkFocus(); } private void linkify(){ Linkify.addLinks(itemContentView, Linkify.WEB_URLS); Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); + itemContentView.setMovementMethod(null); } } From 6e76610f303af3428f5ec0afc565ce98cc4bb7ed Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 17:57:14 +0700 Subject: [PATCH 18/49] Eliminate bunch of ExoPlayer warnings --- app/src/main/java/org/schabi/newpipe/player/BasePlayer.java | 2 +- .../org/schabi/newpipe/player/playback/MediaSourceManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index a07afcea9..50a60ecb1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -556,7 +556,7 @@ public abstract class BasePlayer implements } private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> triggerProgressUpdate(), error -> Log.e(TAG, "Progress update failure: ", error)); diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 85c852f57..bbe391807 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -318,7 +318,7 @@ public class MediaSourceManager { //////////////////////////////////////////////////////////////////////////*/ private Observable getEdgeIntervalSignal() { - return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) + return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .filter(ignored -> playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis)); } From a7c31e6bcc28067573aebd8a198091cdc053a7f4 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Fri, 8 Nov 2019 14:26:12 +0700 Subject: [PATCH 19/49] RecyclerView scroll fixes * Move all focus-related work arouns to NewPipeRecyclerView * Try to pass focus within closer parents first * Do small arrow scroll if there are not more focusables in move direction --- .../newpipe/views/NewPipeRecyclerView.java | 173 ++++++++++++++++-- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java index 76dee200f..435281d14 100644 --- a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -18,55 +18,202 @@ package org.schabi.newpipe.views; import android.content.Context; +import android.graphics.Rect; +import android.os.Build; import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; public class NewPipeRecyclerView extends RecyclerView { - private static final String TAG = "FixedRecyclerView"; + private static final String TAG = "NewPipeRecyclerView"; + + private Rect focusRect = new Rect(); + private Rect tempFocus = new Rect(); + + private boolean allowDpadScroll; public NewPipeRecyclerView(@NonNull Context context) { super(context); + + init(); } public NewPipeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); + + init(); } public NewPipeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + + init(); } - @Override - public View focusSearch(int direction) { - return null; + private void init() { + setFocusable(true); + + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + } + + public void setFocusScrollAllowed(boolean allowDpadScroll) { + this.allowDpadScroll = allowDpadScroll; } @Override public View focusSearch(View focused, int direction) { + // RecyclerView has buggy focusSearch(), that calls into Adapter several times, + // but ultimately fails to produce correct results in many cases. To add insult to injury, + // it's focusSearch() hard-codes several behaviors, incompatible with widely accepted focus + // handling practices: RecyclerView does not allow Adapter to give focus to itself (!!) and + // always checks, that returned View is located in "correct" direction (which prevents us + // from temporarily giving focus to special hidden View). return null; } + @Override + protected void removeDetachedView(View child, boolean animate) { + if (child.hasFocus()) { + // If the focused child is being removed (can happen during very fast scrolling), + // temporarily give focus to ourselves. This will usually result in another child + // gaining focus (which one does not really matter, because at that point scrolling + // is FAST, and that child will soon be off-screen too) + requestFocus(); + } + + super.removeDetachedView(child, animate); + } + + // we override focusSearch to always return null, so all moves moves lead to dispatchUnhandledMove() + // as added advantage, we can fully swallow some kinds of moves (such as downward movement, that + // happens when loading additional contents is in progress + @Override public boolean dispatchUnhandledMove(View focused, int direction) { - View found = super.focusSearch(focused, direction); - if (found != null) { - found.requestFocus(direction); + tempFocus.setEmpty(); + + // save focus rect before further manipulation (both focusSearch() and scrollBy() + // can mess with focused View by moving it off-screen and detaching) + + if (focused != null) { + View focusedItem = findContainingItemView(focused); + if (focusedItem != null) { + focusedItem.getHitRect(focusRect); + } + } + + // call focusSearch() to initiate layout, but disregard returned View for now + View adapterResult = super.focusSearch(focused, direction); + if (adapterResult != null && !isOutside(adapterResult)) { + adapterResult.requestFocus(direction); return true; } - if (direction == View.FOCUS_UP) { - if (canScrollVertically(-1)) { - scrollBy(0, -10); - return true; - } + if (arrowScroll(direction)) { + // if RecyclerView can not yield focus, but there is still some scrolling space in indicated, + // direction, scroll some fixed amount in that direction (the same logic in ScrollView) + return true; + } - return false; + if (focused != this && direction == FOCUS_DOWN && !allowDpadScroll) { + Log.i(TAG, "Consuming downward scroll: content load in progress"); + return true; + } + + if (tryFocusFinder(direction)) { + return true; + } + + if (adapterResult != null) { + adapterResult.requestFocus(direction); + return true; } return super.dispatchUnhandledMove(focused, direction); } + + private boolean tryFocusFinder(int direction) { + if (Build.VERSION.SDK_INT >= 28) { + // Android 9 implemented bunch of handy changes to focus, that render code below less useful, and + // also broke findNextFocusFromRect in way, that render this hack useless + return false; + } + + FocusFinder finder = FocusFinder.getInstance(); + + // try to use FocusFinder instead of adapter + ViewGroup root = (ViewGroup) getRootView(); + + tempFocus.set(focusRect); + + root.offsetDescendantRectToMyCoords(this, tempFocus); + + View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction); + if (focusFinderResult != null && !isOutside(focusFinderResult)) { + focusFinderResult.requestFocus(direction); + return true; + } + + // look for focus in our ancestors, increasing search scope with each failure + // this provides much better locality than using FocusFinder with root + ViewGroup parent = (ViewGroup) getParent(); + + while (parent != root) { + tempFocus.set(focusRect); + + parent.offsetDescendantRectToMyCoords(this, tempFocus); + + View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction); + if (candidate != null && candidate.requestFocus(direction)) { + return true; + } + + parent = (ViewGroup) parent.getParent(); + } + + return false; + } + + private boolean arrowScroll(int direction) { + switch (direction) { + case FOCUS_DOWN: + if (!canScrollVertically(1)) { + return false; + } + scrollBy(0, 100); + break; + case FOCUS_UP: + if (!canScrollVertically(-1)) { + return false; + } + scrollBy(0, -100); + break; + case FOCUS_LEFT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(-100, 0); + break; + case FOCUS_RIGHT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(100, 0); + break; + default: + return false; + } + + return true; + } + + private boolean isOutside(View view) { + return findContainingItemView(view) == null; + } } From b5558a8b78962234d855f318e1b059bd88892242 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Fri, 8 Nov 2019 14:41:16 +0700 Subject: [PATCH 20/49] Remove FixedGridLayoutManager --- .../fragments/list/BaseListFragment.java | 3 +- .../newpipe/local/BaseLocalListFragment.java | 3 +- .../subscription/SubscriptionFragment.java | 3 +- .../newpipe/views/FixedGridLayoutManager.java | 59 ------------------- .../giga/ui/fragment/MissionsFragment.java | 3 +- 5 files changed, 4 insertions(+), 67 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java 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 88684f2e7..a3844a92f 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 @@ -35,7 +35,6 @@ 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 org.schabi.newpipe.views.FixedGridLayoutManager; import java.util.List; import java.util.Queue; @@ -157,7 +156,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); - final GridLayoutManager lm = new FixedGridLayoutManager(activity, spanCount); + final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount)); return lm; } diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index c1293e240..414a9b6b5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -18,7 +18,6 @@ import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; -import org.schabi.newpipe.views.FixedGridLayoutManager; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -96,7 +95,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); width += (24 * resources.getDisplayMetrics().density); final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width); - final GridLayoutManager lm = new FixedGridLayoutManager(activity, spanCount); + final GridLayoutManager lm = new GridLayoutManager(activity, spanCount); lm.setSpanSizeLookup(itemListAdapter.getSpanSizeLookup(spanCount)); return lm; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index ea820b71e..bff6c1b3a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -57,7 +57,6 @@ import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.CollapsibleView; -import org.schabi.newpipe.views.FixedGridLayoutManager; import java.io.File; import java.text.SimpleDateFormat; @@ -193,7 +192,7 @@ public class SubscriptionFragment extends BaseStateFragment - * FixedGridLayoutManager.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ -package org.schabi.newpipe.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.FocusFinder; -import android.view.View; -import android.view.ViewGroup; - -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -// Version of GridLayoutManager that works around https://issuetracker.google.com/issues/37067220 -public class FixedGridLayoutManager extends GridLayoutManager { - public FixedGridLayoutManager(Context context, int spanCount) { - super(context, spanCount); - } - - public FixedGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public FixedGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) { - super(context, spanCount, orientation, reverseLayout); - } - - @Override - public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) { - FocusFinder ff = FocusFinder.getInstance(); - - View result = ff.findNextFocus((ViewGroup) focused.getParent(), focused, focusDirection); - if (result != null) { - return super.onFocusSearchFailed(focused, focusDirection, recycler, state); - } - - if (focusDirection == View.FOCUS_DOWN) { - scrollVerticallyBy(10, recycler, state); - return null; - } - - return super.onFocusSearchFailed(focused, focusDirection, recycler, state); - } -} diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 3792f030a..26da47b1f 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -30,7 +30,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.views.FixedGridLayoutManager; import java.io.File; import java.io.IOException; @@ -109,7 +108,7 @@ public class MissionsFragment extends Fragment { mList = v.findViewById(R.id.mission_recycler); // Init layouts managers - mGridManager = new FixedGridLayoutManager(getActivity(), SPAN_SIZE); + mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { From 9801cf50e38abd0214340d48dba1e5b8e1572cef Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:34:31 +0659 Subject: [PATCH 21/49] Save/restore focused item --- .../fragments/list/BaseListFragment.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 a3844a92f..a2821a65e 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 @@ -34,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.NewPipeRecyclerView; import org.schabi.newpipe.views.SuperScrollLayoutManager; import java.util.List; @@ -50,6 +51,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem protected InfoListAdapter infoListAdapter; protected RecyclerView itemsList; private int updateFlags = 0; + private int focusedPosition = -1; private static final int LIST_MODE_UPDATE_FLAG = 0x32; @@ -111,9 +113,22 @@ public abstract class BaseListFragment extends BaseStateFragment implem 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(Queue objectsToSave) { objectsToSave.add(infoListAdapter.getItemsList()); + objectsToSave.add(getFocusedPosition()); } @Override @@ -121,6 +136,20 @@ public abstract class BaseListFragment extends BaseStateFragment implem public void readFrom(@NonNull Queue savedObjects) throws Exception { infoListAdapter.getItemsList().clear(); infoListAdapter.getItemsList().addAll((List) savedObjects.poll()); + restoreFocus((Integer) savedObjects.poll()); + } + + private void restoreFocus(Integer position) { + if (position == null || position < 0) { + return; + } + + itemsList.post(() -> { + RecyclerView.ViewHolder focusedHolder = itemsList.findViewHolderForAdapterPosition(position); + if (focusedHolder != null) { + focusedHolder.itemView.requestFocus(); + } + }); } @Override @@ -135,6 +164,18 @@ public abstract class BaseListFragment extends BaseStateFragment implem savedState = StateSaver.tryToRestore(bundle, this); } + @Override + public void onStop() { + focusedPosition = getFocusedPosition(); + super.onStop(); + } + + @Override + public void onStart() { + super.onStart(); + restoreFocus(focusedPosition); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ From 7bb5cacb0dff6f167046f612af5caf3f603f69da Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:37:16 +0659 Subject: [PATCH 22/49] Special MovementMethod for video description Video descriptions can be very long. Some of them are basically walls of text with couple of lines at top or bottom. They are also not scrolled within TextView itself, - instead NewPipe expects user to scroll their containing ViewGroup. This renders all builtin MovementMethod implementations useless. This commit adds a new MovementMethod, that uses requestRectangleOnScreen to intelligently re-position the TextView within it's scrollable container. --- .../fragments/detail/VideoDetailFragment.java | 5 +- .../views/LargeTextMovementMethod.java | 290 ++++++++++++++++++ .../fragment_video_detail.xml | 1 + 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java 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 fd2a3285d..c698d4ad4 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 @@ -87,6 +87,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; @@ -441,10 +442,13 @@ public class VideoDetailFragment 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); } } @@ -481,7 +485,6 @@ public class VideoDetailFragment 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()); videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view); diff --git a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java new file mode 100644 index 000000000..1f9ab5e2d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java @@ -0,0 +1,290 @@ +/* + * Copyright 2019 Alexander Rvachev + * FocusOverlayView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.views; + +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TextView; + +public class LargeTextMovementMethod extends LinkMovementMethod { + private final Rect visibleRect = new Rect(); + + private int dir; + + @Override + public void onTakeFocus(TextView view, Spannable text, int dir) { + Selection.removeSelection(text); + + super.onTakeFocus(view, text, dir); + + this.dir = dirToRelative(dir); + } + + @Override + protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event) { + if (!doHandleMovement(widget, buffer, keyCode, movementMetaState, event)) { + // clear selection to make sure, that it does not confuse focus handling code + Selection.removeSelection(buffer); + return false; + } + + return true; + } + + private boolean doHandleMovement(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event) { + int newDir = keyToDir(keyCode); + + if (dir != 0 && newDir != dir) { + return false; + } + + this.dir = 0; + + ViewGroup root = findScrollableParent(widget); + + widget.getHitRect(visibleRect); + + root.offsetDescendantRectToMyCoords((View) widget.getParent(), visibleRect); + + return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); + } + + @Override + protected boolean up(TextView widget, Spannable buffer) { + if (gotoPrev(widget, buffer)) { + return true; + } + + return super.up(widget, buffer); + } + + @Override + protected boolean left(TextView widget, Spannable buffer) { + if (gotoPrev(widget, buffer)) { + return true; + } + + return super.left(widget, buffer); + } + + @Override + protected boolean right(TextView widget, Spannable buffer) { + if (gotoNext(widget, buffer)) { + return true; + } + + return super.right(widget, buffer); + } + + @Override + protected boolean down(TextView widget, Spannable buffer) { + if (gotoNext(widget, buffer)) { + return true; + } + + return super.down(widget, buffer); + } + + private boolean gotoPrev(TextView view, Spannable buffer) { + Layout layout = view.getLayout(); + if (layout == null) { + return false; + } + + View root = findScrollableParent(view); + + int rootHeight = root.getHeight(); + + if (visibleRect.top >= 0) { + // we fit entirely into the viewport, no need for fancy footwork + return false; + } + + int topExtra = -visibleRect.top; + + int firstVisibleLineNumber = layout.getLineForVertical(topExtra); + + // when deciding whether to pass "focus" to span, account for one more line + // this ensures, that focus is never passed to spans partially outside scroll window + int visibleStart = firstVisibleLineNumber == 0 ? 0 : layout.getLineStart(firstVisibleLineNumber - 1); + + ClickableSpan[] candidates = buffer.getSpans(visibleStart, buffer.length(), ClickableSpan.class); + + if (candidates.length != 0) { + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + int bestStart = -1; + int bestEnd = -1; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + int end = buffer.getSpanEnd(candidates[i]); + + if ((end < selEnd || selStart == selEnd) && start >= visibleStart) { + if (end > bestEnd) { + bestStart = buffer.getSpanStart(candidates[i]); + bestEnd = end; + } + } + } + + if (bestStart >= 0) { + Selection.setSelection(buffer, bestEnd, bestStart); + return true; + } + } + + float fourLines = view.getTextSize() * 4; + + visibleRect.left = 0; + visibleRect.right = view.getWidth(); + visibleRect.top = Math.max(0, (int) (topExtra - fourLines)); + visibleRect.bottom = visibleRect.top + rootHeight; + + return view.requestRectangleOnScreen(visibleRect); + } + + private boolean gotoNext(TextView view, Spannable buffer) { + Layout layout = view.getLayout(); + if (layout == null) { + return false; + } + + View root = findScrollableParent(view); + + int rootHeight = root.getHeight(); + + if (visibleRect.bottom <= rootHeight) { + // we fit entirely into the viewport, no need for fancy footwork + return false; + } + + int bottomExtra = visibleRect.bottom - rootHeight; + + int visibleBottomBorder = view.getHeight() - bottomExtra; + + int lineCount = layout.getLineCount(); + + int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder); + + // when deciding whether to pass "focus" to span, account for one more line + // this ensures, that focus is never passed to spans partially outside scroll window + int visibleEnd = lastVisibleLineNumber == lineCount - 1 ? buffer.length() : layout.getLineEnd(lastVisibleLineNumber - 1); + + ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class); + + if (candidates.length != 0) { + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + int bestStart = Integer.MAX_VALUE; + int bestEnd = Integer.MAX_VALUE; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + int end = buffer.getSpanEnd(candidates[i]); + + if ((start > selStart || selStart == selEnd) && end <= visibleEnd) { + if (start < bestStart) { + bestStart = start; + bestEnd = buffer.getSpanEnd(candidates[i]); + } + } + } + + if (bestEnd < Integer.MAX_VALUE) { + // cool, we have managed to find next link without having to adjust self within view + Selection.setSelection(buffer, bestStart, bestEnd); + return true; + } + } + + // there are no links within visible area, but still some text past visible area + // scroll visible area further in required direction + float fourLines = view.getTextSize() * 4; + + visibleRect.left = 0; + visibleRect.right = view.getWidth(); + visibleRect.bottom = Math.min((int) (visibleBottomBorder + fourLines), view.getHeight()); + visibleRect.top = visibleRect.bottom - rootHeight; + + return view.requestRectangleOnScreen(visibleRect); + } + + private ViewGroup findScrollableParent(View view) { + View current = view; + + ViewParent parent; + do { + parent = current.getParent(); + + if (parent == current || !(parent instanceof View)) { + return (ViewGroup) view.getRootView(); + } + + current = (View) parent; + + if (current.isScrollContainer()) { + return (ViewGroup) current; + } + } + while (true); + } + + private int dirToRelative(int dir) { + switch (dir) { + case View.FOCUS_DOWN: + case View.FOCUS_RIGHT: + return View.FOCUS_FORWARD; + case View.FOCUS_UP: + case View.FOCUS_LEFT: + return View.FOCUS_BACKWARD; + } + + return dir; + } + + private int keyToDir(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + return View.FOCUS_BACKWARD; + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + return View.FOCUS_FORWARD; + } + + return View.FOCUS_FORWARD; + } +} diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 02d330ade..6d54525db 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -15,6 +15,7 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="5" + android:isScrollContainer="true" android:fitsSystemWindows="true"> Date: Thu, 14 Nov 2019 20:48:19 +0659 Subject: [PATCH 23/49] Add hints for focus transition from description --- .../main/res/layout-large-land/fragment_video_detail.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 6d54525db..e1a680e5d 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -379,6 +379,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" + android:focusable="true" + android:descendantFocusability="afterDescendants" android:padding="6dp"> @@ -467,6 +469,8 @@ android:layout_marginTop="5dp" android:orientation="vertical" android:visibility="gone" + android:focusable="true" + android:descendantFocusability="afterDescendants" tools:visibility="visible"> Date: Thu, 14 Nov 2019 20:50:35 +0659 Subject: [PATCH 24/49] More fixes to comment focus handling --- .../holder/CommentsMiniInfoItemHolder.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index e7b09f3e2..198766069 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -126,14 +126,28 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { } private void allowLinkFocus() { + itemContentView.setMovementMethod(LinkMovementMethod.getInstance()); + } + + private void denyLinkFocus() { + itemContentView.setMovementMethod(null); + } + + private boolean shouldFocusLinks() { if (itemView.isInTouchMode()) { - return; + return false; } URLSpan[] urls = itemContentView.getUrls(); - if (urls != null && urls.length != 0) { - itemContentView.setMovementMethod(LinkMovementMethod.getInstance()); + return urls != null && urls.length != 0; + } + + private void determineLinkFocus() { + if (shouldFocusLinks()) { + allowLinkFocus(); + } else { + denyLinkFocus(); } } @@ -151,8 +165,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { linkify(); - if (!hasEllipsis) { - allowLinkFocus(); + if (hasEllipsis) { + denyLinkFocus(); + } else { + determineLinkFocus(); } } @@ -168,13 +184,11 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { itemContentView.setMaxLines(commentExpandedLines); itemContentView.setText(commentText); linkify(); - allowLinkFocus(); + determineLinkFocus(); } private void linkify(){ Linkify.addLinks(itemContentView, Linkify.WEB_URLS); Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); - - itemContentView.setMovementMethod(null); } } From 7d75950624f56512a713a2b1ba4cbdc700d89673 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:54:40 +0659 Subject: [PATCH 25/49] Disable srolling down comment list while comments are loading Prevents comment list from losing focus to some outside View when user tries to scroll down after reaching "end" --- .../fragments/list/BaseListInfoFragment.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 9a8e1fd17..7363d221c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.views.NewPipeRecyclerView; import java.util.Queue; @@ -17,6 +18,8 @@ import icepick.State; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public abstract class BaseListInfoFragment @@ -136,9 +139,13 @@ public abstract class BaseListInfoFragment isLoading.set(true); if (currentWorker != null) currentWorker.dispose(); + + forbidDownwardFocusScroll(); + currentWorker = loadMoreItemsLogic() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) + .doFinally(this::allowDownwardFocusScroll) .subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { isLoading.set(false); handleNextItems(InfoItemsPage); @@ -148,6 +155,18 @@ public abstract class BaseListInfoFragment }); } + private void forbidDownwardFocusScroll() { + if (itemsList instanceof NewPipeRecyclerView) { + ((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(false); + } + } + + private void allowDownwardFocusScroll() { + if (itemsList instanceof NewPipeRecyclerView) { + ((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(true); + } + } + @Override public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); From 436c75ca6c081b34388a835924b1270277d50ffe Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 22:43:54 +0659 Subject: [PATCH 26/49] Make comment pic explicitly non-focusable --- app/src/main/res/layout/list_comments_item.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/list_comments_item.xml b/app/src/main/res/layout/list_comments_item.xml index 393d7d1b4..41606201f 100644 --- a/app/src/main/res/layout/list_comments_item.xml +++ b/app/src/main/res/layout/list_comments_item.xml @@ -18,6 +18,7 @@ android:layout_alignParentTop="true" android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:contentDescription="@string/list_thumbnail_view_description" + android:focusable="false" android:src="@drawable/buddy" tools:ignore="RtlHardcoded" /> From a1e02f770434237b0be4bf0bf8a938a33018b509 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 16 Nov 2019 13:02:46 +0659 Subject: [PATCH 27/49] Default to landscape orientation for Android TV --- .../main/java/org/schabi/newpipe/player/MainVideoPlayer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 38da4d8b2..0650e2a26 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -164,13 +164,14 @@ public final class MainVideoPlayer extends AppCompatActivity super.onChange(selfChange); if (globalScreenOrientationLocked()) { final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), false); + getString(R.string.last_orientation_landscape_key), FireTvUtils.isFireTv()); setLandscape(lastOrientationWasLandscape); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } }; + getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); @@ -238,7 +239,7 @@ public final class MainVideoPlayer extends AppCompatActivity if (globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), false); + getString(R.string.last_orientation_landscape_key), FireTvUtils.isFireTv()); setLandscape(lastOrientationWasLandscape); } From c0fb96a911c7c0464e26b78597e58aa96b19bde9 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 16 Nov 2019 13:05:59 +0659 Subject: [PATCH 28/49] Release seekbar on any confirmation key, not just DPAD_CENTER --- .../java/org/schabi/newpipe/util/FireTvUtils.java | 13 +++++++++++++ .../org/schabi/newpipe/views/FocusAwareSeekBar.java | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java index 879b54e1f..2c5090381 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.util; import android.annotation.SuppressLint; import android.content.pm.PackageManager; +import android.view.KeyEvent; import org.schabi.newpipe.App; public class FireTvUtils { @@ -15,4 +16,16 @@ public class FireTvUtils { return pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV) || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } + + public static boolean isConfirmKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_SPACE: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + return true; + default: + return false; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index 3789ea344..dafd5ae6f 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -25,6 +25,7 @@ import android.view.ViewTreeObserver; import android.widget.SeekBar; import androidx.appcompat.widget.AppCompatSeekBar; +import org.schabi.newpipe.util.FireTvUtils; /** * SeekBar, adapted for directional navigation. It emulates touch-related callbacks @@ -57,7 +58,7 @@ public final class FocusAwareSeekBar extends AppCompatSeekBar { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isInTouchMode() && keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + if (!isInTouchMode() && FireTvUtils.isConfirmKey(keyCode)) { releaseTrack(); } From dc7ae3917e97889def2f7315e7ca4d947dbdd847 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 17 Nov 2019 16:53:11 +0659 Subject: [PATCH 29/49] Leanback launcher support --- app/src/main/AndroidManifest.xml | 2 ++ .../main/res/mipmap-xhdpi/newpipe_tv_banner.png | Bin 0 -> 2138 bytes 2 files changed, 2 insertions(+) create mode 100644 app/src/main/res/mipmap-xhdpi/newpipe_tv_banner.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9052dabab..3583d0312 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + diff --git a/app/src/main/res/mipmap-xhdpi/newpipe_tv_banner.png b/app/src/main/res/mipmap-xhdpi/newpipe_tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..4be6644504b178aa612b218202626b4b5d9e2a26 GIT binary patch literal 2138 zcmdT`i#OZZ7XC#QnU2k3^SOFRAEAiK4{2qg9Wpsz+;r^d`u7M|GxkJDS|2 zC{fXiW|)dfYcdk*cG{sSDOIH&p*5i-5d=Xb++WxH6LZg6`@Fuj&feeNXPxhqhKKs= zeqr(j006p}0N-x`0D=bRPjs}vepa-u4-T4ylbAC)V9VD@x(TkKR|BF60I;X+vq9EN z7BS$aai(8XW&|!JlbC={28cwWQ)&j5keF~a*$IcgOqZcd06=>bt2* zMeoX}pV}{@+PGul%LFUyK0z?8J`I&jq%`8(>>UF-6Jtb$h7z@Ke#%%y_cwc7=)(@U#6inNelwd%a{vI+na zOUH810is!LOF}=^)VopvXb23@b0!Z8RI5}2FI_0(U3LXvpaw*zYPU8-E+#>bi^Z_%xW=$$Ll_em== zq9!KQXa9l{F^JWqcyUHA@XOD^`YPc_;T>rgaTI<{tVuER*!+;x^OLCL0Cn}XM2QG5 z2iSyAuM<^B=N!eDXuh83se9%yN!nRkE0Xuo2E4)^goY@_m=eQhURES2_r4E}Ckw1_ zz`%m%m54_ZniL`OlCMi%whJ~AFKvrsXk}Lc@_{%!M!vx0uPw0NwfXC##fMyMs$3dX zX+7x%3O$X0IDhj6H)!6MkOtyUW^{*`J1E8737fT8jesR%gk8s+2?sOJ(WFdq7Tx&Q z_VPPEWfz!iIzt+kYW{gju9!&X|GxYYbfro09C;g2;qWl(bOuc&iH~F4{1Iv?+*GGi zZ#jvT2t*(-b}YW=o@&{rD+gXf=~Ut6s#X3!dWY%zJiI~dmQm#uAu*nv;M3Jn8CQ|7 zh{i8<@praILEI8#+m5Aq$n3l_3unu72^&IWhg!i-PZ?^+C46+&vE<>lmyk*YhVtCf1I>#%RcUk2v}ppmq+gg# zPa0#(TiPaUq%cUshI%t`Ov^PTST5tO&+RvjEJSYd;rz{&^|>LiC`Qghx0eO50f$zg3t+C(%ON? zDdpiI^a_;{+tp!o;coug>8*b&inT{T81!i&8LjM{(YK~D87w4m`NJXy)5)ey>!X#7 zv%duX+(Qm~y7`}}$>#^0gAUFKf|QyPvXi*EEVo+UT9x$R7RTAmlHrIy$h#j6Erq5i z_YTFlNXC;CM)0z*E`$`kq=mmR{W*S+ICkJ~&!AQn+o)5k`(gz3Xh{rAEk?pY!h#VP3oEaZY=x zfgz$(*AtX%Wa_;OF=OjKL7+Q~cxHXH3pc@SHi~~}+WqdAzVBMbB^#+fhLP546P~}? zF2Qv)%0}jNO{xACArFF^whw+&ZA}{Hq$jJ@8}J+EF+_!QY__*QzThm;DFnP@;+EL2 z?IZ8ti1E2ivh&q2(y(h%0Q;i1Fx_4%V)Nn1}EMnwpr*3{}cND2vDH#2(5m z#uYCVdG5F$-&M&U8?V$GK`qt2VP56Ct7Yc*I@cpdnfD>)#S2pvj(pdFs2DWE5Y27B znh+hcyt5t0xUQ9>`M`9#kRDuHQ$REYW-$bGK=NYK6tr$wa= z<%RXpf`oU?>?ao|-B;@4SD=)s$AtyaMaK%bjeFOEz$fGG2_8zuIzP6K`QQwm%yRAy zh6USk++UCjU&GPjtwoN!kX&ZU^Mp#jf;7}R#=f$U9!6>G`AZ^RzMkmpa6tV4`F$4{ zJHF2dHX6_9XH2hS>P#5GSlhI-qn?-6AqKETpu5=~@JdUCfYp)vR~#JFA^$gxliS|7 z7tweb%WjwW47M3vuyeMkcKKsSquPpO<8uSntr}WCQaA8ffp+)&U+U9eYN|xG(W$3i T=$3)s006-Fh5E8iUcB}{BjNT* literal 0 HcmV?d00001 From 106e538d0824d23604ce83c8bf9ad0916dc65ee5 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 17 Nov 2019 16:54:18 +0659 Subject: [PATCH 30/49] Excpicitly disable touchscreen requirement --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3583d0312..3284202fd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + Date: Sun, 17 Nov 2019 16:55:22 +0659 Subject: [PATCH 31/49] Disable touchScreenBlocksFocus on AppBarLayout For some inexplicable reason this attribute got enabled by default on Android 9, which effectively prevents details screen from working --- app/src/main/res/layout-large-land/fragment_video_detail.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index e1a680e5d..684adc222 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -23,6 +23,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" + android:touchscreenBlocksFocus="false" android:fitsSystemWindows="true" app:elevation="0dp" app:layout_behavior="com.google.android.material.appbar.FlingBehavior"> From 29136d633a9f54d774e5dbc39b85c73d6b2b06ae Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 1 Dec 2019 12:33:34 +0659 Subject: [PATCH 32/49] Intercept ActivityNotFoundException for ACTION_CAPTIONING_SETTINGS --- .../newpipe/settings/AppearanceSettingsFragment.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index ce22b84e9..72d720824 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.settings; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.preference.Preference; @@ -42,7 +44,11 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { - startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); + try { + startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.general_error, Toast.LENGTH_SHORT).show(); + } } return super.onPreferenceTreeClick(preference); From 3f51114129c813ac83bf658f88934f968990faa9 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 1 Dec 2019 12:38:01 +0659 Subject: [PATCH 33/49] Improve usability of settings on TV devices * Add focus overlay to SettingsActivity * Make screen "Contents of Main Page" navigable from remote --- .../java/org/schabi/newpipe/settings/SettingsActivity.java | 6 ++++++ app/src/main/res/layout/list_choose_tabs.xml | 1 + 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index a3f218074..e53b7ba07 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -12,7 +12,9 @@ import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.FireTvUtils; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.FocusOverlayView; /* @@ -56,6 +58,10 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc .replace(R.id.fragment_holder, new MainSettingsFragment()) .commit(); } + + if (FireTvUtils.isFireTv()) { + FocusOverlayView.setupFocusObserver(this); + } } @Override diff --git a/app/src/main/res/layout/list_choose_tabs.xml b/app/src/main/res/layout/list_choose_tabs.xml index ce17e0382..82c9dd081 100644 --- a/app/src/main/res/layout/list_choose_tabs.xml +++ b/app/src/main/res/layout/list_choose_tabs.xml @@ -12,6 +12,7 @@ android:layout_marginTop="3dp" android:minHeight="?listPreferredItemHeightSmall" android:orientation="horizontal" + android:focusable="true" app:cardCornerRadius="5dp" app:cardElevation="4dp"> From 8c9015b57bd489a192e69f4a8977c745810403b2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Tue, 10 Dec 2019 21:21:35 +0659 Subject: [PATCH 34/49] Remove commented code --- .../com/google/android/material/appbar/FlingBehavior.java | 5 ----- 1 file changed, 5 deletions(-) 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 ea2857b03..3af2c95bc 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,6 +1,5 @@ package com.google.android.material.appbar; -import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; @@ -35,8 +34,6 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return false; } - int offset = getTopAndBottomOffset(); - int dy; if (focusScrollRect.bottom > height) { @@ -49,8 +46,6 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return false; } - //int newOffset = offset + dy; - int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); return consumed == dy; From caa1de8aff77b92255d68113ade22afe7d17e8ed Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Wed, 29 Jan 2020 03:15:50 +0659 Subject: [PATCH 35/49] Rename FireTvUtils to AndroidTvUtils and isFireTv() to isTV() Because those methods are no longer exclusive to Amazon devices --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 6 +++--- .../main/java/org/schabi/newpipe/RouterActivity.java | 4 ++-- .../org/schabi/newpipe/download/DownloadActivity.java | 5 ++--- .../newpipe/fragments/list/search/SearchFragment.java | 4 ++-- .../org/schabi/newpipe/player/MainVideoPlayer.java | 10 +++++----- .../org/schabi/newpipe/settings/SettingsActivity.java | 4 ++-- .../util/{FireTvUtils.java => AndroidTvUtils.java} | 4 ++-- .../org/schabi/newpipe/views/FocusAwareSeekBar.java | 4 ++-- 8 files changed, 20 insertions(+), 21 deletions(-) rename app/src/main/java/org/schabi/newpipe/util/{FireTvUtils.java => AndroidTvUtils.java} (92%) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index a2f161847..d2cbb49e0 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -69,7 +69,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.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PeertubeHelper; @@ -140,7 +140,7 @@ public class MainActivity extends AppCompatActivity { ErrorActivity.reportUiError(this, e); } - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } @@ -489,7 +489,7 @@ public class MainActivity extends AppCompatActivity { public void onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { View drawerPanel = findViewById(R.id.navigation_layout); if (drawer.isDrawerOpen(drawerPanel)) { drawer.closeDrawers(); diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index c5b97f86f..412bea0e1 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -45,7 +45,7 @@ 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.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -319,7 +319,7 @@ public class RouterActivity extends AppCompatActivity { alertDialog.show(); - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(alertDialog); } } 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 6ceacbb05..514c3dd37 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -12,8 +12,7 @@ import android.view.MenuItem; import android.view.ViewTreeObserver; import org.schabi.newpipe.R; -import org.schabi.newpipe.settings.SettingsActivity; -import org.schabi.newpipe.util.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -53,7 +52,7 @@ public class DownloadActivity extends AppCompatActivity { } }); - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index f2e8aa244..9e4fd467c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -40,7 +40,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; -import org.schabi.newpipe.util.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -471,7 +471,7 @@ public class SearchFragment if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } - if(FireTvUtils.isFireTv()){ + if(AndroidTvUtils.isTv()){ showKeyboardSearch(); } }); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index fa742f771..470e1c963 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -78,7 +78,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -166,7 +166,7 @@ public final class MainVideoPlayer extends AppCompatActivity super.onChange(selfChange); if (globalScreenOrientationLocked()) { final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), FireTvUtils.isFireTv()); + getString(R.string.last_orientation_landscape_key), AndroidTvUtils.isTv()); setLandscape(lastOrientationWasLandscape); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); @@ -178,7 +178,7 @@ public final class MainVideoPlayer extends AppCompatActivity Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } @@ -206,7 +206,7 @@ public final class MainVideoPlayer extends AppCompatActivity default: break; case KeyEvent.KEYCODE_BACK: - if (FireTvUtils.isFireTv() && playerImpl.isControlsVisible()) { + if (AndroidTvUtils.isTv() && playerImpl.isControlsVisible()) { playerImpl.hideControls(0, 0); hideSystemUi(); return true; @@ -241,7 +241,7 @@ public final class MainVideoPlayer extends AppCompatActivity if (globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), FireTvUtils.isFireTv()); + getString(R.string.last_orientation_landscape_key), AndroidTvUtils.isTv()); setLandscape(lastOrientationWasLandscape); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index e53b7ba07..49d6d49fe 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -12,7 +12,7 @@ import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -59,7 +59,7 @@ public class SettingsActivity extends AppCompatActivity implements BasePreferenc .commit(); } - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java similarity index 92% rename from app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java rename to app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java index 2c5090381..203501a51 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java @@ -6,9 +6,9 @@ import android.content.pm.PackageManager; import android.view.KeyEvent; import org.schabi.newpipe.App; -public class FireTvUtils { +public class AndroidTvUtils { @SuppressLint("InlinedApi") - public static boolean isFireTv(){ + public static boolean isTv(){ final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; PackageManager pm = App.getApp().getPackageManager(); diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index dafd5ae6f..8ccff85d5 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -25,7 +25,7 @@ import android.view.ViewTreeObserver; import android.widget.SeekBar; import androidx.appcompat.widget.AppCompatSeekBar; -import org.schabi.newpipe.util.FireTvUtils; +import org.schabi.newpipe.util.AndroidTvUtils; /** * SeekBar, adapted for directional navigation. It emulates touch-related callbacks @@ -58,7 +58,7 @@ public final class FocusAwareSeekBar extends AppCompatSeekBar { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isInTouchMode() && FireTvUtils.isConfirmKey(keyCode)) { + if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) { releaseTrack(); } From 6a3a72eb065bdf9419e58f01e90e4bf4a576d6a2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Wed, 26 Feb 2020 06:40:46 +0659 Subject: [PATCH 36/49] NewPipeRecyclerView should allow scrolling down by default --- .../main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java index 435281d14..41b823db8 100644 --- a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -36,7 +36,7 @@ public class NewPipeRecyclerView extends RecyclerView { private Rect focusRect = new Rect(); private Rect tempFocus = new Rect(); - private boolean allowDpadScroll; + private boolean allowDpadScroll = true; public NewPipeRecyclerView(@NonNull Context context) { super(context); From 1cc5a67d82a555424fa3d22ff744434369060ea2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 12 Mar 2020 05:29:37 +0659 Subject: [PATCH 37/49] Fix focus getting stuck by cycling within the same list item --- .../org/schabi/newpipe/views/SuperScrollLayoutManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java index 3946b8435..25864b51d 100644 --- a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -98,6 +98,12 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { continue; } + if (view == focusedItem) { + // do not pass focus back to the item View itself - it makes no sense + // (we can still pass focus to it's children however) + continue; + } + int candidate = getDistance(sourcePosition, view, listDirection); if (candidate < 0) { continue; From 9cb3cf250c6db2bded79ad04dbbe4268b8e5e351 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 12 Mar 2020 05:32:20 +0659 Subject: [PATCH 38/49] Intercept ActivityNotFoundException for ACTION_MANAGE_OVERLAY_PERMISSION --- .../main/java/org/schabi/newpipe/util/PermissionHelper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index f32bb6587..19dab6ef7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.util; import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -89,7 +90,10 @@ public class PermissionHelper { if (!Settings.canDrawOverlays(context)) { Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); + try { + context.startActivity(i); + } catch (ActivityNotFoundException ignored) { + } return false; } else return true; } From 512046e300c8ff1d8a336cb71b34134ed6c6c8ae Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 14 Mar 2020 13:22:02 +0659 Subject: [PATCH 39/49] Fix navigating to action bar buttons on API 28 Keyboard focus clusters prevent that from working, so we simply remove all focus clusters. While they are generally a good idea, focus clusters were created with Chrome OS and it's keyboard-driven interface in mind - there is no documented way to move focus between clusters using only IR remote. As such, there are no negative consequences to disabling them on Android TV. --- .../newpipe/views/FocusOverlayView.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java index b0b9cc421..582da38fb 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -27,6 +27,7 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -37,6 +38,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.appcompat.view.WindowCallbackWrapper; import org.schabi.newpipe.R; @@ -212,6 +214,8 @@ public final class FocusOverlayView extends Drawable implements ViewGroup decor = (ViewGroup) window.getDecorView(); decor.getOverlay().add(overlay); + fixFocusHierarchy(decor); + ViewTreeObserver observer = decor.getViewTreeObserver(); observer.addOnScrollChangedListener(overlay); observer.addOnGlobalFocusChangeListener(overlay); @@ -245,4 +249,42 @@ public final class FocusOverlayView extends Drawable implements animator.sendEmptyMessageDelayed(0, 100); } + + private static void fixFocusHierarchy(View decor) { + // During Android 8 development some dumb ass decided, that action bar has to be a keyboard focus cluster. + // Unfortunately, keyboard clusters do not work for primary auditory of key navigation — Android TV users + // (Android TV remotes do not have keyboard META key for moving between clusters). We have to fix this + // unfortunate accident. While we are at it, let's deal with touchscreenBlocksFocus too. + + if (Build.VERSION.SDK_INT < 26) { + return; + } + + if (!(decor instanceof ViewGroup)) { + return; + } + + clearFocusObstacles((ViewGroup) decor); + } + + @RequiresApi(api = 26) + private static void clearFocusObstacles(ViewGroup viewGroup) { + viewGroup.setTouchscreenBlocksFocus(false); + + if (viewGroup.isKeyboardNavigationCluster()) { + viewGroup.setKeyboardNavigationCluster(false); + + return; // clusters aren't supposed to nest + } + + int childCount = viewGroup.getChildCount(); + + for (int i = 0; i < childCount; ++i) { + View view = viewGroup.getChildAt(i); + + if (view instanceof ViewGroup) { + clearFocusObstacles((ViewGroup) view); + } + } + } } From 381b491845038b79f62d80c8e65d346fd9b5d1e7 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 15 Mar 2020 09:19:22 +0659 Subject: [PATCH 40/49] Prevent foocus from escaping open navigation drawer When contents of NewPipe navigation drawer change, NavigationMenuView (which is actually a RecyclerView) removes and re-adds all its adapter children, which leads to temporary loss of focus on currently focused drawer child. This situation was not anticipated by developers of original support library DrawerLayout: while NavigationMenuView itself is able to keep focus from escaping via onRequestFocusInDescendants(), the implementation of that method in DrawerLayout does not pass focus to previously focused View. In fact it does not pass focus correctly at all because the AOSP implementation of that method does not call addFocusables() and simply focuses the first available VISIBLE View, without regard to state of drawers. --- .../newpipe/views/FocusAwareDrawerLayout.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java index 2354427a3..45e4a8e34 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -19,6 +19,7 @@ package org.schabi.newpipe.views; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; @@ -44,6 +45,34 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { super(context, attrs, defStyle); } + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // SDK implementation of this method picks whatever visible View takes the focus first without regard to addFocusables + // if the open drawer is temporarily empty, the focus escapes outside of it, which can be confusing + + boolean hasOpenPanels = false; + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + + DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + + if (lp.gravity != 0 && isDrawerVisible(child)) { + hasOpenPanels = true; + + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + + if (hasOpenPanels) { + return false; + } + + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { boolean hasOpenPanels = false; From 3a611adc1136bbd14db53dca4723bb988a841988 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 15 Mar 2020 12:04:21 +0659 Subject: [PATCH 41/49] Prevent NavigationMenuView from gobbling up focus --- app/src/main/res/layout-v21/drawer_header.xml | 4 +--- app/src/main/res/layout/drawer_header.xml | 4 +--- app/src/main/res/layout/drawer_layout.xml | 9 +++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml index 9ed9f833a..7a7c6f155 100644 --- a/app/src/main/res/layout-v21/drawer_header.xml +++ b/app/src/main/res/layout-v21/drawer_header.xml @@ -1,9 +1,7 @@ + android:layout_height="150dp">