Skip to content
Open
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 @@ -22,9 +22,17 @@ import com.buzbuz.smartautoclicker.core.dumb.domain.model.DumbScenario

interface LocalAccessibilityService {

fun startDumbScenario(dumbScenario: DumbScenario)
fun startSmartScenario(resultCode: Int, data: Intent, scenario: Scenario)
fun isScenarioRunning(): Boolean
fun isSmartScreenRecordActive(): Boolean
fun getSmartScenarioId(): Long?
fun getDumbScenarioId(): Long?
fun launchDumbScenario(dumbScenario: DumbScenario)
fun launchSmartScenario(resultCode: Int, data: Intent, scenario: Scenario)
fun replaceDumbScenario(dumbScenario: DumbScenario)
fun replaceSmartScenario(resultCode: Int, data: Intent, scenario: Scenario)
fun replaceSmartScenarioWithCurrentProjection(scenario: Scenario)
fun runCurrentScenario()
fun stopScenario()
fun release()

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ interface AndroidActionExecutor: Dumpable {
* due to the queuing system).
*/
fun postNotification(notificationRequest: ActionNotificationRequest)

/** Fire a named external automation event. */
fun fireExternalAction(externalActionName: String)
}

/** The maximum supported duration for a gesture. This limitation comes from Android GestureStroke API. */
const val GESTURE_DURATION_MAX_VALUE = 59_999L
const val GESTURE_DURATION_MAX_VALUE = 59_999L
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import android.util.AndroidRuntimeException
import android.util.Log

import com.buzbuz.smartautoclicker.core.common.actions.gesture.GestureExecutor
import com.buzbuz.smartautoclicker.core.common.actions.external.ExternalActionEventContract
import com.buzbuz.smartautoclicker.core.common.actions.model.ActionNotificationRequest
import com.buzbuz.smartautoclicker.core.common.actions.notification.NotificationRequestExecutor
import com.buzbuz.smartautoclicker.core.common.actions.text.TextExecutor
Expand Down Expand Up @@ -129,9 +130,19 @@ internal class AndroidActionExecutorImpl @Inject constructor(
notificationRequestExecutor.postNotification(notificationRequest)
}

override fun fireExternalAction(externalActionName: String) {
val service = accessibilityService ?: return

try {
service.sendBroadcast(ExternalActionEventContract.createRequestQueryIntent(externalActionName))
} catch (iaex: IllegalArgumentException) {
Log.w(TAG, "Can't fire external action, Intent is invalid.", iaex)
}
}

override fun dump(writer: PrintWriter, prefix: CharSequence) {
gestureExecutor.dump(writer, prefix)
}
}

private const val TAG = "ServiceActionExecutor"
private const val TAG = "ServiceActionExecutor"
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.actions.external

import android.content.Intent
import android.os.Bundle

object ExternalActionEventContract {

const val ACTION_EDIT_EVENT = "net.dinglisch.android.tasker.ACTION_EDIT_EVENT"
const val ACTION_QUERY_CONDITION = "com.twofortyfouram.locale.intent.action.QUERY_CONDITION"
const val ACTION_REQUEST_QUERY = "com.twofortyfouram.locale.intent.action.REQUEST_QUERY"

const val EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
const val EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
const val EXTRA_STRING_JSON = "com.twofortyfouram.locale.intent.extra.STRING_JSON"
const val EXTRA_STRING_ACTIVITY_CLASS_NAME = "com.twofortyfouram.locale.intent.extra.ACTIVITY"

const val RESULT_CONDITION_SATISFIED = 16
const val RESULT_CONDITION_UNSATISFIED = 17
const val RESULT_CONDITION_UNKNOWN = 18

private const val EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA =
"net.dinglisch.android.tasker.extras.PASS_THROUGH_DATA"

private const val EXTRA_FIRED_ACTION_NAME =
"com.buzbuz.smartautoclicker.extra.EXTERNAL_ACTION_NAME"

const val EVENT_CONFIGURATION_ACTIVITY_CLASS_NAME =
"com.buzbuz.smartautoclicker.feature.externallaunch.localeplugin.ui.ExternalActionEventConfigurationActivity"

fun createConfigurationResult(configurationJson: String, blurb: String): Intent =
Intent()
.putExtra(EXTRA_BUNDLE, Bundle().apply { putString(EXTRA_STRING_JSON, configurationJson) })
.putExtra(EXTRA_STRING_BLURB, blurb)

fun readConfigurationJson(intent: Intent?): String? =
intent?.getBundleExtra(EXTRA_BUNDLE)?.getString(EXTRA_STRING_JSON)

fun createRequestQueryIntent(externalActionName: String): Intent =
Intent(ACTION_REQUEST_QUERY)
.putExtra(EXTRA_STRING_ACTIVITY_CLASS_NAME, EVENT_CONFIGURATION_ACTIVITY_CLASS_NAME)
.putExtra(
EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
Bundle().apply { putString(EXTRA_FIRED_ACTION_NAME, externalActionName.trim()) },
)

fun readFiredExternalActionName(intent: Intent?): String? =
intent
?.getBundleExtra(EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA)
?.getString(EXTRA_FIRED_ACTION_NAME)
?.trim()
?.takeIf { it.isNotEmpty() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ class OverlayManager @Inject internal constructor(
fun isOverlayStackVisible(): Boolean =
getBackStackTop()?.lifecycle?.currentState?.isAtLeast(Lifecycle.State.STARTED) ?: false

/**
* @return true when a visible child overlay is open above a scenario's main menu.
*
* The root overlay is the scenario menu. Any further overlay is an editor, dialog, or other
* foreground interaction that should not be interrupted by an external scenario replacement.
*/
fun hasVisibleOverlayAboveRoot(): Boolean =
!isOverlayStackHidden() && overlayBackStack.size > 1

/** @return true when a child overlay is open above a scenario's main menu, even if the stack is hidden. */
fun hasOverlayAboveRoot(): Boolean = overlayBackStack.size > 1

/**
* Set an overlay as being shown above all overlays in the backstack.
* It will not be added to the backstack, and can be seen as "an overlay for overlays".
Expand Down Expand Up @@ -374,4 +386,4 @@ class OverlayManager @Inject internal constructor(
}

/** Tag for logs. */
private const val TAG = "OverlayManager"
private const val TAG = "OverlayManager"
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ class OverlayManagerTests {
Assert.assertEquals(mockOverlay2, overlayManager.getBackStackTop())
}

@Test
fun hasVisibleOverlayAboveRoot_returnsTrueOnlyForChildOverlays() {
overlayManager.navigateTo(mockContext, mockOverlay1)
Assert.assertFalse(overlayManager.hasVisibleOverlayAboveRoot())

overlayManager.navigateTo(mockContext, mockOverlay2)
Assert.assertTrue(overlayManager.hasVisibleOverlayAboveRoot())
}

@Test
fun stackTop_navigateUp_initial() {
overlayManager.navigateUp(mockContext)
Expand Down Expand Up @@ -234,4 +243,4 @@ class OverlayManagerTests {
}
Mockito.verifyNoMoreInteractions(mockOverlay1, mockOverlay2)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ import com.buzbuz.smartautoclicker.core.base.data.getNotificationSettingsIntent
@SuppressLint("InlinedApi")
data class PermissionPostNotification(
private val optional: Boolean = false,
val purpose: Purpose = Purpose.GENERAL,
) : Permission.Dangerous(optional), Permission.ForApiRange {

/** Explains why notifications are requested in the preparation dialog. */
enum class Purpose {
GENERAL,
EXTERNAL_LAUNCH_FALLBACK,
}

override val fromApiLvl: Int
get() = Build.VERSION_CODES.TIRAMISU

Expand All @@ -41,4 +48,4 @@ data class PermissionPostNotification(

override fun isGranted(context: Context): Boolean =
context.getSystemService(NotificationManager::class.java).areNotificationsEnabled()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,21 @@ private fun Permission.toPermissionDialogUiState(): PermissionDialogUiState =

is PermissionPostNotification -> PermissionDialogUiState(
permission = this,
titleRes = R.string.dialog_title_permission_notification,
descriptionRes = R.string.message_permission_desc_notification,
titleRes = when (purpose) {
PermissionPostNotification.Purpose.GENERAL -> R.string.dialog_title_permission_notification
PermissionPostNotification.Purpose.EXTERNAL_LAUNCH_FALLBACK ->
R.string.dialog_title_permission_launch_fallback_notification
},
descriptionRes = when (purpose) {
PermissionPostNotification.Purpose.GENERAL -> R.string.message_permission_desc_notification
PermissionPostNotification.Purpose.EXTERNAL_LAUNCH_FALLBACK ->
R.string.message_permission_desc_launch_fallback_notification
},
)

is PermissionAccessibilityService -> PermissionDialogUiState(
permission = this,
titleRes = R.string.dialog_title_permission_accessibility,
descriptionRes = R.string.message_permission_desc_accessibility,
)
}
}
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">طلب إذن</string>
<string name="button_deny">ينكر</string>

</resources>
<string name="dialog_title_permission_launch_fallback_notification">إشعارات بدء التشغيل الاحتياطية</string>
<string name="message_permission_desc_launch_fallback_notification">اختياري: يسمح لـ Klick\'r بعرض إشعار لإكمال بدء سيناريو عندما لا يتمكن تطبيق الأتمتة من فتحه مباشرةً، مثلما يكون الهاتف مقفلاً.</string>
</resources>
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">Solicitar permiso</string>
<string name="button_deny">Denegar</string>

</resources>
<string name="dialog_title_permission_launch_fallback_notification">Notificaciones de inicio alternativo</string>
<string name="message_permission_desc_launch_fallback_notification">Opcional: permite que Klick\'r muestre una notificación para terminar de iniciar un escenario cuando la aplicación de automatización no puede abrirlo directamente, por ejemplo, mientras el teléfono está bloqueado.</string>
</resources>
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">Requête de permission</string>
<string name="button_deny">Refuser</string>

</resources>
<string name="dialog_title_permission_launch_fallback_notification">Notifications de lancement de secours</string>
<string name="message_permission_desc_launch_fallback_notification">Facultatif : permet à Klick\'r d\'afficher une notification pour terminer le lancement d\'un scénario lorsque l\'application d\'automatisation ne peut pas l\'ouvrir directement, par exemple lorsque le téléphone est verrouillé.</string>
</resources>
2 changes: 2 additions & 0 deletions core/common/permissions/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">Richiesta di permesso</string>
<string name="button_deny">Negare</string>

<string name="dialog_title_permission_launch_fallback_notification">Notifiche di avvio alternative</string>
<string name="message_permission_desc_launch_fallback_notification">Facoltativo: consente a Klick\'r di mostrare una notifica per completare l\'avvio di uno scenario quando l\'app di automazione non può aprirlo direttamente, ad esempio mentre il telefono è bloccato.</string>
</resources>
2 changes: 2 additions & 0 deletions core/common/permissions/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@

<string name="button_request_permission">権限をリクエスト</string>
<string name="button_deny">拒否</string>
<string name="dialog_title_permission_launch_fallback_notification">起動時の代替通知</string>
<string name="message_permission_desc_launch_fallback_notification">任意:端末がロック中など、オートメーションアプリがシナリオを直接開けないときに、Klick\'r が起動を完了するための通知を表示できるようにします。</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">Solicitar permissão</string>
<string name="button_deny">Negar</string>

<string name="dialog_title_permission_launch_fallback_notification">Notificações de inicialização alternativa</string>
<string name="message_permission_desc_launch_fallback_notification">Opcional: permite que o Klick\'r mostre uma notificação para concluir a inicialização de um cenário quando o aplicativo de automação não puder abri-lo diretamente, como quando o telefone estiver bloqueado.</string>
</resources>
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@
<string name="button_request_permission">Запросить разрешение</string>
<string name="button_deny">Отклонить</string>

</resources>
<string name="dialog_title_permission_launch_fallback_notification">Резервные уведомления о запуске</string>
<string name="message_permission_desc_launch_fallback_notification">Необязательно: позволяет Klick\'r показать уведомление, чтобы завершить запуск сценария, когда приложение автоматизации не может открыть его напрямую, например когда телефон заблокирован.</string>
</resources>
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values-uk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@
<string name="button_request_permission">Запросити дозвіл</string>
<string name="button_deny">Відхилити</string>

</resources>
<string name="dialog_title_permission_launch_fallback_notification">Сповіщення для резервного запуску</string>
<string name="message_permission_desc_launch_fallback_notification">Необов\'язково: дозволяє Klick\'r показати сповіщення для завершення запуску сценарію, коли застосунок автоматизації не може відкрити його безпосередньо, наприклад коли телефон заблоковано.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@

<string name="button_request_permission">请求权限</string>
<string name="button_deny">拒绝</string>
<string name="dialog_title_permission_launch_fallback_notification">备用启动通知</string>
<string name="message_permission_desc_launch_fallback_notification">可选:当自动化应用无法直接打开场景(例如手机已锁定)时,允许 Klick\'r 显示通知以完成启动。</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@

<string name="button_request_permission">請求權限</string>
<string name="button_deny">拒絕</string>
<string name="dialog_title_permission_launch_fallback_notification">備用啟動通知</string>
<string name="message_permission_desc_launch_fallback_notification">選用:當自動化應用程式無法直接開啟情境(例如手機已鎖定)時,允許 Klick\'r 顯示通知以完成啟動。</string>
</resources>
4 changes: 3 additions & 1 deletion core/common/permissions/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
-->
<string name="dialog_title_permission_accessibility">Accessibility service</string>
<string name="dialog_title_permission_notification">Notification</string>
<string name="dialog_title_permission_launch_fallback_notification">Launch notifications</string>
<string name="dialog_title_permission_mandatory_denied">Permission denied</string>
<string name="dialog_title_permission_overlay">Overlay</string>

Expand All @@ -39,6 +40,7 @@
service permission, click on the button bellow.</string>
<string name="message_permission_desc_notification">Optional: Shows the notification while the application is running in order
to easily return to the scenario selection screen.</string>
<string name="message_permission_desc_launch_fallback_notification">Optional: lets Klick\'r offer a tap-to-launch notification when Android blocks an automatic launch.</string>
<string name="message_permission_mandatory_denied">This permission is mandatory for Klick\'r.
It won\'t be able to work correctly without it.</string>

Expand All @@ -49,4 +51,4 @@
<string name="button_request_permission">Request permission</string>
<string name="button_deny">Deny</string>

</resources>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ class AccessibilityTroubleshootingDialog : DialogFragment() {
}

private fun showDontKillMyApp() {
context?.safeStartWebBrowserActivity("https://dontkillmyapp.com?app=Klick%27r")
context?.safeStartWebBrowserActivity("https://dontkillmyapp.com/?app=Klick%27r")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.
*/
package com.buzbuz.smartautoclicker.core.common.quality.ui

import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.buzbuz.smartautoclicker.core.base.extensions.safeStartWebBrowserActivity
import com.buzbuz.smartautoclicker.core.common.quality.databinding.DialogAccessibilityTroubleshootingBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/** A generic, Don’t Kill My App-style explanation for an Android background-launch failure. */
class BackgroundLaunchTroubleshootingDialog : DialogFragment() {

companion object {
const val FRAGMENT_TAG = "BackgroundLaunchTroubleshootingDialog"

private const val ARG_TITLE = "title"
private const val ARG_MESSAGE = "message"
private const val ARG_HELP_URL = "help_url"

fun newInstance(title: String, message: String, helpUrl: String) =
BackgroundLaunchTroubleshootingDialog().apply {
arguments = Bundle().apply {
putString(ARG_TITLE, title)
putString(ARG_MESSAGE, message)
putString(ARG_HELP_URL, helpUrl)
}
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val args = requireArguments()
val binding = DialogAccessibilityTroubleshootingBinding.inflate(layoutInflater).apply {
titlePermission.text = args.getString(ARG_TITLE)
descPermission.text = args.getString(ARG_MESSAGE)
buttonOpenWebsite.setOnClickListener {
context?.safeStartWebBrowserActivity(args.getString(ARG_HELP_URL).orEmpty())
}
buttonUnderstood.setOnClickListener { dismiss() }
}

return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.create()
}
}
Loading
Loading