From eaa1179572e68dc264a54677314d39e18808e628 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:02:16 +0700 Subject: [PATCH 01/46] 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 4a2662f5384..ea2857b0307 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 @@ // 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 d6fd1dd0083..a3844a92f8c 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.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 @@ protected View getListFooter() { } 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 00000000000..33fe7b9ccb8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) Eltex ltd 2019 + * SuperScrollLayoutManager.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.graphics.Rect; +import android.view.FocusFinder; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public final class SuperScrollLayoutManager extends LinearLayoutManager { + private final Rect handy = new Rect(); + + public SuperScrollLayoutManager(Context context) { + super(context); + } + + @Override + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate, boolean focusedChildVisible) { + if (!parent.isInTouchMode()) { + // only activate when in directional navigation mode (Android TV etc) — fine grained + // touch scrolling is better served by nested scroll system + + if (!focusedChildVisible || getFocusedChild() == child) { + handy.set(rect); + + parent.offsetDescendantRectToMyCoords(child, handy); + + parent.requestRectangleOnScreen(handy, immediate); + } + } + + return super.requestChildRectangleOnScreen(parent, child, rect, immediate, focusedChildVisible); + } +} From 4806ac62ee62719998aa1361df8f3ce7156e9c06 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:04:13 +0700 Subject: [PATCH 02/46] 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 fdc7e6d6b01..bd5b2d5c7ea 100644 --- a/app/src/main/res/layout/toolbar_search_layout.xml +++ b/app/src/main/res/layout/toolbar_search_layout.xml @@ -19,6 +19,7 @@ android:drawablePadding="8dp" android:focusable="true" android:focusableInTouchMode="true" + android:nextFocusDown="@+id/suggestions_list" android:hint="@string/search" android:imeOptions="actionSearch|flagNoFullscreen" android:inputType="textFilter|textNoSuggestions" From 8952e2b0cdf362a9afb813f2c4ed20f998d474f5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:07:27 +0700 Subject: [PATCH 03/46] 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 c24d77d0328..3c18c25f6cd 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.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.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 @@ protected void onNewIntent(Intent intent) { public void onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); + if (FireTvUtils.isFireTv()) { + View drawerPanel = findViewById(R.id.navigation_layout); + if (drawer.isDrawerOpen(drawerPanel)) { + drawer.closeDrawers(); + return; + } + } + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); // If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it if (fragment instanceof BackPressable) { if (((BackPressable) fragment).onBackPressed()) return; } - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { finish(); } else super.onBackPressed(); From 2b39438eba2f4a78c288fd8a5121ce0672c544d0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:10:57 +0700 Subject: [PATCH 04/46] 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 a3844a92f8c..88684f2e716 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.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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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 414a9b6b59b..c1293e2402c 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 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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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 bff6c1b3a1a..ea820b71e4e 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.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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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/views/FixedGridLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java new file mode 100644 index 00000000000..a3ea5929b0f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) Eltex ltd 2019 + * 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 00000000000..76dee200fdc --- /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 26da47b1ff0..3792f030a8a 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.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 View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 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 01eeb085592..643d7d4f0c0 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/46] 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 b535db2b890..902e81f1fee 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -178,7 +178,6 @@ android:textSize="15sp" android:textStyle="bold" android:clickable="true" - android:focusable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -194,7 +193,6 @@ android:textColor="@android:color/white" android:textSize="12sp" android:clickable="true" - android:focusable="true" tools:text="The Video Artist LONG very LONG very Long"/> From 1bb96ef4058994fe2eabaa93cc5fdcf819acfa44 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:16:21 +0700 Subject: [PATCH 06/46] 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 00000000000..778e50e52ca --- /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 02b0a7b8646..186e184f3e6 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/46] 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 69666463e2f..879b54e1f69 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java @@ -1,10 +1,18 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; +import android.content.pm.PackageManager; + import org.schabi.newpipe.App; public class FireTvUtils { + @SuppressLint("InlinedApi") public static boolean isFireTv(){ final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; - return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); + + PackageManager pm = App.getApp().getPackageManager(); + + return pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV) + || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } } From 644ad110c06638c50b4bd3800d3a334b4f976c7d Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:25:30 +0700 Subject: [PATCH 08/46] 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 186e184f3e6..02d330ade11 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/46] 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 7a3e60c660e..5663e1ea2b3 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.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.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 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 @@ protected void onNewIntent(Intent intent) { } } + @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 void onClick(View v) { 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 void showControls(long duration) { 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 boolean onSingleTapConfirmed(MotionEvent e) { 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 360475ba204..0d9c1405871 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 boolean isSomePopupMenuVisible() { 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 void showControls(long duration) { animateView(controlsRoot, true, duration); } + public void safeHideControls(final long duration, long delay) { + if (DEBUG) Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); + if (rootView.isInTouchMode()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed( + () -> animateView(controlsRoot, false, duration), delay); + } + } + public void hideControls(final long duration, long delay) { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); controlsVisibilityHandler.removeCallbacksAndMessages(null); From d8bd8d87ec6eb6ca7379740be27d7d7545ae31d3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:42:32 +0700 Subject: [PATCH 10/46] 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 902e81f1fee..2859b6c5d0f 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -196,8 +196,9 @@ tools:text="The Video Artist LONG very LONG very Long"/> - - @@ -268,8 +272,9 @@ tools:ignore="RtlHardcoded" tools:visibility="visible"> - - From 7db1ba40ebf530181984c99804b1ab6d392a1742 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 16:48:34 +0700 Subject: [PATCH 11/46] 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 00000000000..0e8097dbead --- /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 92e73234f27..a965f5f6554 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + From a8a28294d3d43f9720141c561dc1e9fa3b10d421 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Sep 2019 17:42:56 +0700 Subject: [PATCH 12/46] 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 00000000000..3789ea344da --- /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 2859b6c5d0f..c40931a1a37 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/46] 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 0e8097dbead..2354427a346 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 void addFocusables(ArrayList views, int direction, int focusableMod content.addFocusables(views, direction, focusableMode); } } + + // this override isn't strictly necessary, but it is helpful when DrawerLayout isn't the topmost + // view in hierarchy (such as when system or builtin appcompat ActionBar is used) + @Override + @SuppressLint("RtlHardcoded") + public void openDrawer(@NonNull View drawerView, boolean animate) { + super.openDrawer(drawerView, animate); + + LayoutParams params = (LayoutParams) drawerView.getLayoutParams(); + + int gravity = GravityCompat.getAbsoluteGravity(params.gravity, ViewCompat.getLayoutDirection(this)); + + int direction = 0; + + switch (gravity) { + case Gravity.LEFT: + direction = FOCUS_LEFT; + break; + case Gravity.RIGHT: + direction = FOCUS_RIGHT; + break; + case Gravity.TOP: + direction = FOCUS_UP; + break; + case Gravity.BOTTOM: + direction = FOCUS_DOWN; + break; + } + + drawerView.requestFocus(direction); + } } From d23227d427831184f6ae12162593c6112bcd4b11 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 14:17:03 +0700 Subject: [PATCH 14/46] 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 3c18c25f6cd..8d2702d0b78 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.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 @@ protected void onCreate(Bundle savedInstanceState) { } 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 1be6e096a22..c5b97f86f55 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.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 @@ private void showDialog(final List choices) { 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 449a790e842..56265d321ef 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 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 void onGlobalLayout() { 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 5663e1ea2b3..38da4d8b2d1 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.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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { hideSystemUi(); setContentView(R.layout.activity_main_player); + playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); @@ -172,6 +174,10 @@ public void onChange(boolean selfChange) { 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 00000000000..b0b9cc4213e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -0,0 +1,248 @@ +/* + * Copyright 2019 Alexander Rvachev + * FocusOverlayView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.views; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.WindowCallbackWrapper; + +import org.schabi.newpipe.R; + +import java.lang.ref.WeakReference; + +public final class FocusOverlayView extends Drawable implements + ViewTreeObserver.OnGlobalFocusChangeListener, + ViewTreeObserver.OnDrawListener, + ViewTreeObserver.OnGlobalLayoutListener, + ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnTouchModeChangeListener { + + private boolean isInTouchMode; + + private final Rect focusRect = new Rect(); + + private final Paint rectPaint = new Paint(); + + private final Handler animator = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + updateRect(); + } + }; + + private WeakReference focused; + + public FocusOverlayView(Context context) { + rectPaint.setStyle(Paint.Style.STROKE); + rectPaint.setStrokeWidth(2); + rectPaint.setColor(context.getResources().getColor(R.color.white)); + } + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (newFocus != null && newFocus.getWidth() > 0 && newFocus.getHeight() > 0) { + newFocus.getGlobalVisibleRect(focusRect); + + focused = new WeakReference<>(newFocus); + } else { + focusRect.setEmpty(); + + focused = null; + } + + if (l != focusRect.left || r != focusRect.right || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + + focused = new WeakReference<>(newFocus); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + private void updateRect() { + if (focused == null) { + return; + } + + View focused = this.focused.get(); + + int l = focusRect.left; + int r = focusRect.right; + int t = focusRect.top; + int b = focusRect.bottom; + + if (focused != null) { + focused.getGlobalVisibleRect(focusRect); + } else { + focusRect.setEmpty(); + } + + if (l != focusRect.left || r != focusRect.right || t != focusRect.top || b != focusRect.bottom) { + invalidateSelf(); + } + } + + @Override + public void onDraw() { + updateRect(); + } + + @Override + public void onScrollChanged() { + updateRect(); + + animator.removeMessages(0); + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onGlobalLayout() { + updateRect(); + + animator.sendEmptyMessageDelayed(0, 1000); + } + + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + this.isInTouchMode = isInTouchMode; + + if (isInTouchMode) { + updateRect(); + } else { + invalidateSelf(); + } + } + + public void setCurrentFocus(View focused) { + if (focused == null) { + return; + } + + this.isInTouchMode = focused.isInTouchMode(); + + onGlobalFocusChanged(null, focused); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (!isInTouchMode && focusRect.width() != 0) { + canvas.drawRect(focusRect, rectPaint); + } + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSPARENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + } + + public static void setupFocusObserver(Dialog dialog) { + Rect displayRect = new Rect(); + + Window window = dialog.getWindow(); + assert window != null; + + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(dialog.getContext()); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + public static void setupFocusObserver(Activity activity) { + Rect displayRect = new Rect(); + + Window window = activity.getWindow(); + View decor = window.getDecorView(); + decor.getWindowVisibleDisplayFrame(displayRect); + + FocusOverlayView overlay = new FocusOverlayView(activity); + overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); + + setupOverlay(window, overlay); + } + + private static void setupOverlay(Window window, FocusOverlayView overlay) { + ViewGroup decor = (ViewGroup) window.getDecorView(); + decor.getOverlay().add(overlay); + + ViewTreeObserver observer = decor.getViewTreeObserver(); + observer.addOnScrollChangedListener(overlay); + observer.addOnGlobalFocusChangeListener(overlay); + observer.addOnGlobalLayoutListener(overlay); + observer.addOnTouchModeChangeListener(overlay); + + overlay.setCurrentFocus(decor.getFocusedChild()); + + // Some key presses don't actually move focus, but still result in movement on screen. + // For example, MovementMethod of TextView may cause requestRectangleOnScreen() due to + // some "focusable" spans, which in turn causes CoordinatorLayout to "scroll" it's children. + // Unfortunately many such forms of "scrolling" do not count as scrolling for purpose + // of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly + // receiving keys from Window. + window.setCallback(new WindowCallbackWrapper(window.getCallback()) { + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + boolean res = super.dispatchKeyEvent(event); + overlay.onKey(event); + return res; + } + }); + } + + private void onKey(KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + + updateRect(); + + animator.sendEmptyMessageDelayed(0, 100); + } +} From 28fb864ed01bf0af2989527d984e500f91717fe1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 17:20:15 +0700 Subject: [PATCH 15/46] 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 14e98962533..fd2a3285d3f 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 @@ protected void initViews(View rootView, Bundle savedInstanceState) { setHeightThumbnail(); - + thumbnailBackgroundButton.requestFocus(); } @Override From 79c962fc8805183fdbe44b5dfe0ab7b2e32cd141 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 30 Sep 2019 12:02:07 +0700 Subject: [PATCH 16/46] 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 33fe7b9ccb8..3946b843587 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 @@ 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 boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonN 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 0ee62c05d3d..4ced11d3539 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/46] 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 4d94ec3927f..e7b09f3e260 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 void updateFromItem(final InfoItem infoItem, final HistoryRecordManager h }); } + 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 @@ private void expand() { itemContentView.setMaxLines(commentExpandedLines); itemContentView.setText(commentText); linkify(); + allowLinkFocus(); } private void linkify(){ Linkify.addLinks(itemContentView, Linkify.WEB_URLS); Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); + itemContentView.setMovementMethod(null); } } From 6e76610f303af3428f5ec0afc565ce98cc4bb7ed Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 23 Sep 2019 17:57:14 +0700 Subject: [PATCH 18/46] 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 a07afcea94d..50a60ecb1c3 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 void triggerProgressUpdate() { } 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 85c852f5716..bbe391807f5 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 @@ private synchronized void maybeSynchronizePlayer() { //////////////////////////////////////////////////////////////////////////*/ private Observable getEdgeIntervalSignal() { - return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS) + return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .filter(ignored -> playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis)); } From a7c31e6bcc28067573aebd8a198091cdc053a7f4 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Fri, 8 Nov 2019 14:26:12 +0700 Subject: [PATCH 19/46] 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 | 171 ++++++++++++++++-- 1 file changed, 159 insertions(+), 12 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 76dee200fdc..435281d144f 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 (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; } - if (direction == View.FOCUS_UP) { - if (canScrollVertically(-1)) { - scrollBy(0, -10); + 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; } - return false; + parent = (ViewGroup) parent.getParent(); } - return super.dispatchUnhandledMove(focused, direction); + return false; + } + + private boolean arrowScroll(int direction) { + switch (direction) { + case FOCUS_DOWN: + if (!canScrollVertically(1)) { + return false; + } + scrollBy(0, 100); + break; + case FOCUS_UP: + if (!canScrollVertically(-1)) { + return false; + } + scrollBy(0, -100); + break; + case FOCUS_LEFT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(-100, 0); + break; + case FOCUS_RIGHT: + if (!canScrollHorizontally(-1)) { + return false; + } + scrollBy(100, 0); + break; + default: + return false; + } + + return true; + } + + private boolean isOutside(View view) { + return findContainingItemView(view) == null; } } From b5558a8b78962234d855f318e1b059bd88892242 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Fri, 8 Nov 2019 14:41:16 +0700 Subject: [PATCH 20/46] 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 88684f2e716..a3844a92f8c 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.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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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 c1293e2402c..414a9b6b59b 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 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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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 ea820b71e4e..bff6c1b3a1a 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.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 @@ protected RecyclerView.LayoutManager getGridLayoutManager() { 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/views/FixedGridLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java deleted file mode 100644 index a3ea5929b0f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/views/FixedGridLayoutManager.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) Eltex ltd 2019 - * 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 3792f030a8a..26da47b1ff0 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.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 View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, mList = v.findViewById(R.id.mission_recycler); // Init layouts managers - mGridManager = new FixedGridLayoutManager(getActivity(), SPAN_SIZE); + mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { From 9801cf50e38abd0214340d48dba1e5b8e1572cef Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:34:31 +0659 Subject: [PATCH 21/46] 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 a3844a92f8c..a2821a65e1f 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.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 String generateSuffix() { 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 void writeTo(Queue objectsToSave) { 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 @@ protected void onRestoreInstanceState(@NonNull Bundle bundle) { savedState = StateSaver.tryToRestore(bundle, this); } + @Override + public void onStop() { + focusedPosition = getFocusedPosition(); + super.onStop(); + } + + @Override + public void onStart() { + super.onStart(); + restoreFocus(focusedPosition); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ From 7bb5cacb0dff6f167046f612af5caf3f603f69da Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:37:16 +0659 Subject: [PATCH 22/46] 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 fd2a3285d3f..c698d4ad4cf 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.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 @@ private void toggleTitleAndDescription() { 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 @@ protected void initViews(View rootView, Bundle savedInstanceState) { 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 00000000000..1f9ab5e2d6c --- /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 02d330ade11..6d54525db74 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/46] 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 6d54525db74..e1a680e5da8 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/46] 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 e7b09f3e260..1987660698f 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 void updateFromItem(final InfoItem infoItem, final HistoryRecordManager h } 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 @@ private void ellipsize() { linkify(); - if (!hasEllipsis) { - allowLinkFocus(); + if (hasEllipsis) { + denyLinkFocus(); + } else { + determineLinkFocus(); } } @@ -168,13 +184,11 @@ private void expand() { itemContentView.setMaxLines(commentExpandedLines); itemContentView.setText(commentText); linkify(); - allowLinkFocus(); + determineLinkFocus(); } private void linkify(){ Linkify.addLinks(itemContentView, Linkify.WEB_URLS); Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); - - itemContentView.setMovementMethod(null); } } From 7d75950624f56512a713a2b1ba4cbdc700d89673 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 20:54:40 +0659 Subject: [PATCH 25/46] 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 9a8e1fd17e1..7363d221c67 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 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 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 @@ protected void loadMoreItems() { 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 @@ protected void loadMoreItems() { }); } + private void forbidDownwardFocusScroll() { + if (itemsList instanceof NewPipeRecyclerView) { + ((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(false); + } + } + + private void allowDownwardFocusScroll() { + if (itemsList instanceof NewPipeRecyclerView) { + ((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(true); + } + } + @Override public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); From 436c75ca6c081b34388a835924b1270277d50ffe Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 14 Nov 2019 22:43:54 +0659 Subject: [PATCH 26/46] 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 393d7d1b4d8..41606201f64 100644 --- a/app/src/main/res/layout/list_comments_item.xml +++ b/app/src/main/res/layout/list_comments_item.xml @@ -18,6 +18,7 @@ android:layout_alignParentTop="true" android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:contentDescription="@string/list_thumbnail_view_description" + android:focusable="false" android:src="@drawable/buddy" tools:ignore="RtlHardcoded" /> From a1e02f770434237b0be4bf0bf8a938a33018b509 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 16 Nov 2019 13:02:46 +0659 Subject: [PATCH 27/46] 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 38da4d8b2d1..0650e2a260a 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 void onChange(boolean selfChange) { 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 @@ protected void onResume() { if (globalScreenOrientationLocked()) { boolean lastOrientationWasLandscape = defaultPreferences.getBoolean( - getString(R.string.last_orientation_landscape_key), false); + getString(R.string.last_orientation_landscape_key), FireTvUtils.isFireTv()); setLandscape(lastOrientationWasLandscape); } From c0fb96a911c7c0464e26b78597e58aa96b19bde9 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 16 Nov 2019 13:05:59 +0659 Subject: [PATCH 28/46] 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 879b54e1f69..2c50903813f 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 @@ 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 static boolean isFireTv(){ 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 3789ea344da..dafd5ae6fff 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.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 void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isInTouchMode() && keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + if (!isInTouchMode() && FireTvUtils.isConfirmKey(keyCode)) { releaseTrack(); } From dc7ae3917e97889def2f7315e7ca4d947dbdd847 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 17 Nov 2019 16:53:11 +0659 Subject: [PATCH 29/46] 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 9052dababc6..3583d0312d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + diff --git a/app/src/main/res/mipmap-xhdpi/newpipe_tv_banner.png b/app/src/main/res/mipmap-xhdpi/newpipe_tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..4be6644504b178aa612b218202626b4b5d9e2a26 GIT binary patch literal 2138 zcmdT`i#OZZ7XC#QnU2k3^SOFRAEAiK4{2qg9Wpsz+;r^d`u7M|GxkJDS|2 zC{fXiW|)dfYcdk*cG{sSDOIH&p*5i-5d=Xb++WxH6LZg6`@Fuj&feeNXPxhqhKKs= zeqr(j006p}0N-x`0D=bRPjs}vepa-u4-T4ylbAC)V9VD@x(TkKR|BF60I;X+vq9EN z7BS$aai(8XW&|!JlbC={28cwWQ)&j5keF~a*$IcgOqZcd06=>bt2* zMeoX}pV}{@+PGul%LFUyK0z?8J`I&jq%`8(>>UF-6Jtb$h7z@Ke#%%y_cwc7=)(@U#6inNelwd%a{vI+na zOUH810is!LOF}=^)VopvXb23@b0!Z8RI5}2FI_0(U3LXvpaw*zYPU8-E+#>bi^Z_%xW=$$Ll_em== zq9!KQXa9l{F^JWqcyUHA@XOD^`YPc_;T>rgaTI<{tVuER*!+;x^OLCL0Cn}XM2QG5 z2iSyAuM<^B=N!eDXuh83se9%yN!nRkE0Xuo2E4)^goY@_m=eQhURES2_r4E}Ckw1_ zz`%m%m54_ZniL`OlCMi%whJ~AFKvrsXk}Lc@_{%!M!vx0uPw0NwfXC##fMyMs$3dX zX+7x%3O$X0IDhj6H)!6MkOtyUW^{*`J1E8737fT8jesR%gk8s+2?sOJ(WFdq7Tx&Q z_VPPEWfz!iIzt+kYW{gju9!&X|GxYYbfro09C;g2;qWl(bOuc&iH~F4{1Iv?+*GGi zZ#jvT2t*(-b}YW=o@&{rD+gXf=~Ut6s#X3!dWY%zJiI~dmQm#uAu*nv;M3Jn8CQ|7 zh{i8<@praILEI8#+m5Aq$n3l_3unu72^&IWhg!i-PZ?^+C46+&vE<>lmyk*YhVtCf1I>#%RcUk2v}ppmq+gg# zPa0#(TiPaUq%cUshI%t`Ov^PTST5tO&+RvjEJSYd;rz{&^|>LiC`Qghx0eO50f$zg3t+C(%ON? zDdpiI^a_;{+tp!o;coug>8*b&inT{T81!i&8LjM{(YK~D87w4m`NJXy)5)ey>!X#7 zv%duX+(Qm~y7`}}$>#^0gAUFKf|QyPvXi*EEVo+UT9x$R7RTAmlHrIy$h#j6Erq5i z_YTFlNXC;CM)0z*E`$`kq=mmR{W*S+ICkJ~&!AQn+o)5k`(gz3Xh{rAEk?pY!h#VP3oEaZY=x zfgz$(*AtX%Wa_;OF=OjKL7+Q~cxHXH3pc@SHi~~}+WqdAzVBMbB^#+fhLP546P~}? zF2Qv)%0}jNO{xACArFF^whw+&ZA}{Hq$jJ@8}J+EF+_!QY__*QzThm;DFnP@;+EL2 z?IZ8ti1E2ivh&q2(y(h%0Q;i1Fx_4%V)Nn1}EMnwpr*3{}cND2vDH#2(5m z#uYCVdG5F$-&M&U8?V$GK`qt2VP56Ct7Yc*I@cpdnfD>)#S2pvj(pdFs2DWE5Y27B znh+hcyt5t0xUQ9>`M`9#kRDuHQ$REYW-$bGK=NYK6tr$wa= z<%RXpf`oU?>?ao|-B;@4SD=)s$AtyaMaK%bjeFOEz$fGG2_8zuIzP6K`QQwm%yRAy zh6USk++UCjU&GPjtwoN!kX&ZU^Mp#jf;7}R#=f$U9!6>G`AZ^RzMkmpa6tV4`F$4{ zJHF2dHX6_9XH2hS>P#5GSlhI-qn?-6AqKETpu5=~@JdUCfYp)vR~#JFA^$gxliS|7 z7tweb%WjwW47M3vuyeMkcKKsSquPpO<8uSntr}WCQaA8ffp+)&U+U9eYN|xG(W$3i T=$3)s006-Fh5E8iUcB}{BjNT* literal 0 HcmV?d00001 From 106e538d0824d23604ce83c8bf9ad0916dc65ee5 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 17 Nov 2019 16:54:18 +0659 Subject: [PATCH 30/46] 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 3583d0312d9..3284202fd47 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/46] 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 e1a680e5da8..684adc2220f 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -23,6 +23,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" + android:touchscreenBlocksFocus="false" android:fitsSystemWindows="true" app:elevation="0dp" app:layout_behavior="com.google.android.material.appbar.FlingBehavior"> From 29136d633a9f54d774e5dbc39b85c73d6b2b06ae Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 1 Dec 2019 12:33:34 +0659 Subject: [PATCH 32/46] 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 ce22b84e97d..72d72082444 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 void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { - startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); + try { + startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.general_error, Toast.LENGTH_SHORT).show(); + } } return super.onPreferenceTreeClick(preference); From 3f51114129c813ac83bf658f88934f968990faa9 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 1 Dec 2019 12:38:01 +0659 Subject: [PATCH 33/46] 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 a3f218074dc..e53b7ba0700 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.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 @@ protected void onCreate(Bundle savedInstanceBundle) { .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 ce17e0382be..82c9dd08134 100644 --- a/app/src/main/res/layout/list_choose_tabs.xml +++ b/app/src/main/res/layout/list_choose_tabs.xml @@ -12,6 +12,7 @@ android:layout_marginTop="3dp" android:minHeight="?listPreferredItemHeightSmall" android:orientation="horizontal" + android:focusable="true" app:cardCornerRadius="5dp" app:cardElevation="4dp"> From 8c9015b57bd489a192e69f4a8977c745810403b2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Tue, 10 Dec 2019 21:21:35 +0659 Subject: [PATCH 34/46] 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 ea2857b0307..3af2c95bce1 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 boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordi return false; } - int offset = getTopAndBottomOffset(); - int dy; if (focusScrollRect.bottom > height) { @@ -49,8 +46,6 @@ public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordi return false; } - //int newOffset = offset + dy; - int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); return consumed == dy; From caa1de8aff77b92255d68113ade22afe7d17e8ed Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Wed, 29 Jan 2020 03:15:50 +0659 Subject: [PATCH 35/46] 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 a2f16184703..d2cbb49e0f1 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.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 @@ protected void onCreate(Bundle savedInstanceState) { ErrorActivity.reportUiError(this, e); } - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } @@ -489,7 +489,7 @@ protected void onNewIntent(Intent intent) { 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 c5b97f86f55..412bea0e171 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.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 @@ private void showDialog(final List choices) { 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 6ceacbb0562..514c3dd379f 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.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 void onGlobalLayout() { } }); - 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 f2e8aa244e6..9e4fd467cb0 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.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 @@ private void initSearchListeners() { 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 fa742f7712b..470e1c9630b 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.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 void onChange(boolean selfChange) { 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 void onChange(boolean selfChange) { Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); - if (FireTvUtils.isFireTv()) { + if (AndroidTvUtils.isTv()) { FocusOverlayView.setupFocusObserver(this); } } @@ -206,7 +206,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { 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 @@ protected void onResume() { 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 e53b7ba0700..49d6d49febe 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.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 @@ protected void onCreate(Bundle savedInstanceBundle) { .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 2c50903813f..203501a517a 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.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 dafd5ae6fff..8ccff85d532 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.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 void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!isInTouchMode() && FireTvUtils.isConfirmKey(keyCode)) { + if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) { releaseTrack(); } From 6a3a72eb065bdf9419e58f01e90e4bf4a576d6a2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Wed, 26 Feb 2020 06:40:46 +0659 Subject: [PATCH 36/46] 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 435281d144f..41b823db87d 100644 --- a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -36,7 +36,7 @@ public class NewPipeRecyclerView extends RecyclerView { private Rect focusRect = new Rect(); private Rect tempFocus = new Rect(); - private boolean allowDpadScroll; + private boolean allowDpadScroll = true; public NewPipeRecyclerView(@NonNull Context context) { super(context); From 1cc5a67d82a555424fa3d22ff744434369060ea2 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 12 Mar 2020 05:29:37 +0659 Subject: [PATCH 37/46] 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 3946b843587..25864b51d58 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 View onInterceptFocusSearch(@NonNull View focused, int direction) { continue; } + if (view == focusedItem) { + // do not pass focus back to the item View itself - it makes no sense + // (we can still pass focus to it's children however) + continue; + } + int candidate = getDistance(sourcePosition, view, listDirection); if (candidate < 0) { continue; From 9cb3cf250c6db2bded79ad04dbbe4268b8e5e351 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Thu, 12 Mar 2020 05:32:20 +0659 Subject: [PATCH 38/46] 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 f32bb6587d0..19dab6ef741 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 @@ 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 static boolean checkSystemAlertWindowPermission(Context context) { if (!Settings.canDrawOverlays(context)) { Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); + try { + context.startActivity(i); + } catch (ActivityNotFoundException ignored) { + } return false; } else return true; } From 512046e300c8ff1d8a336cb71b34134ed6c6c8ae Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sat, 14 Mar 2020 13:22:02 +0659 Subject: [PATCH 39/46] 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 b0b9cc4213e..582da38fb9b 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.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.Window; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.appcompat.view.WindowCallbackWrapper; import org.schabi.newpipe.R; @@ -212,6 +214,8 @@ private static void setupOverlay(Window window, FocusOverlayView overlay) { 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 @@ private void onKey(KeyEvent event) { animator.sendEmptyMessageDelayed(0, 100); } + + private static void fixFocusHierarchy(View decor) { + // During Android 8 development some dumb ass decided, that action bar has to be a keyboard focus cluster. + // Unfortunately, keyboard clusters do not work for primary auditory of key navigation — Android TV users + // (Android TV remotes do not have keyboard META key for moving between clusters). We have to fix this + // unfortunate accident. While we are at it, let's deal with touchscreenBlocksFocus too. + + if (Build.VERSION.SDK_INT < 26) { + return; + } + + if (!(decor instanceof ViewGroup)) { + return; + } + + clearFocusObstacles((ViewGroup) decor); + } + + @RequiresApi(api = 26) + private static void clearFocusObstacles(ViewGroup viewGroup) { + viewGroup.setTouchscreenBlocksFocus(false); + + if (viewGroup.isKeyboardNavigationCluster()) { + viewGroup.setKeyboardNavigationCluster(false); + + return; // clusters aren't supposed to nest + } + + int childCount = viewGroup.getChildCount(); + + for (int i = 0; i < childCount; ++i) { + View view = viewGroup.getChildAt(i); + + if (view instanceof ViewGroup) { + clearFocusObstacles((ViewGroup) view); + } + } + } } From 381b491845038b79f62d80c8e65d346fd9b5d1e7 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 15 Mar 2020 09:19:22 +0659 Subject: [PATCH 40/46] 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 2354427a346..45e4a8e34af 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 @@ 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 FocusAwareDrawerLayout(@NonNull Context context, @Nullable AttributeSet a super(context, attrs, defStyle); } + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // SDK implementation of this method picks whatever visible View takes the focus first without regard to addFocusables + // if the open drawer is temporarily empty, the focus escapes outside of it, which can be confusing + + boolean hasOpenPanels = false; + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + + DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + + if (lp.gravity != 0 && isDrawerVisible(child)) { + hasOpenPanels = true; + + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + + if (hasOpenPanels) { + return false; + } + + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { boolean hasOpenPanels = false; From 3a611adc1136bbd14db53dca4723bb988a841988 Mon Sep 17 00:00:00 2001 From: Alexander-- Date: Sun, 15 Mar 2020 12:04:21 +0659 Subject: [PATCH 41/46] 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 9ed9f833a7d..7a7c6f155e1 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">