From 0d0b3b888faba9cd501bc83d793f62617473076e 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 beca712a956f3dfeba343f2d249d544c04b88b6c 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 9429fbfa65b6d05fbf2bee85f3e43165234c6162 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 b1aa3b018b0f9010b709cd19c232a2a7d2143ba9 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 2b8bd2c8901b1e09032fed8d21e3b5a66358a5d3 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 bf21a7a1a35b0385bd35f13fb37508c39ea6a729 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 02c945bddd2c10a8618aeef78724bea30fd96804 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 9329c2f7f1b5b4f58e26a09b37b687dbab1ced9e 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 ddc609e8d8d5257b89d81db4454eefae737fe953 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 924fb49eb0f24b22a708867c84f59d5f44357ab9 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 d2935d616c548605a3390b92e8996a50aac63f1b 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 2f30802d292ac2209ef9a06b810065440ca082d0 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 f831c84c420aa15f7fcd4748acc118caa90e1316 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 10e38f7ea496a1aa5b3e65f3d338703d697efd3c 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 ac28cc73640f9b71125d5dde8c5ce7654d6d2661 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 8018f6d37f5b2affc197ca1e8afdbb870987d08a 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 86fc9c078533dab7d26f92e5c02e63d9ff0814e5 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 31bd60f3b1304d4f3b11a25843962d0d0bf8d496 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 330eb896d85ed673e3c4d71800ae171d9b674eae 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 8d98f9a96746f6c7ede65bd057b702b3b1840dbc 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 72d23158c3426f3d503592e35f3a30b2a003d622 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 430381df4ead587a6febc8a0c65346f3a0b5bdff 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 37cf665aa85ecfffdd497352cd032dff57678344 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 bc4ee8b7ff874c78bf0735ef78f9cc8a689da060 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 68f4b5c8e5739657e27dbf5e54da12c1e65fc3c7 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 9c306afec6ff7c07e457fbd4ebe392bf2b17a047 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 909d1bbccca4afa7972f7cf6db3e19d7e7008014 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 5559898332267b5a11e5549ba54e095845a7ce8d 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 06e987dafd6d7add31f8f537ce838217d33ebce4 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 4ea4b5436dbb319e1185e3e72cf8180aef1d4412 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 d37706f814ddbfd2274b7f0aa379240642ed8898 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 5c86e436a3b14b77bf92149ec46130fece858643 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 e6b38ec80216b4baedb434f4fcc31cc4306174e3 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">