From ddc7e90c482646c8f59cb454ba41f0503e4a741c Mon Sep 17 00:00:00 2001 From: Andrey Pavlenko Date: Sun, 27 Aug 2023 23:19:02 +0200 Subject: [PATCH] Implemented mouse cursor emulator for D-pad navigation --- build.gradle | 12 +- depends/utils | 2 +- .../me/aap/fermata/auto/MainCarActivity.java | 349 +++++++++++++++++- fermata/src/auto/res/drawable/cursor.xml | 10 + .../main/java/me/aap/fermata/action/Key.java | 2 +- .../aap/fermata/action/KeyEventHandler.java | 10 + .../ui/activity/MainActivityDelegate.java | 5 + .../ui/activity/MainActivityPrefs.java | 5 + .../fermata/ui/fragment/SettingsFragment.java | 7 + .../fermata/ui/view/MediaItemListView.java | 7 + .../me/aap/fermata/ui/view/MediaItemView.java | 74 ++-- fermata/src/main/res/values-ru/strings.xml | 1 + fermata/src/main/res/values/ids.xml | 2 + fermata/src/main/res/values/strings.xml | 1 + .../addon/web/FermataChromeClient.java | 2 + .../fermata/addon/web/yt/YoutubeWebView.java | 8 + 16 files changed, 446 insertions(+), 51 deletions(-) create mode 100644 fermata/src/auto/res/drawable/cursor.xml diff --git a/build.gradle b/build.gradle index 1aa2a2e4..232512a3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ ext { def abi = project.properties['ABI'] - VERSION_CODE = 226 - VERSION_NAME = "1.9.3" + VERSION_CODE = 227 + VERSION_NAME = "1.9.4" SDK_MIN_VERSION = 23 - SDK_TARGET_VERSION = 33 - SDK_COMPILE_VERSION = 33 + SDK_TARGET_VERSION = 34 + SDK_COMPILE_VERSION = 34 BUILD_TOOLS_VERSION = "33.0.1" ABI_FILTERS = (abi != null) ? abi.split(",") : ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] localProps = gradle.ext.localProps @@ -27,7 +27,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath 'com.google.gms:google-services:4.3.15' } } @@ -134,6 +134,6 @@ gradle.projectsEvaluated { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/depends/utils b/depends/utils index 55227c4b..eb5addbf 160000 --- a/depends/utils +++ b/depends/utils @@ -1 +1 @@ -Subproject commit 55227c4beefae30934bf692192d748aae8b78872 +Subproject commit eb5addbff7841094102a947c794e4cf6c08de072 diff --git a/fermata/src/auto/java/me/aap/fermata/auto/MainCarActivity.java b/fermata/src/auto/java/me/aap/fermata/auto/MainCarActivity.java index af6a14c1..2f70587c 100644 --- a/fermata/src/auto/java/me/aap/fermata/auto/MainCarActivity.java +++ b/fermata/src/auto/java/me/aap/fermata/auto/MainCarActivity.java @@ -1,5 +1,16 @@ package me.aap.fermata.auto; +import static android.view.KeyEvent.KEYCODE_BACK; +import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP; +import static android.view.KeyEvent.KEYCODE_DPAD_UP_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP_RIGHT; +import static android.view.View.VISIBLE; import static me.aap.utils.async.Completed.completed; import static me.aap.utils.async.Completed.failed; import static me.aap.utils.ui.UiUtils.showAlert; @@ -7,28 +18,48 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.os.Bundle; import android.os.OperationCanceledException; +import android.os.SystemClock; import android.text.TextWatcher; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.Window; +import android.webkit.WebView; import android.widget.EditText; import android.widget.TextView.OnEditorActionListener; import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.google.android.apps.auto.sdk.CarActivity; import com.google.android.apps.auto.sdk.CarUiController; +import me.aap.fermata.R; import me.aap.fermata.media.service.FermataMediaServiceConnection; import me.aap.fermata.ui.activity.FermataActivity; import me.aap.fermata.ui.activity.MainActivityDelegate; +import me.aap.fermata.ui.view.MediaItemListView; +import me.aap.fermata.ui.view.VideoView; import me.aap.utils.async.FutureSupplier; +import me.aap.utils.function.Cancellable; import me.aap.utils.function.Supplier; import me.aap.utils.log.Log; import me.aap.utils.ui.activity.ActivityDelegate; +import me.aap.utils.ui.fragment.ActivityFragment; +import me.aap.utils.ui.menu.OverlayMenu; /** * @author Andrey Pavlenko @@ -37,7 +68,8 @@ public class MainCarActivity extends CarActivity implements FermataActivity { static FermataMediaServiceConnection service; @SuppressWarnings("unchecked") @NonNull - private FutureSupplier delegate = (FutureSupplier) NO_DELEGATE; + private FutureSupplier delegate = + (FutureSupplier) NO_DELEGATE; private CarEditText editText; private TextWatcher textWatcher; @@ -67,8 +99,7 @@ public void onCreate(Bundle savedInstanceState) { onCreate(savedInstanceState, s); } else { delegate = FermataMediaServiceConnection.connect(this, true).main() - .onFailure(err -> showAlert(getContext(), String.valueOf(err))) - .map(c -> { + .onFailure(err -> showAlert(getContext(), String.valueOf(err))).map(c -> { service = c; return onCreate(savedInstanceState, c); }); @@ -93,8 +124,7 @@ public void onResume() { @SuppressWarnings("unchecked") public void onDestroy() { super.onDestroy(); - getActivityDelegate() - .onSuccess(MainActivityDelegate::onActivityDestroy) + getActivityDelegate().onSuccess(MainActivityDelegate::onActivityDestroy) .thenRun(() -> ActivityDelegate.setContextToDelegate(null)); delegate = (FutureSupplier) NO_DELEGATE; } @@ -214,21 +244,318 @@ public boolean setTextInput(String text) { @Override public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { MainActivityDelegate d = delegate.peek(); - return (d != null) ? d.onKeyUp(keyCode, keyEvent, super::onKeyUp) - : super.onKeyUp(keyCode, keyEvent); + if (d == null) return super.onKeyUp(keyCode, keyEvent); + + if (d.getPrefs().useDpadCursor(d)) { + switch (keyCode) { + case KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_LEFT, + KEYCODE_DPAD_UP_RIGHT, KEYCODE_DPAD_DOWN_LEFT, KEYCODE_DPAD_DOWN_RIGHT -> { + return true; + } + case KEYCODE_BACK -> { + if (findViewById(R.id.cursor) != null) return true; + } + case KEYCODE_DPAD_CENTER -> { + Cursor c = (Cursor) findViewById(R.id.cursor); + if ((c != null) && c.isFocused()) return c.click(); + } + } + } + + return d.onKeyDown(keyCode, keyEvent, super::onKeyDown); } @Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { MainActivityDelegate d = delegate.peek(); - return (d != null) ? d.onKeyDown(keyCode, keyEvent, super::onKeyDown) - : super.onKeyDown(keyCode, keyEvent); + if (d == null) return super.onKeyDown(keyCode, keyEvent); + if (!d.getPrefs().useDpadCursor(d)) return d.onKeyDown(keyCode, keyEvent, super::onKeyDown); + + float x = 0; + float y = 0; + View screen = null; + Cursor cursor = null; + + switch (keyCode) { + case KEYCODE_DPAD_UP -> { + OverlayMenu m = d.getActiveMenu(); + if (m instanceof View v) { + View f = v.focusSearch(View.FOCUS_UP); + if (f != null) { + f.requestFocus(); + return true; + } + } + y = -1; + } + case KEYCODE_DPAD_DOWN -> { + OverlayMenu m = d.getActiveMenu(); + if (m instanceof View v) { + View f = v.focusSearch(View.FOCUS_DOWN); + if (f != null) { + f.requestFocus(); + return true; + } + } + y = 1; + } + case KEYCODE_DPAD_LEFT -> x = -1; + case KEYCODE_DPAD_RIGHT -> x = 1; + case KEYCODE_DPAD_UP_LEFT -> { + y = -1; + x = -1; + } + case KEYCODE_DPAD_UP_RIGHT -> { + y = -1; + x = 1; + } + case KEYCODE_DPAD_DOWN_LEFT -> { + y = 1; + x = -1; + } + case KEYCODE_DPAD_DOWN_RIGHT -> { + y = 1; + x = 1; + } + case KEYCODE_BACK -> { + screen = findViewById(R.id.main_activity); + cursor = screen.findViewById(R.id.cursor); + if ((cursor == null) || cursor.isFocused()) + return d.onKeyDown(keyCode, keyEvent, super::onKeyDown); + } + case KEYCODE_DPAD_CENTER -> { + screen = findViewById(R.id.main_activity); + cursor = screen.findViewById(R.id.cursor); + if (cursor == null) return d.onKeyDown(keyCode, keyEvent, super::onKeyDown); + } + default -> {return d.onKeyDown(keyCode, keyEvent, super::onKeyDown);} + } + + if (screen == null) { + screen = findViewById(R.id.main_activity); + cursor = screen.findViewById(R.id.cursor); + } + + int w = screen.getWidth(); + int h = screen.getHeight(); + int cursorSize = (int) (Math.min(w, h) * 0.05f); + + if (cursor == null) { + cursor = new Cursor(d, (int) (Math.min(w, h) * 0.05f)); + ((ViewGroup) screen).addView(cursor); + cursor.move(w / 2f, h / 2f, keyCode); + } else if (!cursor.isVisible()) { + cursor.setVisibility(VISIBLE); + cursor.move(cursor.getX(), cursor.getY(), keyCode); + } else { + float step = cursorSize / 3f; + float cursorX = cursor.getX() + (step * x * cursor.accel); + float cursorY = cursor.getY() + (step * y * cursor.accel); + cursorX = Math.max(0, Math.min(w - step, cursorX)); + cursorY = Math.max(0, Math.min(h - step, cursorY)); + cursor.move(cursorX, cursorY, keyCode); + } + + return true; } @Override public boolean onKeyLongPress(int keyCode, KeyEvent keyEvent) { MainActivityDelegate d = delegate.peek(); - return (d != null) ? d.onKeyLongPress(keyCode, keyEvent, super::onKeyLongPress) - : super.onKeyLongPress(keyCode, keyEvent); + return (d != null) ? d.onKeyLongPress(keyCode, keyEvent, super::onKeyLongPress) : + super.onKeyLongPress(keyCode, keyEvent); + } + + private static final class Cursor extends AppCompatImageView + implements View.OnClickListener, View.OnLongClickListener { + private final MainActivityDelegate activity; + private Cancellable hide = Cancellable.CANCELED; + private Cancellable resetAccel = Cancellable.CANCELED; + int accel = 1; + + Cursor(MainActivityDelegate d, int size) { + super(d.getContext()); + activity = d; + setId(R.id.cursor); + setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + setLayoutParams(new ConstraintLayout.LayoutParams(size, size)); + setImageDrawable(AppCompatResources.getDrawable(d.getContext(), R.drawable.cursor)); + setOnClickListener(this); + setOnLongClickListener(this); + + Drawable transparent = new ColorDrawable(Color.TRANSPARENT); + StateListDrawable bg = new StateListDrawable(); + bg.addState(new int[]{android.R.attr.state_focused}, transparent); + bg.addState(new int[]{}, transparent); + setBackground(bg); + } + + boolean isVisible() { + return getVisibility() == VISIBLE; + } + + @Override + public void onClick(View v) { + click(); + } + + boolean click() { + delayedHide(); + float x = getX(); + float y = getY(); + OverlayMenu m = activity.getActiveMenu(); + if ((m instanceof ViewGroup v) && click(v, x - v.getX(), y - v.getY())) return true; + click(screen(), x, y); + return true; + } + + private boolean click(ViewGroup parent, float cursorX, float cursorY) { + for (int i = 0, n = parent.getChildCount(); i < n; i++) { + View v = parent.getChildAt(i); + if (v.getVisibility() != VISIBLE) continue; + float x = cursorX - v.getX(); + if ((x > 0) && (x < v.getWidth())) { + float y = cursorY - v.getY(); + if ((y >= 0) && (y < v.getHeight())) { + if (v instanceof WebView) { + touch(v, x, y, true); + return true; + } + if (v instanceof VideoView) { + touch(v, x, y, false); + return true; + } + if (v.isClickable()) { + v.performClick(); + v.requestFocus(); + activity.post(this::requestFocus); + return true; + } + + if ((v instanceof ViewGroup vg) && click(vg, x, y)) return true; + touch(v, x, y, false); + return true; + } + } + } + return false; + } + + private void touch(View v, float x, float y, boolean clearFocus) { + long time = SystemClock.uptimeMillis(); + MotionEvent down = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, x, y, 0); + if (clearFocus) { + clearFocus(); + setVisibility(GONE); + } + v.dispatchTouchEvent(down); + activity.postDelayed(() -> { + MotionEvent up = MotionEvent.obtain(time, time + 100, MotionEvent.ACTION_UP, x, y, 0); + v.dispatchTouchEvent(up); + if (!clearFocus) activity.post(this::requestFocus); + }, 100); + } + + @Override + public boolean onLongClick(View v) { + delayedHide(); + longClick(screen(), getX(), getY()); + return true; + } + + private void longClick(ViewGroup parent, float cursorX, float cursorY) { + for (int i = 0, n = parent.getChildCount(); i < n; i++) { + View v = parent.getChildAt(i); + if (v.getVisibility() != VISIBLE) continue; + float x = cursorX - v.getX(); + if ((x > 0) && (x < v.getWidth())) { + float y = cursorY - v.getY(); + if ((y >= 0) && (y < v.getHeight())) { + if (v.isLongClickable()) { + v.performLongClick(); + activity.post(this::requestFocus); + return; + } + if (v instanceof ViewGroup vg) longClick(vg, x, y); + return; + } + } + } + } + + private void delayedHide() { + hide.cancel(); + hide = activity.postDelayed(() -> { + hide = Cancellable.CANCELED; + clearFocus(); + setVisibility(GONE); + MediaItemListView.focusActive(activity.getContext(), null); + }, 5000); + } + + void move(float cursorX, float cursorY, int keyCode) { + delayedHide(); + + if (resetAccel.cancel()) accel += 1; + resetAccel = activity.postDelayed(() -> { + resetAccel = Cancellable.CANCELED; + accel = 1; + }, 200); + + if ((keyCode == KEYCODE_DPAD_UP) && (getY() == 0)) { + scroll(true); + } else if ((keyCode == KEYCODE_DPAD_DOWN) && (getY() >= screen().getHeight() - getHeight())) { + scroll(false); + } + + if (!focusFb(screen(), cursorX, cursorY)) requestFocus(); + animate().x(cursorX).y(cursorY).setDuration(0).start(); + } + + private boolean focusFb(ViewGroup screen, float cursorX, float cursorY) { + View fb = screen.findViewById(R.id.floating_button); + float fbX = fb.getX(); + float fbY = fb.getY(); + if ((cursorX >= fbX) && (cursorX < fbX + fb.getWidth()) && (cursorY >= fbY) && + (cursorY < fbY + fb.getHeight())) { + fb.requestFocus(); + return true; + } + return false; + } + + private void scroll(boolean up) { + ActivityFragment f = activity.getActiveFragment(); + if (f == null) return; + View root = f.getView(); + if (root instanceof ViewGroup vg) scroll(up, vg); + } + + private boolean scroll(boolean up, View v) { + if (v instanceof RecyclerView rv) { + LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); + if (lm == null) return false; + int pos = lm.findFirstVisibleItemPosition(); + if (up) { + if (pos > 0) lm.scrollToPositionWithOffset(pos - 1, 0); + } else { + if (pos < lm.getItemCount() - 1) lm.scrollToPositionWithOffset(pos + 1, 0); + } + return true; + } else if (v instanceof WebView wv) { + if (up) wv.pageUp(false); + else wv.pageDown(false); + return true; + } else if (v instanceof ViewGroup vg) { + for (int i = 0, n = vg.getChildCount(); i < n; i++) { + if (scroll(up, vg.getChildAt(i))) return true; + } + } + return false; + } + + private ViewGroup screen() { + return (ViewGroup) getParent(); + } } } diff --git a/fermata/src/auto/res/drawable/cursor.xml b/fermata/src/auto/res/drawable/cursor.xml new file mode 100644 index 00000000..1985cf17 --- /dev/null +++ b/fermata/src/auto/res/drawable/cursor.xml @@ -0,0 +1,10 @@ + + + diff --git a/fermata/src/main/java/me/aap/fermata/action/Key.java b/fermata/src/main/java/me/aap/fermata/action/Key.java index f4440791..9dbd9f64 100644 --- a/fermata/src/main/java/me/aap/fermata/action/Key.java +++ b/fermata/src/main/java/me/aap/fermata/action/Key.java @@ -32,7 +32,7 @@ public enum Key { HEADSETHOOK(KeyEvent.KEYCODE_HEADSETHOOK, Action.PLAY_PAUSE, Action.NEXT, Action.ACTIVATE_VOICE_CTRL), SEARCH(KeyEvent.KEYCODE_SEARCH, Action.ACTIVATE_VOICE_CTRL), - BACK(KeyEvent.KEYCODE_BACK, Action.BACK_OR_EXIT), + BACK(KeyEvent.KEYCODE_BACK, Action.BACK_OR_EXIT, Action.NONE, Action.NONE), ESCAPE(KeyEvent.KEYCODE_ESCAPE, Action.BACK_OR_EXIT), DEL(KeyEvent.KEYCODE_DEL, Action.STOP), MENU(KeyEvent.KEYCODE_MENU, Action.MENU, Action.CP_MENU, Action.CP_MENU), diff --git a/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java b/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java index 7abffbad..8971ede5 100644 --- a/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java +++ b/fermata/src/main/java/me/aap/fermata/action/KeyEventHandler.java @@ -76,6 +76,13 @@ public boolean handle(KeyEvent event, IntObjectFunction defau if (clickHandler == null) return defaultHandler.apply(code, event); var longClickHandler = k.getLongClickHandler(); if (longClickHandler == null) return defaultHandler.apply(code, event); + + if ((dblClickHandler == Action.NONE.getHandler()) && + (longClickHandler == Action.NONE.getHandler())) { + clickHandler.handle(cb, activity, uptimeMillis()); + return true; + } + worker = new Worker(event, clickHandler, dblClickHandler, longClickHandler); return false; } @@ -137,6 +144,9 @@ boolean handle(KeyEvent e) { if (dblClickHandler == clickHandler) { longClickTime = uptimeMillis(); handle(longClickHandler); + } else { + worker = null; + handle(clickHandler); } return true; } diff --git a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java index f1b407f6..392434fe 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java +++ b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityDelegate.java @@ -745,6 +745,11 @@ public boolean goToItem(Item i) { return true; } + @Override + protected boolean exitOnBackPressed() { + return !isCarActivity(); + } + @Override public OverlayMenu createMenu(View anchor) { return findViewById(R.id.context_menu); diff --git a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java index 5d016e22..4d06c5e7 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java +++ b/fermata/src/main/java/me/aap/fermata/ui/activity/MainActivityPrefs.java @@ -39,6 +39,7 @@ public interface MainActivityPrefs Pref HIDE_BARS = Pref.b("HIDE_BARS", false); Pref FULLSCREEN = Pref.b("FULLSCREEN", false); Pref SHOW_PG_UP_DOWN = Pref.b("SHOW_PG_UP_DOWN", true); + Pref USE_DPAD_CURSOR = AUTO ? Pref.b("USE_DPAD_CURSOR", true) : null; Pref NAV_BAR_POS = Pref.i("NAV_BAR_POS", NavBarView.POSITION_BOTTOM); Pref NAV_BAR_SIZE = Pref.f("NAV_BAR_SIZE", 1f); Pref TOOL_BAR_SIZE = Pref.f("TOOL_BAR_SIZE", 1f); @@ -132,6 +133,10 @@ default boolean getShowPgUpDownPref(MainActivityDelegate a) { return getBooleanPref(SHOW_PG_UP_DOWN); } + default boolean useDpadCursor(MainActivityDelegate a) { + return AUTO && a.isCarActivity() && getBooleanPref(USE_DPAD_CURSOR); + } + static boolean hasNavBarPosPref(MainActivityDelegate a, List> prefs) { if (AUTO && a.isCarActivity()) return prefs.contains(NAV_BAR_POS_AA); return prefs.contains(NAV_BAR_POS); diff --git a/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java b/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java index 26808f04..275f103e 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java +++ b/fermata/src/main/java/me/aap/fermata/ui/fragment/SettingsFragment.java @@ -653,6 +653,13 @@ private void addInterface(MainActivityDelegate a, PreferenceSet ps, Pref { + o.store = a.getPrefs(); + o.pref = MainActivityPrefs.USE_DPAD_CURSOR; + o.title = R.string.use_dpad_cursor; + }); + } ps.addListPref(o -> { o.store = a.getPrefs(); o.pref = nbPos; diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemListView.java b/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemListView.java index 79c00b89..890337b9 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemListView.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemListView.java @@ -190,6 +190,13 @@ public static View focusSearchFirstVisible(Context ctx, @Nullable View focused) ActivityFragment f = MainActivityDelegate.get(ctx).getActiveFragment(); if (f instanceof MediaLibFragment) { MediaItemListView lv = ((MediaLibFragment) f).getListView(); + LinearLayoutManager lm = (LinearLayoutManager) lv.getLayoutManager(); + if (lm != null) { + int pos = lm.findFirstVisibleItemPosition(); + if (pos != RecyclerView.NO_POSITION) { + return lv.getAdapter().getList().get(pos).getView(); + } + } View first = lv.getChildAt(0); return (first != null) ? first : lv.focusEmpty(); } diff --git a/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemView.java b/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemView.java index 57faef2f..36b391ba 100644 --- a/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemView.java +++ b/fermata/src/main/java/me/aap/fermata/ui/view/MediaItemView.java @@ -65,10 +65,11 @@ /** * @author Andrey Pavlenko */ -public class MediaItemView extends ConstraintLayout implements OnLongClickListener, - OnCheckedChangeListener, Item.ChangeListener { - private static final RotateAnimation rotate = new RotateAnimation(0, 360, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); +public class MediaItemView extends ConstraintLayout + implements OnLongClickListener, OnCheckedChangeListener, Item.ChangeListener { + private static final RotateAnimation rotate = + new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, + 0.5f); private static Drawable loadingDrawable; @ColorInt private static int iconColor; @@ -90,8 +91,9 @@ public class MediaItemView extends ConstraintLayout implements OnLongClickListen public MediaItemView(Context ctx, AttributeSet attrs) { super(ctx, attrs, R.attr.appMediaItemStyle); - TypedArray ta = ctx.obtainStyledAttributes(attrs, R.styleable.MediaItemView, - R.attr.appMediaItemStyle, R.style.AppTheme_MediaItemStyle); + TypedArray ta = + ctx.obtainStyledAttributes(attrs, R.styleable.MediaItemView, R.attr.appMediaItemStyle, + R.style.AppTheme_MediaItemStyle); iconColor = ta.getColor(R.styleable.MediaItemView_iconColor, 0); hintColor = ta.getColor(R.styleable.MediaItemView_hintColor, 0); titleTextAppearance = ta.getResourceId(R.styleable.MediaItemView_titleTextAppearance, 0); @@ -119,7 +121,8 @@ public void setSize(Context ctx, boolean grid, float size) { setTextAppearance(ctx, getTitle(), titleTextAppearance, size); setTextAppearance(ctx, getSubtitle(), subtitleTextAppearance, size); if (!grid) { - int iconSize = (int) (getTitle().getTextSize() + getSubtitle().getTextSize() + toPx(ctx, 10)); + int iconSize = (int) (getTitle().getTextSize() + getSubtitle().getTextSize() + toPx(ctx, + 10)); ImageView i = getIcon(); ViewGroup.LayoutParams lp = i.getLayoutParams(); lp.height = iconSize; @@ -128,8 +131,7 @@ public void setSize(Context ctx, boolean grid, float size) { } } - private void setTextAppearance(Context ctx, TextView v, @StyleRes int res, - float scale) { + private void setTextAppearance(Context ctx, TextView v, @StyleRes int res, float scale) { v.setTextAppearance(res); v.setTextSize(COMPLEX_UNIT_PX, getTextAppearanceSize(ctx, res) * scale); v.setTextColor(textTint); @@ -198,8 +200,8 @@ private FutureSupplier load(MediaItemWrapper w, boolean setProgress(i, 0, 0); } - FutureSupplier load = loading = i.getMediaDescription().main() - .addConsumer((md, fail, p, total) -> { + FutureSupplier load = + loading = i.getMediaDescription().main().addConsumer((md, fail, p, total) -> { if (getItemWrapper() != w) return; if (fail != null) { @@ -217,28 +219,30 @@ private FutureSupplier load(MediaItemWrapper w, boolean } if ((p == PROGRESS_DONE) || (p == 3)) { Bundle b = md.getExtras(); - if (b != null) setProgress(i, b.getLong(STREAM_START_TIME), b.getLong(STREAM_END_TIME)); + if (b != null) setProgress(i, b.getLong(STREAM_START_TIME), + b.getLong(STREAM_END_TIME)); } if ((p == PROGRESS_DONE) || (p == 4)) { Uri uri = md.getIconUri(); if (uri != null) { - FutureSupplier loadIcon = i.getLib().getBitmap(uri.toString(), true, true) - .main().onCompletion((bm, err) -> { - if (getItemWrapper() != w) return; - - ImageView icon = getIcon(); - icon.clearAnimation(); - cancelLoading(); - - if (bm != null) { - icon.setImageTintList(null); - icon.setImageBitmap(bm); - } else { - icon.setImageTintList(iconTint); - icon.setImageResource(i.getIcon()); - } - }); + FutureSupplier loadIcon = + i.getLib().getBitmap(uri.toString(), true, true).main() + .onCompletion((bm, err) -> { + if (getItemWrapper() != w) return; + + ImageView icon = getIcon(); + icon.clearAnimation(); + cancelLoading(); + + if (bm != null) { + icon.setImageTintList(null); + icon.setImageBitmap(bm); + } else { + icon.setImageTintList(iconTint); + icon.setImageResource(i.getIcon()); + } + }); if (!loadIcon.isDone()) { ImageView icon = getIcon(); @@ -353,7 +357,8 @@ public void onDrawForeground(Canvas canvas) { if ((item instanceof ArchiveItem) && !((ArchiveItem) item).isExpired()) { d = archiveLabelDrawable; if (d == null) { - d = archiveLabelDrawable = VectorDrawableCompat.create(getResources(), R.drawable.archive_label, null); + d = archiveLabelDrawable = + VectorDrawableCompat.create(getResources(), R.drawable.archive_label, null); if (d == null) return; d.setTint(hintColor); } @@ -367,14 +372,16 @@ public void onDrawForeground(Canvas canvas) { if (prefs.getWatchedPref()) { d = watchedVideoDrawable; if (d == null) { - d = watchedVideoDrawable = VectorDrawableCompat.create(getResources(), R.drawable.done, null); + d = watchedVideoDrawable = + VectorDrawableCompat.create(getResources(), R.drawable.done, null); if (d == null) return; d.setTint(hintColor); } } else if (prefs.getPositionPref() > 0) { d = watchingVideoDrawable; if (d == null) { - d = watchingVideoDrawable = VectorDrawableCompat.create(getResources(), R.drawable.watching, null); + d = watchingVideoDrawable = + VectorDrawableCompat.create(getResources(), R.drawable.watching, null); if (d == null) return; d.setTint(hintColor); } @@ -497,9 +504,12 @@ public boolean onLongClick(View v) { } @Override - protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { + protected void onFocusChanged(boolean gainFocus, int direction, + @Nullable Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (!BuildConfig.AUTO || !gainFocus) return; + MainActivityDelegate d = getMainActivity(); + if (!d.getPrefs().useDpadCursor(d)) return; // The next item in the list must always be visible for rotary input scrolling MediaItemListView lv = getListView(); diff --git a/fermata/src/main/res/values-ru/strings.xml b/fermata/src/main/res/values-ru/strings.xml index b929b1a6..b3ceaf87 100644 --- a/fermata/src/main/res/values-ru/strings.xml +++ b/fermata/src/main/res/values-ru/strings.xml @@ -30,6 +30,7 @@ Скрывать верхнюю и нижнюю панель, когда появляется панель управления Полноэкранный режим Показывать кнопки вверх/вниз + Использовать курсор для D-pad навигации Размер панели навигации Размер панели инструментов Размер панели управления diff --git a/fermata/src/main/res/values/ids.xml b/fermata/src/main/res/values/ids.xml index 570b1ccd..f9293a74 100644 --- a/fermata/src/main/res/values/ids.xml +++ b/fermata/src/main/res/values/ids.xml @@ -130,4 +130,6 @@ + + \ No newline at end of file diff --git a/fermata/src/main/res/values/strings.xml b/fermata/src/main/res/values/strings.xml index 9fd1f2cf..7eeb9e1e 100644 --- a/fermata/src/main/res/values/strings.xml +++ b/fermata/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Hide bars Hide top and bottom bars when the control panel appears Show page up/down buttons + Use cursor for D-pad navigation Fullscreen mode Navigation bar size Tool bar size diff --git a/modules/web/src/main/java/me/aap/fermata/addon/web/FermataChromeClient.java b/modules/web/src/main/java/me/aap/fermata/addon/web/FermataChromeClient.java index 920dfd20..738d2d82 100644 --- a/modules/web/src/main/java/me/aap/fermata/addon/web/FermataChromeClient.java +++ b/modules/web/src/main/java/me/aap/fermata/addon/web/FermataChromeClient.java @@ -117,6 +117,7 @@ public void onShowCustomView(View view, CustomViewCallback callback) { customView = view; customViewCallback = callback; addCustomView(view); + getWebView().setVisibility(GONE); MainActivityDelegate a = MainActivityDelegate.get(view.getContext()); setFullScreen(a, true); @@ -135,6 +136,7 @@ public void onHideCustomView() { touchStamp = 0; MainActivityDelegate a = MainActivityDelegate.get(customView.getContext()); removeCustomView(customView); + getWebView().setVisibility(VISIBLE); setFullScreen(a, false); customViewCallback.onCustomViewHidden(); customView = null; diff --git a/modules/web/src/main/java/me/aap/fermata/addon/web/yt/YoutubeWebView.java b/modules/web/src/main/java/me/aap/fermata/addon/web/yt/YoutubeWebView.java index 2d308e21..b86d9aa6 100644 --- a/modules/web/src/main/java/me/aap/fermata/addon/web/yt/YoutubeWebView.java +++ b/modules/web/src/main/java/me/aap/fermata/addon/web/yt/YoutubeWebView.java @@ -18,6 +18,7 @@ import me.aap.fermata.addon.web.FermataChromeClient; import me.aap.fermata.addon.web.FermataJsInterface; import me.aap.fermata.addon.web.FermataWebView; +import me.aap.fermata.media.service.MediaSessionCallback; import me.aap.fermata.ui.activity.MainActivityDelegate; import me.aap.utils.async.FutureSupplier; import me.aap.utils.async.Promise; @@ -58,6 +59,13 @@ public void loadUrl(@NonNull String url) { super.loadUrl(url); } + @Override + public void goBack() { + MediaSessionCallback cb = MainActivityDelegate.get(getContext()).getMediaSessionCallback(); + if (cb.getEngine() instanceof YoutubeMediaEngine) cb.onStop(); + super.goBack(); + } + @Override protected void pageLoaded(String uri) { attachListeners();