Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<WindowInsetsAnimationCompat>,
): 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@
-->

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">ظروف سلبية</string>
<string name="item_desc_tutorial_4">تأكد من عدم عرض شيء ما قبل تنفيذ الإجراء</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">سيناريو أحداث متعددة</string>
<string name="item_desc_tutorial_5">تعرف على كيفية الجمع بين أحداث متعددة لتحقيق أتمتة معقدة</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@
-->

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">Rastrear una serie de objetivos móviles</string>
<string name="item_desc_tutorial_4">Rastrea y haz clic en una serie de objetivos móviles que se mueven</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">Clicar hasta que desaparezcan</string>
<string name="item_desc_tutorial_5">Rastrea y haz clic en objetivos móviles hasta que desaparezcan</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@
-->

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">Conditions négatives</string>
<string name="item_desc_tutorial_4">Vérifiez qu\'un élément n\'est pas affiché avant d\'exécuter une action</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">Scénario avec plusieurs événements</string>
<string name="item_desc_tutorial_5">Apprenez à combiner plusieurs événements pour automatiser des tâches complexes</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,8 @@
<string name="button_text_tutorial_next">Avanti</string>

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">Condizioni negative</string>
<string name="item_desc_tutorial_4">Verifica che qualcosa non sia visualizzato prima di eseguire un\'azione</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">Scenario con eventi multipli</string>
<string name="item_desc_tutorial_5">Scopri come combinare eventi multipli per realizzare automazioni complesse</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
<string name="message_time_left">残り時間: %1$d</string>
<string name="button_text_tutorial_start_game">ゲームを開始する</string>
<string name="button_text_tutorial_next">次</string>
<string name="item_title_tutorial_4">マイナス条件</string>
<string name="item_desc_tutorial_4">アクションを実行する前に、何かが表示されていないことを確認してください</string>
<string name="item_title_tutorial_5">複数のイベントのシナリオ</string>
<string name="item_desc_tutorial_5">複数のイベントを組み合わせて複雑な自動化を実現する方法を学びます</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@
-->

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">Condições negativas</string>
<string name="item_desc_tutorial_4">Verifique se algo não está sendo exibido antes de executar uma ação</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">Cenário de múltiplos eventos</string>
<string name="item_desc_tutorial_5">Aprenda a combinar múltiplos eventos para alcançar automação complexa</string>


<!-- Generic tutorial buttons -->
Expand Down
4 changes: 0 additions & 4 deletions feature/tutorial/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,8 @@
-->

<!-- Two still targets press when only one is visible tutorial -->
<string name="item_title_tutorial_4">Отрицательные условия</string>
<string name="item_desc_tutorial_4">Проверьте, что что-то не отображается перед выполнением действия</string>

<!-- Two moving targets press in order tutorial -->
<string name="item_title_tutorial_5">Сценарий с несколькими событиями</string>
<string name="item_desc_tutorial_5">Научитесь комбинировать несколько событий для достижения сложной автоматизации</string>


<!-- Generic tutorial buttons -->
Expand Down
Loading
Loading