diff --git a/core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt b/core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt index 4364b4244..5f4c88dee 100644 --- a/core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt +++ b/core/common/actions/src/main/java/com/buzbuz/smartautoclicker/core/common/actions/gesture/GestureExecutor.kt @@ -23,9 +23,9 @@ import android.util.Log import com.buzbuz.smartautoclicker.core.base.Dumpable import com.buzbuz.smartautoclicker.core.base.addDumpTabulationLvl + import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull - import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton @@ -63,7 +63,7 @@ internal class GestureExecutor @Inject constructor() : Dumpable { resultCallback = resultCallback ?: newGestureResultCallback() - val timeoutMs = gesture.gestureDurationMs() * GESTURE_CALLBACK_TIMEOUT_DURATION_RATIO_MS + val timeoutMs = gesture.timeoutDurationMs() val result = withTimeoutOrNull(timeoutMs.milliseconds) { suspendCancellableCoroutine { continuation -> currentContinuation = continuation @@ -123,14 +123,22 @@ internal class GestureExecutor @Inject constructor() : Dumpable { } } -private fun GestureDescription.gestureDurationMs(): Long { +private fun GestureDescription.durationMs(): Long { var maxEndTime = 0L for (i in 0 until strokeCount) { val stroke = getStroke(i) maxEndTime = maxOf(maxEndTime, stroke.startTime + stroke.duration) } + return maxEndTime } -private const val GESTURE_CALLBACK_TIMEOUT_DURATION_RATIO_MS = 1.25 +private fun GestureDescription.timeoutDurationMs(): Long = + (durationMs() * GESTURE_CALLBACK_TIMEOUT_DURATION_RATIO_MS) + .coerceIn(GESTURE_CALLBACK_TIMEOUT_MIN_MS, GESTURE_CALLBACK_TIMEOUT_MAX_MS) + +private const val GESTURE_CALLBACK_TIMEOUT_MIN_MS = 100L +private const val GESTURE_CALLBACK_TIMEOUT_MAX_MS = 65_000L +private const val GESTURE_CALLBACK_TIMEOUT_DURATION_RATIO_MS = 2 + private const val TAG = "GestureExecutor" \ No newline at end of file diff --git a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/OverlayDialog.kt b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/OverlayDialog.kt index b471cf0a1..605d914a9 100644 --- a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/OverlayDialog.kt +++ b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/OverlayDialog.kt @@ -20,6 +20,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.Window import android.view.WindowManager import android.view.inputmethod.InputMethodManager @@ -41,7 +42,7 @@ import java.io.PrintWriter * This class ensure that all dialogs opened from a service will have the same behaviour. It provides basic lifecycle * alike methods to ease the view initialization/cleaning. */ -abstract class OverlayDialog(@StyleRes theme: Int? = null) : BaseOverlay(theme, recreateOnRotation = true, useWindowContext = true) { +abstract class OverlayDialog(@StyleRes theme: Int? = null) : BaseOverlay(theme, recreateOnRotation = true) { /** The Android InputMethodManger, for ensuring the keyboard dismiss on dialog dismiss. */ private lateinit var inputMethodManager: InputMethodManager @@ -105,7 +106,7 @@ abstract class OverlayDialog(@StyleRes theme: Int? = null) : BaseOverlay(theme, window?.apply { setType(OverlayManager.OVERLAY_WINDOW_TYPE) - setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) + applySoftInputMode(this) decorView.setOnTouchListener(hideSoftInputTouchListener) } @@ -120,6 +121,14 @@ abstract class OverlayDialog(@StyleRes theme: Int? = null) : BaseOverlay(theme, onDialogCreated(dialog!!) } + /** Soft keyboard: resize dialog so input fields stay visible (accessibility overlay). */ + protected open fun applySoftInputMode(window: Window) { + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN, + ) + } + @CallSuper override fun onStart() { if (isShown) return diff --git a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialog.kt b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialog.kt index 0855e701d..f7f9dc992 100644 --- a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialog.kt +++ b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialog.kt @@ -21,6 +21,8 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager import androidx.annotation.CallSuper import androidx.annotation.StyleRes @@ -47,6 +49,8 @@ abstract class NavBarDialog(@StyleRes theme: Int) : OverlayDialog(theme) { lateinit var floatingActionButtons: IncludeFloatingActionButtonsBinding lateinit var topBarBinding: IncludeDialogNavigationTopBarBinding + private var imeLiftController: NavBarDialogImeLiftController? = null + abstract fun inflateMenu(navBarView: NavigationBarView) abstract fun onCreateContent(navItemId: Int): NavBarDialogContent @@ -55,6 +59,17 @@ abstract class NavBarDialog(@StyleRes theme: Int) : OverlayDialog(theme) { open fun onContentViewChanged(navItemId: Int) = Unit + /** + * Keep window size unchanged while the IME is shown. + * Portrait bottom bar sits on the dialog [CoordinatorLayout]; [SOFT_INPUT_ADJUST_RESIZE] parks the sheet mid-screen. + */ + override fun applySoftInputMode(window: Window) { + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING or + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN, + ) + } + override fun onCreateView(): ViewGroup { baseViewBinding = DialogBaseNavBarBinding.inflate(LayoutInflater.from(context)).apply { layoutTopBar.apply { @@ -99,6 +114,15 @@ abstract class NavBarDialog(@StyleRes theme: Int) : OverlayDialog(theme) { setupPortraitViews() } + val coordinator = dialogCoordinatorLayout + val decor = dialog.window?.decorView + if (coordinator != null && decor != null) { + imeLiftController = NavBarDialogImeLiftController( + liftTarget = coordinator, + insetHost = decor, + ).also { it.start() } + } + updateContentView( itemId = navBarView.selectedItemId, forceUpdate = true, @@ -116,6 +140,8 @@ abstract class NavBarDialog(@StyleRes theme: Int) : OverlayDialog(theme) { } override fun onDestroy() { + imeLiftController?.stop() + imeLiftController = null contentMap.values.forEach { content -> content.destroy() } diff --git a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialogImeLiftController.kt b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialogImeLiftController.kt new file mode 100644 index 000000000..340eb84bb --- /dev/null +++ b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/dialog/implementation/navbar/NavBarDialogImeLiftController.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2026 Kevin Buzeau + * + * 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 com.buzbuz.smartautoclicker.core.common.overlays.dialog.implementation.navbar + +import android.animation.ValueAnimator +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator + +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat + +/** + * Lifts a dialog content root when the IME is visible. + * + * [NavBarDialog] keeps [android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING] so the portrait bottom bar + * is not parked by system resize; this controller mirrors IME height via translation instead. + */ +internal class NavBarDialogImeLiftController( + private val liftTarget: View, + private val insetHost: View, +) { + + private var lastImeLiftPx: Int = 0 + private var imeAnimationRunning: Boolean = false + private var imeLiftAnimator: ValueAnimator? = null + + private val insetsAnimationCallback = + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) { + imeAnimationRunning = true + imeLiftAnimator?.cancel() + } + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList, + ): WindowInsetsCompat { + applyImeLift(resolveImeLiftPx(insets)) + return insets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) { + imeAnimationRunning = false + syncImeLiftAnimated() + } + } + } + + fun start() { + ViewCompat.setWindowInsetsAnimationCallback(insetHost, insetsAnimationCallback) + ViewCompat.setOnApplyWindowInsetsListener(insetHost) { _, insets -> + if (!imeAnimationRunning) { + applyImeLift(resolveImeLiftPx(insets)) + } + insets + } + ViewCompat.requestApplyInsets(insetHost) + } + + fun stop() { + imeLiftAnimator?.cancel() + imeLiftAnimator = null + ViewCompat.setWindowInsetsAnimationCallback(insetHost, null) + ViewCompat.setOnApplyWindowInsetsListener(insetHost, null) + applyImeLift(0) + } + + private fun syncImeLiftAnimated() { + val insets = ViewCompat.getRootWindowInsets(insetHost) ?: return + val target = resolveImeLiftPx(insets) + if (target == lastImeLiftPx) return + + imeLiftAnimator?.cancel() + val start = lastImeLiftPx + imeLiftAnimator = ValueAnimator.ofInt(start, target).apply { + duration = IME_LIFT_ANIMATION_MS + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { animator -> + applyImeLift(animator.animatedValue as Int) + } + start() + } + } + + private fun resolveImeLiftPx(insets: WindowInsetsCompat): Int { + if (!insets.isVisible(WindowInsetsCompat.Type.ime())) return 0 + return insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + } + + private fun applyImeLift(imeBottomPx: Int) { + if (imeBottomPx == lastImeLiftPx) return + lastImeLiftPx = imeBottomPx + liftTarget.translationY = -imeBottomPx.toFloat() + } + + private companion object { + const val IME_LIFT_ANIMATION_MS = 180L + } +} diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/notification/NotificationDialog.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/notification/NotificationDialog.kt index 60f6749ee..fb0007a84 100644 --- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/notification/NotificationDialog.kt +++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/notification/NotificationDialog.kt @@ -102,13 +102,13 @@ class NotificationDialog( } setOnCheckboxClickedListener { showCounterSelectionDialog { selectedCounter -> - setTextValue( - textField.text.toString().appendCounterReference( - counterName = selectedCounter, - atIndex = textField.selectionEnd, - ), - force = true, + val textToWrite = textField.text.toString().appendCounterReference( + counterName = selectedCounter, + atIndex = textField.selectionEnd, ) + + viewModel.setNotificationMessage(textToWrite) + setTextValue(value = textToWrite, force = true) } } } diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/settext/SetTextDialog.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/settext/SetTextDialog.kt index 50853c2c3..d135958fa 100644 --- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/settext/SetTextDialog.kt +++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/action/settext/SetTextDialog.kt @@ -107,13 +107,13 @@ class SetTextDialog( setOnTextChangedListener { viewModel.setTextToWrite(it.toString()) } setOnCheckboxClickedListener { showCounterSelectionDialog { selectedCounter -> - setTextValue( - textField.text.toString().appendCounterReference( - counterName = selectedCounter, - atIndex = textField.selectionEnd, - ), - force = true, + val textToWrite = textField.text.toString().appendCounterReference( + counterName = selectedCounter, + atIndex = textField.selectionEnd, ) + + viewModel.setTextToWrite(textToWrite) + setTextValue(value = textToWrite, force = true) } } } diff --git a/feature/tutorial/src/main/res/values-ar/strings.xml b/feature/tutorial/src/main/res/values-ar/strings.xml index 144183fe8..87d83732b 100644 --- a/feature/tutorial/src/main/res/values-ar/strings.xml +++ b/feature/tutorial/src/main/res/values-ar/strings.xml @@ -42,12 +42,8 @@ --> - ظروف سلبية - تأكد من عدم عرض شيء ما قبل تنفيذ الإجراء - سيناريو أحداث متعددة - تعرف على كيفية الجمع بين أحداث متعددة لتحقيق أتمتة معقدة diff --git a/feature/tutorial/src/main/res/values-es/strings.xml b/feature/tutorial/src/main/res/values-es/strings.xml index 9e4d99b22..56533052e 100644 --- a/feature/tutorial/src/main/res/values-es/strings.xml +++ b/feature/tutorial/src/main/res/values-es/strings.xml @@ -42,12 +42,8 @@ --> - Rastrear una serie de objetivos móviles - Rastrea y haz clic en una serie de objetivos móviles que se mueven - Clicar hasta que desaparezcan - Rastrea y haz clic en objetivos móviles hasta que desaparezcan diff --git a/feature/tutorial/src/main/res/values-fr/strings.xml b/feature/tutorial/src/main/res/values-fr/strings.xml index 0eb864925..fef2f0846 100644 --- a/feature/tutorial/src/main/res/values-fr/strings.xml +++ b/feature/tutorial/src/main/res/values-fr/strings.xml @@ -42,12 +42,8 @@ --> - Conditions négatives - Vérifiez qu\'un élément n\'est pas affiché avant d\'exécuter une action - Scénario avec plusieurs événements - Apprenez à combiner plusieurs événements pour automatiser des tâches complexes diff --git a/feature/tutorial/src/main/res/values-it/strings.xml b/feature/tutorial/src/main/res/values-it/strings.xml index 57710d632..32208ba27 100644 --- a/feature/tutorial/src/main/res/values-it/strings.xml +++ b/feature/tutorial/src/main/res/values-it/strings.xml @@ -26,12 +26,8 @@ Avanti - Condizioni negative - Verifica che qualcosa non sia visualizzato prima di eseguire un\'azione - Scenario con eventi multipli - Scopri come combinare eventi multipli per realizzare automazioni complesse diff --git a/feature/tutorial/src/main/res/values-ja/strings.xml b/feature/tutorial/src/main/res/values-ja/strings.xml index 1c3764ea0..2699453cf 100644 --- a/feature/tutorial/src/main/res/values-ja/strings.xml +++ b/feature/tutorial/src/main/res/values-ja/strings.xml @@ -7,10 +7,6 @@ 残り時間: %1$d ゲームを開始する - マイナス条件 - アクションを実行する前に、何かが表示されていないことを確認してください - 複数のイベントのシナリオ - 複数のイベントを組み合わせて複雑な自動化を実現する方法を学びます diff --git a/feature/tutorial/src/main/res/values-pt-rBR/strings.xml b/feature/tutorial/src/main/res/values-pt-rBR/strings.xml index 98f5041b6..4e62fa801 100644 --- a/feature/tutorial/src/main/res/values-pt-rBR/strings.xml +++ b/feature/tutorial/src/main/res/values-pt-rBR/strings.xml @@ -42,12 +42,8 @@ --> - Condições negativas - Verifique se algo não está sendo exibido antes de executar uma ação - Cenário de múltiplos eventos - Aprenda a combinar múltiplos eventos para alcançar automação complexa diff --git a/feature/tutorial/src/main/res/values-ru/strings.xml b/feature/tutorial/src/main/res/values-ru/strings.xml index ec14fb839..70219e700 100644 --- a/feature/tutorial/src/main/res/values-ru/strings.xml +++ b/feature/tutorial/src/main/res/values-ru/strings.xml @@ -42,12 +42,8 @@ --> - Отрицательные условия - Проверьте, что что-то не отображается перед выполнением действия - Сценарий с несколькими событиями - Научитесь комбинировать несколько событий для достижения сложной автоматизации diff --git a/feature/tutorial/src/main/res/values-uk/strings.xml b/feature/tutorial/src/main/res/values-uk/strings.xml index d67c22d7f..60779ee6a 100644 --- a/feature/tutorial/src/main/res/values-uk/strings.xml +++ b/feature/tutorial/src/main/res/values-uk/strings.xml @@ -42,12 +42,8 @@ --> - Негативні умови - Перевірте, що щось не відображається перед виконанням дії - Сценарій з кількома подіями - Навчіться комбінувати кілька подій для досягнення складної автоматизації diff --git a/feature/tutorial/src/main/res/values-zh-rCN/strings.xml b/feature/tutorial/src/main/res/values-zh-rCN/strings.xml index 745bbd488..71b324a6a 100644 --- a/feature/tutorial/src/main/res/values-zh-rCN/strings.xml +++ b/feature/tutorial/src/main/res/values-zh-rCN/strings.xml @@ -42,12 +42,8 @@ --> - 负条件 - 在执行操作之前验证某物是否未显示 - 多事件方案 - 学习如何组合多个事件以实现复杂的自动化 关闭 diff --git a/feature/tutorial/src/main/res/values-zh-rTW/strings.xml b/feature/tutorial/src/main/res/values-zh-rTW/strings.xml index dfa150dbd..919b0a64b 100644 --- a/feature/tutorial/src/main/res/values-zh-rTW/strings.xml +++ b/feature/tutorial/src/main/res/values-zh-rTW/strings.xml @@ -42,12 +42,8 @@ --> - 負條件 - 在執行操作之前驗證某物是否未顯示 - 多事件情境 - 學習如何組合多個事件以實現複雜的自動化 關閉