From 96ca75e3f8a6f12917f3b477dc7fda353fbd0ab8 Mon Sep 17 00:00:00 2001 From: "Error ." Date: Fri, 6 Nov 2026 19:19:08 +0000 Subject: [PATCH 1/3] Handle D-pad input after mouse mode on TV Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ArchiveConfirmationFragment.java | 5 + .../ui/fragments/BaseDialogFragment.java | 118 ++++++++++++++++++ .../InstallConfirmationFragment.java | 5 + 3 files changed, 128 insertions(+) diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java index 117cddd..846a8a8 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java @@ -22,6 +22,11 @@ public ArchiveConfirmationFragment(PackageUserAction dialogData) { mDialogData = dialogData; } + @Override + protected boolean shouldAutoFocusPositiveButtonOnTv() { + return true; + } + @Override public void onStart() { super.onStart(); diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java index a0d1368..b0651cf 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java @@ -7,6 +7,9 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewTreeObserver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,6 +23,9 @@ public class BaseDialogFragment extends DialogFragment { public final InstallStage mDialogData; InstallViewModel mViewModel; + private ViewTreeObserver mDialogViewTreeObserver; + private ViewTreeObserver.OnWindowFocusChangeListener mWindowFocusChangeListener; + private ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener; public BaseDialogFragment(InstallStage dialogData) { mDialogData = dialogData; @@ -50,7 +56,93 @@ public void onStart() { button.setClickable(true); } } + if (shouldAutoFocusPositiveButtonOnTv() && isTvDevice) { + configureTvConfirmationDialog(alertDialog); + } + } + } + + private void configureTvConfirmationDialog(AlertDialog dialog) { + var positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + var negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + var neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (positiveButton == null) { + return; + } + + if (negativeButton != null) { + negativeButton.setFocusableInTouchMode(true); + } + if (neutralButton != null) { + neutralButton.setFocusableInTouchMode(true); + } + + var decorView = dialog.getWindow().getDecorView(); + mDialogViewTreeObserver = decorView.getViewTreeObserver(); + mWindowFocusChangeListener = hasFocus -> { + if (hasFocus) { + focusPositiveUnlessOtherButtonFocused( + positiveButton, negativeButton, neutralButton); + } + }; + mTouchModeChangeListener = inTouchMode -> { + if (!inTouchMode) { + focusPositiveUnlessOtherButtonFocused( + positiveButton, negativeButton, neutralButton); + } + }; + mDialogViewTreeObserver.addOnWindowFocusChangeListener(mWindowFocusChangeListener); + mDialogViewTreeObserver.addOnTouchModeChangeListener(mTouchModeChangeListener); + + dialog.setOnKeyListener((ignored, keyCode, event) -> + handleTvConfirmationKey( + positiveButton, negativeButton, neutralButton, keyCode, event)); + } + + private void focusPositiveUnlessOtherButtonFocused( + View positiveButton, View negativeButton, View neutralButton) { + if (!hasFocus(negativeButton) && !hasFocus(neutralButton)) { + positiveButton.requestFocus(); + } + } + + private boolean hasFocus(View button) { + return button != null && button.hasFocus(); + } + + private boolean handleTvConfirmationKey( + View positiveButton, View negativeButton, View neutralButton, + int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER + || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) { + if (hasFocus(negativeButton) || hasFocus(neutralButton)) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + positiveButton.requestFocus(); + positiveButton.setPressed(true); + return true; + } + if (event.getAction() == KeyEvent.ACTION_UP) { + positiveButton.setPressed(false); + if (!event.isCanceled()) { + positiveButton.performClick(); + } + return true; + } + } + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT + && event.getAction() == KeyEvent.ACTION_DOWN + && !hasFocus(negativeButton) + && !hasFocus(neutralButton) + && negativeButton != null + && negativeButton.getVisibility() == View.VISIBLE + && negativeButton.isEnabled()) { + negativeButton.requestFocus(); + return true; } + return false; } private boolean isTvDevice() { @@ -62,6 +154,10 @@ private boolean isTvDevice() { || !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); } + protected boolean shouldAutoFocusPositiveButtonOnTv() { + return false; + } + protected void focusPositiveButtonForDpad() { if (getDialog() instanceof AlertDialog alertDialog) { var positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); @@ -76,6 +172,28 @@ protected void focusPositiveButtonForDpad() { } } + @Override + public void onStop() { + removeDialogFocusListeners(); + super.onStop(); + } + + private void removeDialogFocusListeners() { + if (mDialogViewTreeObserver != null && mDialogViewTreeObserver.isAlive()) { + if (mWindowFocusChangeListener != null) { + mDialogViewTreeObserver.removeOnWindowFocusChangeListener( + mWindowFocusChangeListener); + } + if (mTouchModeChangeListener != null) { + mDialogViewTreeObserver.removeOnTouchModeChangeListener( + mTouchModeChangeListener); + } + } + mDialogViewTreeObserver = null; + mWindowFocusChangeListener = null; + mTouchModeChangeListener = null; + } + @Override public void onCancel(@NonNull DialogInterface dialog) { super.onCancel(dialog); diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java index b587d78..c9aad77 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java @@ -23,6 +23,11 @@ public InstallConfirmationFragment(InstallUserAction dialogData) { mDialogData = dialogData; } + @Override + protected boolean shouldAutoFocusPositiveButtonOnTv() { + return true; + } + @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { From 7339ac6931bbafe36492bb700496cbbb10381919 Mon Sep 17 00:00:00 2001 From: "Error ." Date: Fri, 6 Nov 2026 19:29:54 +0000 Subject: [PATCH 2/3] Universal D-Pad OK and arrow navigation on confirmation dialogs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ArchiveConfirmationFragment.java | 2 +- .../ui/fragments/BaseDialogFragment.java | 32 ++++++++++++------- .../InstallConfirmationFragment.java | 2 +- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java index 846a8a8..354db9e 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/ArchiveConfirmationFragment.java @@ -23,7 +23,7 @@ public ArchiveConfirmationFragment(PackageUserAction dialogData) { } @Override - protected boolean shouldAutoFocusPositiveButtonOnTv() { + protected boolean shouldHandleConfirmationDpadKeys() { return true; } diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java index b0651cf..bf45273 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java @@ -56,13 +56,13 @@ public void onStart() { button.setClickable(true); } } - if (shouldAutoFocusPositiveButtonOnTv() && isTvDevice) { - configureTvConfirmationDialog(alertDialog); + if (shouldHandleConfirmationDpadKeys()) { + configureConfirmationDialog(alertDialog); } } } - private void configureTvConfirmationDialog(AlertDialog dialog) { + private void configureConfirmationDialog(AlertDialog dialog) { var positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); var negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); var neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); @@ -70,6 +70,10 @@ private void configureTvConfirmationDialog(AlertDialog dialog) { return; } + positiveButton.setFocusable(true); + positiveButton.setFocusableInTouchMode(true); + positiveButton.post(positiveButton::requestFocus); + if (negativeButton != null) { negativeButton.setFocusableInTouchMode(true); } @@ -95,7 +99,7 @@ private void configureTvConfirmationDialog(AlertDialog dialog) { mDialogViewTreeObserver.addOnTouchModeChangeListener(mTouchModeChangeListener); dialog.setOnKeyListener((ignored, keyCode, event) -> - handleTvConfirmationKey( + handleConfirmationKey( positiveButton, negativeButton, neutralButton, keyCode, event)); } @@ -110,7 +114,7 @@ private boolean hasFocus(View button) { return button != null && button.hasFocus(); } - private boolean handleTvConfirmationKey( + private boolean handleConfirmationKey( View positiveButton, View negativeButton, View neutralButton, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER @@ -120,13 +124,12 @@ private boolean handleTvConfirmationKey( return false; } if (event.getAction() == KeyEvent.ACTION_DOWN) { - positiveButton.requestFocus(); positiveButton.setPressed(true); return true; } if (event.getAction() == KeyEvent.ACTION_UP) { positiveButton.setPressed(false); - if (!event.isCanceled()) { + if (!event.isCanceled() && positiveButton.isEnabled()) { positiveButton.performClick(); } return true; @@ -134,14 +137,19 @@ private boolean handleTvConfirmationKey( } if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && event.getAction() == KeyEvent.ACTION_DOWN - && !hasFocus(negativeButton) - && !hasFocus(neutralButton) && negativeButton != null - && negativeButton.getVisibility() == View.VISIBLE - && negativeButton.isEnabled()) { + && negativeButton.isEnabled() + && !negativeButton.hasFocus()) { negativeButton.requestFocus(); return true; } + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + && event.getAction() == KeyEvent.ACTION_DOWN + && positiveButton.isEnabled() + && !positiveButton.hasFocus()) { + positiveButton.requestFocus(); + return true; + } return false; } @@ -154,7 +162,7 @@ private boolean isTvDevice() { || !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); } - protected boolean shouldAutoFocusPositiveButtonOnTv() { + protected boolean shouldHandleConfirmationDpadKeys() { return false; } diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java index c9aad77..7f5a411 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/InstallConfirmationFragment.java @@ -24,7 +24,7 @@ public InstallConfirmationFragment(InstallUserAction dialogData) { } @Override - protected boolean shouldAutoFocusPositiveButtonOnTv() { + protected boolean shouldHandleConfirmationDpadKeys() { return true; } From c09753e55b5f19c268d4b5edd8980a2c7ddad852 Mon Sep 17 00:00:00 2001 From: "Error ." Date: Fri, 6 Nov 2026 19:46:23 +0000 Subject: [PATCH 3/3] Disable touch mode focus on phones while retaining TV D-Pad key navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ui/fragments/BaseDialogFragment.java | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java index bf45273..31bf0bb 100644 --- a/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java +++ b/app/src/main/java/io/i10nstaller/ui/fragments/BaseDialogFragment.java @@ -1,11 +1,7 @@ package io.i10nstaller.ui.fragments; import android.app.AlertDialog; -import android.app.UiModeManager; -import android.content.Context; import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.content.res.Configuration; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; @@ -42,7 +38,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { public void onStart() { super.onStart(); if (getDialog() instanceof AlertDialog alertDialog) { - boolean isTvDevice = isTvDevice(); for (int which : new int[] { AlertDialog.BUTTON_POSITIVE, AlertDialog.BUTTON_NEGATIVE, @@ -51,8 +46,7 @@ public void onStart() { var button = alertDialog.getButton(which); if (button != null) { button.setFocusable(true); - button.setFocusableInTouchMode( - isTvDevice && which == AlertDialog.BUTTON_POSITIVE); + button.setFocusableInTouchMode(false); button.setClickable(true); } } @@ -71,15 +65,18 @@ private void configureConfirmationDialog(AlertDialog dialog) { } positiveButton.setFocusable(true); - positiveButton.setFocusableInTouchMode(true); - positiveButton.post(positiveButton::requestFocus); - + positiveButton.setFocusableInTouchMode(false); if (negativeButton != null) { - negativeButton.setFocusableInTouchMode(true); + negativeButton.setFocusableInTouchMode(false); } if (neutralButton != null) { - neutralButton.setFocusableInTouchMode(true); + neutralButton.setFocusableInTouchMode(false); } + positiveButton.post(() -> { + if (!positiveButton.isInTouchMode()) { + positiveButton.requestFocus(); + } + }); var decorView = dialog.getWindow().getDecorView(); mDialogViewTreeObserver = decorView.getViewTreeObserver(); @@ -105,7 +102,9 @@ private void configureConfirmationDialog(AlertDialog dialog) { private void focusPositiveUnlessOtherButtonFocused( View positiveButton, View negativeButton, View neutralButton) { - if (!hasFocus(negativeButton) && !hasFocus(neutralButton)) { + if (!positiveButton.isInTouchMode() + && !hasFocus(negativeButton) + && !hasFocus(neutralButton)) { positiveButton.requestFocus(); } } @@ -135,33 +134,25 @@ private boolean handleConfirmationKey( return true; } } - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT - && event.getAction() == KeyEvent.ACTION_DOWN - && negativeButton != null - && negativeButton.isEnabled() - && !negativeButton.hasFocus()) { - negativeButton.requestFocus(); - return true; + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (negativeButton != null && negativeButton.isEnabled()) { + negativeButton.requestFocus(); + } + return true; + } } - if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - && event.getAction() == KeyEvent.ACTION_DOWN - && positiveButton.isEnabled() - && !positiveButton.hasFocus()) { - positiveButton.requestFocus(); - return true; + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (positiveButton.isEnabled()) { + positiveButton.requestFocus(); + } + return true; + } } return false; } - private boolean isTvDevice() { - var context = requireContext(); - var uiModeManager = - (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); - return (uiModeManager != null - && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) - || !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); - } - protected boolean shouldHandleConfirmationDpadKeys() { return false; } @@ -170,9 +161,8 @@ protected void focusPositiveButtonForDpad() { if (getDialog() instanceof AlertDialog alertDialog) { var positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); if (positiveButton != null) { - boolean isTvDevice = isTvDevice(); positiveButton.post(() -> { - if (isTvDevice || !positiveButton.isInTouchMode()) { + if (!positiveButton.isInTouchMode()) { positiveButton.requestFocus(); } });