diff --git a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/manager/OverlayManager.kt b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/manager/OverlayManager.kt
index 3140ae3f6..35d179e07 100644
--- a/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/manager/OverlayManager.kt
+++ b/core/common/overlays/src/main/java/com/buzbuz/smartautoclicker/core/common/overlays/manager/OverlayManager.kt
@@ -343,7 +343,7 @@ class OverlayManager @Inject internal constructor(
}
private fun onOrientationChanged() {
- overlayBackStack.forEachReversed { it.changeOrientation() }
+ overlayBackStack.forEach { it.changeOrientation() }
}
override fun dump(writer: PrintWriter, prefix: CharSequence) {
diff --git a/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInput.kt b/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInput.kt
index f57e1d86b..3060019bf 100644
--- a/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInput.kt
+++ b/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInput.kt
@@ -21,7 +21,6 @@ import android.text.InputType
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes
-import androidx.core.widget.doAfterTextChanged
import com.buzbuz.smartautoclicker.core.ui.R
import com.buzbuz.smartautoclicker.core.ui.databinding.IncludeFieldTextInputBinding
@@ -34,11 +33,13 @@ fun IncludeFieldTextInputBinding.setLabel(@StringRes labelResId: Int) {
fun IncludeFieldTextInputBinding.setText(text: String?, type: Int = InputType.TYPE_CLASS_TEXT) {
textField.apply {
- if (hasFocus()) return
+ val initialized = tag as? Boolean ?: false
+ if (initialized && hasFocus()) return
inputType = type
imeOptions = EditorInfo.IME_ACTION_DONE
setText(text)
+ tag = true
}
}
@@ -51,5 +52,7 @@ fun IncludeFieldTextInputBinding.setError(@StringRes messageId: Int, isError: Bo
}
fun IncludeFieldTextInputBinding.setOnTextChangedListener(listener: (Editable) -> Unit) {
- textField.addTextChangedListener(OnAfterTextChangedListener(listener))
+ textField.addTextChangedListener(OnAfterTextChangedListener { editable ->
+ if (textField.hasFocus()) listener(editable)
+ })
}
\ No newline at end of file
diff --git a/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInputWithCheckbox.kt b/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInputWithCheckbox.kt
index 1095d320a..8c4454718 100644
--- a/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInputWithCheckbox.kt
+++ b/core/common/ui/src/main/java/com/buzbuz/smartautoclicker/core/ui/bindings/fields/FieldTextInputWithCheckbox.kt
@@ -42,21 +42,25 @@ fun IncludeFieldTextInputWithCheckboxBinding.setup(
fun IncludeFieldTextInputWithCheckboxBinding.setNumericValue(value: String, force: Boolean = false) {
textField.apply {
- if (hasFocus() && !force) return
+ val initialized = tag as? Boolean ?: false
+ if (initialized && hasFocus() && !force) return
inputType = InputType.TYPE_CLASS_NUMBER
imeOptions = EditorInfo.IME_ACTION_DONE
textField.setText(value)
+ tag = true
}
}
fun IncludeFieldTextInputWithCheckboxBinding.setTextValue(value: String?, force: Boolean = false) {
textField.apply {
- if (hasFocus() && !force) return
+ val initialized = tag as? Boolean ?: false
+ if (initialized && hasFocus() && !force) return
inputType = InputType.TYPE_CLASS_TEXT
imeOptions = EditorInfo.IME_ACTION_DONE
textField.setText(value)
+ tag = true
}
}
@@ -83,7 +87,9 @@ fun IncludeFieldTextInputWithCheckboxBinding.setError(@StringRes messageId: Int,
}
fun IncludeFieldTextInputWithCheckboxBinding.setOnTextChangedListener(listener: (Editable) -> Unit) {
- textField.addTextChangedListener(OnAfterTextChangedListener(listener))
+ textField.addTextChangedListener(OnAfterTextChangedListener { editable ->
+ if (textField.hasFocus()) listener(editable)
+ })
}
fun IncludeFieldTextInputWithCheckboxBinding.setOnCheckboxClickedListener(listener: () -> Unit) {
diff --git a/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.ClickDatabase/22.json b/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.ClickDatabase/22.json
new file mode 100644
index 000000000..30306dd7d
--- /dev/null
+++ b/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.ClickDatabase/22.json
@@ -0,0 +1,852 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 22,
+ "identityHash": "48541db7df2f3f78c532dd01aa6c6b62",
+ "entities": [
+ {
+ "tableName": "action_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `clickPositionType` TEXT, `x` INTEGER, `y` INTEGER, `clickOnConditionId` INTEGER, `pressDuration` INTEGER, `clickOffsetX` INTEGER, `clickOffsetY` INTEGER, `fromX` INTEGER, `fromY` INTEGER, `toX` INTEGER, `toY` INTEGER, `swipeDuration` INTEGER, `pauseDuration` INTEGER, `isAdvanced` INTEGER, `isBroadcast` INTEGER, `intent_action` TEXT, `component_name` TEXT, `flags` INTEGER, `toggle_all` INTEGER, `toggle_all_type` TEXT, `counter_name` TEXT, `counter_operation` TEXT, `counter_operation_value_type` TEXT, `counter_operation_value` REAL, `counter_operation_counter_name` TEXT, `notification_message_text` TEXT, `notification_importance` INTEGER, `system_action_type` TEXT, `text_value` TEXT, `text_validate_input` INTEGER, FOREIGN KEY(`eventId`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`clickOnConditionId`) REFERENCES `condition_table`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickPositionType",
+ "columnName": "clickPositionType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "x",
+ "columnName": "x",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "y",
+ "columnName": "y",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOnConditionId",
+ "columnName": "clickOnConditionId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "pressDuration",
+ "columnName": "pressDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOffsetX",
+ "columnName": "clickOffsetX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOffsetY",
+ "columnName": "clickOffsetY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "fromX",
+ "columnName": "fromX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "fromY",
+ "columnName": "fromY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toX",
+ "columnName": "toX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toY",
+ "columnName": "toY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "swipeDuration",
+ "columnName": "swipeDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "pauseDuration",
+ "columnName": "pauseDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isAdvanced",
+ "columnName": "isAdvanced",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isBroadcast",
+ "columnName": "isBroadcast",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "intentAction",
+ "columnName": "intent_action",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "componentName",
+ "columnName": "component_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toggleAll",
+ "columnName": "toggle_all",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toggleAllType",
+ "columnName": "toggle_all_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterName",
+ "columnName": "counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperation",
+ "columnName": "counter_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValueType",
+ "columnName": "counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValue",
+ "columnName": "counter_operation_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "counterOperationCounterName",
+ "columnName": "counter_operation_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "notificationMessageText",
+ "columnName": "notification_message_text",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "notificationImportance",
+ "columnName": "notification_importance",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "systemActionType",
+ "columnName": "system_action_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textValue",
+ "columnName": "text_value",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textValidateInput",
+ "columnName": "text_validate_input",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_action_table_eventId",
+ "unique": false,
+ "columnNames": [
+ "eventId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_action_table_eventId` ON `${TABLE_NAME}` (`eventId`)"
+ },
+ {
+ "name": "index_action_table_clickOnConditionId",
+ "unique": false,
+ "columnNames": [
+ "clickOnConditionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_action_table_clickOnConditionId` ON `${TABLE_NAME}` (`clickOnConditionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "eventId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "condition_table",
+ "onDelete": "SET NULL",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "clickOnConditionId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "event_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scenario_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `operator` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `enabled_on_start` INTEGER NOT NULL DEFAULT 1, `type` TEXT NOT NULL, `keep_detecting` INTEGER, `detecetion_cooldown_ms` INTEGER, FOREIGN KEY(`scenario_id`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenario_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conditionOperator",
+ "columnName": "operator",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabledOnStart",
+ "columnName": "enabled_on_start",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keepDetecting",
+ "columnName": "keep_detecting",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionCooldownMs",
+ "columnName": "detecetion_cooldown_ms",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_event_table_scenario_id",
+ "unique": false,
+ "columnNames": [
+ "scenario_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_table_scenario_id` ON `${TABLE_NAME}` (`scenario_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenario_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "scenario_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `detection_quality` INTEGER NOT NULL, `compute_rate` REAL NOT NULL DEFAULT 0.0, `randomize` INTEGER NOT NULL DEFAULT 0, `keep_screen_on` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectionQuality",
+ "columnName": "detection_quality",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "computeRate",
+ "columnName": "compute_rate",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "randomize",
+ "columnName": "randomize",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "keepScreenOn",
+ "columnName": "keep_screen_on",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "condition_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `priority` INTEGER NOT NULL DEFAULT 0, `shouldBeDetected` INTEGER, `path` TEXT, `area_left` INTEGER, `area_top` INTEGER, `area_right` INTEGER, `area_bottom` INTEGER, `threshold` INTEGER, `detection_type` INTEGER, `detection_area_left` INTEGER, `detection_area_top` INTEGER, `detection_area_right` INTEGER, `detection_area_bottom` INTEGER, `broadcast_action` TEXT, `counter_name` TEXT, `counter_comparison_operation` TEXT, `counter_operation_value_type` TEXT, `counter_value` REAL, `counter_value_counter_name` TEXT, `timer_value_ms` INTEGER, `timer_restart_when_reached` INTEGER, `color_rgba` INTEGER, `number_counter_comparison_operation` TEXT, `number_counter_operation_value_type` TEXT, `number_counter_value` REAL, `number_counter_value_counter_name` TEXT, `text_to_detect` TEXT, `text_alphabet` TEXT, FOREIGN KEY(`eventId`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "shouldBeDetected",
+ "columnName": "shouldBeDetected",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "areaLeft",
+ "columnName": "area_left",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaTop",
+ "columnName": "area_top",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaRight",
+ "columnName": "area_right",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaBottom",
+ "columnName": "area_bottom",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "threshold",
+ "columnName": "threshold",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionType",
+ "columnName": "detection_type",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaLeft",
+ "columnName": "detection_area_left",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaTop",
+ "columnName": "detection_area_top",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaRight",
+ "columnName": "detection_area_right",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaBottom",
+ "columnName": "detection_area_bottom",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "broadcastAction",
+ "columnName": "broadcast_action",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterName",
+ "columnName": "counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterComparisonOperation",
+ "columnName": "counter_comparison_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValueType",
+ "columnName": "counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterValue",
+ "columnName": "counter_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "counterOperationCounterName",
+ "columnName": "counter_value_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "timerValueMs",
+ "columnName": "timer_value_ms",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "restartWhenReached",
+ "columnName": "timer_restart_when_reached",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "colorRgba",
+ "columnName": "color_rgba",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "numberCounterComparisonOperation",
+ "columnName": "number_counter_comparison_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "numberCounterOperationValueType",
+ "columnName": "number_counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "numberCounterValue",
+ "columnName": "number_counter_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "numberCounterOperationCounterName",
+ "columnName": "number_counter_value_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textToDetect",
+ "columnName": "text_to_detect",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textAlphabet",
+ "columnName": "text_alphabet",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_condition_table_eventId",
+ "unique": false,
+ "columnNames": [
+ "eventId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_condition_table_eventId` ON `${TABLE_NAME}` (`eventId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "eventId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "intent_extra_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `action_id` INTEGER NOT NULL, `type` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, FOREIGN KEY(`action_id`) REFERENCES `action_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actionId",
+ "columnName": "action_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_intent_extra_table_action_id",
+ "unique": false,
+ "columnNames": [
+ "action_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_intent_extra_table_action_id` ON `${TABLE_NAME}` (`action_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "action_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "action_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "event_toggle_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `action_id` INTEGER NOT NULL, `toggle_type` TEXT NOT NULL, `toggle_event_id` INTEGER NOT NULL, FOREIGN KEY(`action_id`) REFERENCES `action_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`toggle_event_id`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actionId",
+ "columnName": "action_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "toggle_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "toggleEventId",
+ "columnName": "toggle_event_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_event_toggle_table_action_id",
+ "unique": false,
+ "columnNames": [
+ "action_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_toggle_table_action_id` ON `${TABLE_NAME}` (`action_id`)"
+ },
+ {
+ "name": "index_event_toggle_table_toggle_event_id",
+ "unique": false,
+ "columnNames": [
+ "toggle_event_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_toggle_table_toggle_event_id` ON `${TABLE_NAME}` (`toggle_event_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "action_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "action_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "toggle_event_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "scenario_usage_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scenario_id` INTEGER NOT NULL, `last_start_timestamp_ms` INTEGER NOT NULL, `start_count` INTEGER NOT NULL, FOREIGN KEY(`scenario_id`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenario_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStartTimestampMs",
+ "columnName": "last_start_timestamp_ms",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startCount",
+ "columnName": "start_count",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_scenario_usage_table_scenario_id",
+ "unique": false,
+ "columnNames": [
+ "scenario_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_scenario_usage_table_scenario_id` ON `${TABLE_NAME}` (`scenario_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenario_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "counters_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`counterName` TEXT NOT NULL, `scenarioId` INTEGER NOT NULL, `startingValue` REAL NOT NULL, PRIMARY KEY(`counterName`, `scenarioId`), FOREIGN KEY(`scenarioId`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "counterName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenarioId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startingValue",
+ "columnName": "startingValue",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "counterName",
+ "scenarioId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_counters_table_scenarioId",
+ "unique": false,
+ "columnNames": [
+ "scenarioId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_counters_table_scenarioId` ON `${TABLE_NAME}` (`scenarioId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenarioId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '48541db7df2f3f78c532dd01aa6c6b62')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.TutorialDatabase/22.json b/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.TutorialDatabase/22.json
new file mode 100644
index 000000000..9818c18b5
--- /dev/null
+++ b/core/smart/database/schemas/com.buzbuz.smartautoclicker.core.database.TutorialDatabase/22.json
@@ -0,0 +1,900 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 22,
+ "identityHash": "55b304fed49c9b51353bdbabafec20bf",
+ "entities": [
+ {
+ "tableName": "action_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `clickPositionType` TEXT, `x` INTEGER, `y` INTEGER, `clickOnConditionId` INTEGER, `pressDuration` INTEGER, `clickOffsetX` INTEGER, `clickOffsetY` INTEGER, `fromX` INTEGER, `fromY` INTEGER, `toX` INTEGER, `toY` INTEGER, `swipeDuration` INTEGER, `pauseDuration` INTEGER, `isAdvanced` INTEGER, `isBroadcast` INTEGER, `intent_action` TEXT, `component_name` TEXT, `flags` INTEGER, `toggle_all` INTEGER, `toggle_all_type` TEXT, `counter_name` TEXT, `counter_operation` TEXT, `counter_operation_value_type` TEXT, `counter_operation_value` REAL, `counter_operation_counter_name` TEXT, `notification_message_text` TEXT, `notification_importance` INTEGER, `system_action_type` TEXT, `text_value` TEXT, `text_validate_input` INTEGER, FOREIGN KEY(`eventId`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`clickOnConditionId`) REFERENCES `condition_table`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickPositionType",
+ "columnName": "clickPositionType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "x",
+ "columnName": "x",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "y",
+ "columnName": "y",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOnConditionId",
+ "columnName": "clickOnConditionId",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "pressDuration",
+ "columnName": "pressDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOffsetX",
+ "columnName": "clickOffsetX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "clickOffsetY",
+ "columnName": "clickOffsetY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "fromX",
+ "columnName": "fromX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "fromY",
+ "columnName": "fromY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toX",
+ "columnName": "toX",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toY",
+ "columnName": "toY",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "swipeDuration",
+ "columnName": "swipeDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "pauseDuration",
+ "columnName": "pauseDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isAdvanced",
+ "columnName": "isAdvanced",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isBroadcast",
+ "columnName": "isBroadcast",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "intentAction",
+ "columnName": "intent_action",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "componentName",
+ "columnName": "component_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toggleAll",
+ "columnName": "toggle_all",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "toggleAllType",
+ "columnName": "toggle_all_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterName",
+ "columnName": "counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperation",
+ "columnName": "counter_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValueType",
+ "columnName": "counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValue",
+ "columnName": "counter_operation_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "counterOperationCounterName",
+ "columnName": "counter_operation_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "notificationMessageText",
+ "columnName": "notification_message_text",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "notificationImportance",
+ "columnName": "notification_importance",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "systemActionType",
+ "columnName": "system_action_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textValue",
+ "columnName": "text_value",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textValidateInput",
+ "columnName": "text_validate_input",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_action_table_eventId",
+ "unique": false,
+ "columnNames": [
+ "eventId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_action_table_eventId` ON `${TABLE_NAME}` (`eventId`)"
+ },
+ {
+ "name": "index_action_table_clickOnConditionId",
+ "unique": false,
+ "columnNames": [
+ "clickOnConditionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_action_table_clickOnConditionId` ON `${TABLE_NAME}` (`clickOnConditionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "eventId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "condition_table",
+ "onDelete": "SET NULL",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "clickOnConditionId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "event_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scenario_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `operator` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `enabled_on_start` INTEGER NOT NULL DEFAULT 1, `type` TEXT NOT NULL, `keep_detecting` INTEGER, `detecetion_cooldown_ms` INTEGER, FOREIGN KEY(`scenario_id`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenario_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conditionOperator",
+ "columnName": "operator",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabledOnStart",
+ "columnName": "enabled_on_start",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keepDetecting",
+ "columnName": "keep_detecting",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionCooldownMs",
+ "columnName": "detecetion_cooldown_ms",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_event_table_scenario_id",
+ "unique": false,
+ "columnNames": [
+ "scenario_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_table_scenario_id` ON `${TABLE_NAME}` (`scenario_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenario_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "scenario_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `detection_quality` INTEGER NOT NULL, `compute_rate` REAL NOT NULL DEFAULT 0.0, `randomize` INTEGER NOT NULL DEFAULT 0, `keep_screen_on` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectionQuality",
+ "columnName": "detection_quality",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "computeRate",
+ "columnName": "compute_rate",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "randomize",
+ "columnName": "randomize",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "keepScreenOn",
+ "columnName": "keep_screen_on",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "condition_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `priority` INTEGER NOT NULL DEFAULT 0, `shouldBeDetected` INTEGER, `path` TEXT, `area_left` INTEGER, `area_top` INTEGER, `area_right` INTEGER, `area_bottom` INTEGER, `threshold` INTEGER, `detection_type` INTEGER, `detection_area_left` INTEGER, `detection_area_top` INTEGER, `detection_area_right` INTEGER, `detection_area_bottom` INTEGER, `broadcast_action` TEXT, `counter_name` TEXT, `counter_comparison_operation` TEXT, `counter_operation_value_type` TEXT, `counter_value` REAL, `counter_value_counter_name` TEXT, `timer_value_ms` INTEGER, `timer_restart_when_reached` INTEGER, `color_rgba` INTEGER, `number_counter_comparison_operation` TEXT, `number_counter_operation_value_type` TEXT, `number_counter_value` REAL, `number_counter_value_counter_name` TEXT, `text_to_detect` TEXT, `text_alphabet` TEXT, FOREIGN KEY(`eventId`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "shouldBeDetected",
+ "columnName": "shouldBeDetected",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "areaLeft",
+ "columnName": "area_left",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaTop",
+ "columnName": "area_top",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaRight",
+ "columnName": "area_right",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "areaBottom",
+ "columnName": "area_bottom",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "threshold",
+ "columnName": "threshold",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionType",
+ "columnName": "detection_type",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaLeft",
+ "columnName": "detection_area_left",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaTop",
+ "columnName": "detection_area_top",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaRight",
+ "columnName": "detection_area_right",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "detectionAreaBottom",
+ "columnName": "detection_area_bottom",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "broadcastAction",
+ "columnName": "broadcast_action",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterName",
+ "columnName": "counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterComparisonOperation",
+ "columnName": "counter_comparison_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterOperationValueType",
+ "columnName": "counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "counterValue",
+ "columnName": "counter_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "counterOperationCounterName",
+ "columnName": "counter_value_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "timerValueMs",
+ "columnName": "timer_value_ms",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "restartWhenReached",
+ "columnName": "timer_restart_when_reached",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "colorRgba",
+ "columnName": "color_rgba",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "numberCounterComparisonOperation",
+ "columnName": "number_counter_comparison_operation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "numberCounterOperationValueType",
+ "columnName": "number_counter_operation_value_type",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "numberCounterValue",
+ "columnName": "number_counter_value",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "numberCounterOperationCounterName",
+ "columnName": "number_counter_value_counter_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textToDetect",
+ "columnName": "text_to_detect",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "textAlphabet",
+ "columnName": "text_alphabet",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_condition_table_eventId",
+ "unique": false,
+ "columnNames": [
+ "eventId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_condition_table_eventId` ON `${TABLE_NAME}` (`eventId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "eventId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "intent_extra_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `action_id` INTEGER NOT NULL, `type` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, FOREIGN KEY(`action_id`) REFERENCES `action_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actionId",
+ "columnName": "action_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_intent_extra_table_action_id",
+ "unique": false,
+ "columnNames": [
+ "action_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_intent_extra_table_action_id` ON `${TABLE_NAME}` (`action_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "action_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "action_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "event_toggle_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `action_id` INTEGER NOT NULL, `toggle_type` TEXT NOT NULL, `toggle_event_id` INTEGER NOT NULL, FOREIGN KEY(`action_id`) REFERENCES `action_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`toggle_event_id`) REFERENCES `event_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "actionId",
+ "columnName": "action_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "toggle_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "toggleEventId",
+ "columnName": "toggle_event_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_event_toggle_table_action_id",
+ "unique": false,
+ "columnNames": [
+ "action_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_toggle_table_action_id` ON `${TABLE_NAME}` (`action_id`)"
+ },
+ {
+ "name": "index_event_toggle_table_toggle_event_id",
+ "unique": false,
+ "columnNames": [
+ "toggle_event_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_event_toggle_table_toggle_event_id` ON `${TABLE_NAME}` (`toggle_event_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "action_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "action_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "event_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "toggle_event_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "tutorial_success_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tutorial_index` INTEGER NOT NULL, `scenario_id` INTEGER NOT NULL, PRIMARY KEY(`tutorial_index`), FOREIGN KEY(`scenario_id`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "tutorialIndex",
+ "columnName": "tutorial_index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenario_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "tutorial_index"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_tutorial_success_table_scenario_id",
+ "unique": false,
+ "columnNames": [
+ "scenario_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_tutorial_success_table_scenario_id` ON `${TABLE_NAME}` (`scenario_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenario_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "scenario_usage_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scenario_id` INTEGER NOT NULL, `last_start_timestamp_ms` INTEGER NOT NULL, `start_count` INTEGER NOT NULL, FOREIGN KEY(`scenario_id`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenario_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStartTimestampMs",
+ "columnName": "last_start_timestamp_ms",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startCount",
+ "columnName": "start_count",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_scenario_usage_table_scenario_id",
+ "unique": false,
+ "columnNames": [
+ "scenario_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_scenario_usage_table_scenario_id` ON `${TABLE_NAME}` (`scenario_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenario_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "counters_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`counterName` TEXT NOT NULL, `scenarioId` INTEGER NOT NULL, `startingValue` REAL NOT NULL, PRIMARY KEY(`counterName`, `scenarioId`), FOREIGN KEY(`scenarioId`) REFERENCES `scenario_table`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "counterName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scenarioId",
+ "columnName": "scenarioId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startingValue",
+ "columnName": "startingValue",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "counterName",
+ "scenarioId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_counters_table_scenarioId",
+ "unique": false,
+ "columnNames": [
+ "scenarioId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_counters_table_scenarioId` ON `${TABLE_NAME}` (`scenarioId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "scenario_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "scenarioId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '55b304fed49c9b51353bdbabafec20bf')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/DatabaseInfo.kt b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/DatabaseInfo.kt
index 6cd810c97..200c18125 100644
--- a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/DatabaseInfo.kt
+++ b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/DatabaseInfo.kt
@@ -40,4 +40,4 @@ internal const val COUNTERS_TABLE = "counters_table"
internal const val END_CONDITION_TABLE = "end_condition_table"
/** Current version of the database. */
-const val DATABASE_VERSION = 21
\ No newline at end of file
+const val DATABASE_VERSION = 22
\ No newline at end of file
diff --git a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/di/Hilt.kt b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/di/Hilt.kt
index 654cbca14..92e852ce9 100644
--- a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/di/Hilt.kt
+++ b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/di/Hilt.kt
@@ -24,6 +24,7 @@ import com.buzbuz.smartautoclicker.core.database.TutorialDatabase
import com.buzbuz.smartautoclicker.core.database.migrations.Migration10to11
import com.buzbuz.smartautoclicker.core.database.migrations.Migration12to13
import com.buzbuz.smartautoclicker.core.database.migrations.Migration19to20
+import com.buzbuz.smartautoclicker.core.database.migrations.Migration21to22
import com.buzbuz.smartautoclicker.core.database.migrations.Migration1to2
import com.buzbuz.smartautoclicker.core.database.migrations.Migration2to3
import com.buzbuz.smartautoclicker.core.database.migrations.Migration3to4
@@ -62,6 +63,7 @@ internal object SmartDatabaseModule {
Migration10to11,
Migration12to13,
Migration19to20,
+ Migration21to22,
).build()
@Provides
@@ -77,5 +79,6 @@ internal object SmartDatabaseModule {
Migration10to11,
Migration12to13,
Migration19to20,
+ Migration21to22,
).build()
}
\ No newline at end of file
diff --git a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt
index f5eb4c653..9af223861 100644
--- a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt
+++ b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20.kt
@@ -128,17 +128,17 @@ object Migration19to20 : Migration(19, 20) {
getSQLiteTableReference(ACTION_TABLE).apply {
forEachActionCounterMention { sourceVal1, sourceVal2, sourceVal3, scenarioId ->
if (scenarioId == null) return@forEachActionCounterMention
- sourceVal1?.let { value -> countersFound.add(value to scenarioId) }
- sourceVal2?.let { value -> countersFound.add(value to scenarioId) }
- sourceVal3?.let { value -> countersFound.add(value to scenarioId) }
+ sourceVal1?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
+ sourceVal2?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
+ sourceVal3?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
}
}
getSQLiteTableReference(CONDITION_TABLE).apply {
forEachConditionsCounterMention { sourceVal1, sourceVal2, sourceVal3, scenarioId ->
if (scenarioId == null) return@forEachConditionsCounterMention
- sourceVal1?.let { value -> countersFound.add(value to scenarioId) }
- sourceVal2?.let { value -> countersFound.add(value to scenarioId) }
- sourceVal3?.let { value -> countersFound.add(value to scenarioId) }
+ sourceVal1?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
+ sourceVal2?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
+ sourceVal3?.takeIf { it.isNotBlank() }?.let { countersFound.add(it to scenarioId) }
}
}
@@ -223,11 +223,11 @@ object Migration19to20 : Migration(19, 20) {
forEachRow(
distinct = true,
extraClause = """
- AS actions JOIN $EVENT_TABLE AS events
- ON actions.eventId = events.id
- WHERE actions.counter_name IS NOT NULL
- OR actions.counter_operation_counter_name IS NOT NULL
- OR actions.notification_message_counter_name IS NOT NULL
+ AS actions JOIN $EVENT_TABLE AS events
+ ON actions.eventId = events.id
+ WHERE (actions.counter_name IS NOT NULL AND length(trim(actions.counter_name)) > 0)
+ OR (actions.counter_operation_counter_name IS NOT NULL AND length(trim(actions.counter_operation_counter_name)) > 0)
+ OR (actions.notification_message_counter_name IS NOT NULL AND length(trim(actions.notification_message_counter_name)) > 0)
""".trimIndent(),
columnA = SQLiteColumn.Text("actions.counter_name"),
columnB = SQLiteColumn.Text("actions.counter_operation_counter_name"),
@@ -241,11 +241,11 @@ object Migration19to20 : Migration(19, 20) {
forEachRow(
distinct = true,
extraClause = """
- AS conditions JOIN $EVENT_TABLE AS events
- ON conditions.eventId = events.id
- WHERE conditions.counter_name IS NOT NULL
- OR conditions.counter_value_counter_name IS NOT NULL
- OR conditions.number_counter_value_counter_name IS NOT NULL
+ AS conditions JOIN $EVENT_TABLE AS events
+ ON conditions.eventId = events.id
+ WHERE (conditions.counter_name IS NOT NULL AND length(trim(conditions.counter_name)) > 0)
+ OR (conditions.counter_value_counter_name IS NOT NULL AND length(trim(conditions.counter_value_counter_name)) > 0)
+ OR (conditions.number_counter_value_counter_name IS NOT NULL AND length(trim(conditions.number_counter_value_counter_name)) > 0)
""".trimIndent(),
columnA = SQLiteColumn.Text("conditions.counter_name"),
columnB = SQLiteColumn.Text("conditions.counter_value_counter_name"),
diff --git a/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22.kt b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22.kt
new file mode 100644
index 000000000..0ebb3b332
--- /dev/null
+++ b/core/smart/database/src/main/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.database.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+import com.buzbuz.smartautoclicker.core.database.COUNTERS_TABLE
+
+/**
+ * Migration from database v21 to v22.
+ *
+ * Hotfix: removes blank counters (counterName is empty or whitespace-only) that were incorrectly
+ * created during Migration19to20 when a v19 database had empty-string values in counter-name
+ * columns instead of NULL.
+ */
+object Migration21to22 : Migration(21, 22) {
+
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("DELETE FROM `$COUNTERS_TABLE` WHERE length(trim(`counterName`)) = 0")
+ }
+}
diff --git a/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt b/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt
index a72bf6a14..9f38aac68 100644
--- a/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt
+++ b/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration19to20Tests.kt
@@ -193,6 +193,121 @@ class Migration19to20Tests {
}
}
+ // --- Blank counter regression tests (empty-string counter names must not create a blank counter) ---
+
+ @Test
+ fun migrate_blankCounter_notCreatedFromEmptyActionCounterName() {
+ // Given: a CHANGE_COUNTER action whose counter_name is empty string (not null)
+ // This simulates data produced by older app versions that stored "" instead of NULL.
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestEvent(1L, scenarioId)
+ db.insertTestAction(1L, 1L, type = ActionType.CHANGE_COUNTER, counterName = "")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration19to20).use { db ->
+ // Then: no blank counter must be created in the counters table
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ while (cursor.moveToNext()) {
+ val name = cursor.getString(0)
+ assertTrue("Blank counter name must not be created, found: '$name'", name.isNotEmpty())
+ }
+ }
+ }
+ }
+
+ @Test
+ fun migrate_blankCounter_notCreatedFromEmptyActionOperationCounterName() {
+ // Given: a CHANGE_COUNTER action whose counter_operation_counter_name is empty string
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestEvent(1L, scenarioId)
+ db.insertTestAction(1L, 1L, type = ActionType.CHANGE_COUNTER, counterOperationCounterName = "")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration19to20).use { db ->
+ // Then
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ while (cursor.moveToNext()) {
+ val name = cursor.getString(0)
+ assertTrue("Blank counter name must not be created, found: '$name'", name.isNotEmpty())
+ }
+ }
+ }
+ }
+
+ @Test
+ fun migrate_blankCounter_notCreatedFromEmptyNotificationCounterName() {
+ // Given: a NOTIFICATION action whose notification_message_counter_name is empty string
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestEvent(1L, scenarioId)
+ db.insertTestAction(
+ id = 1L,
+ eventId = 1L,
+ type = ActionType.NOTIFICATION,
+ notificationMessageCounterName = "",
+ )
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration19to20).use { db ->
+ // Then
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ while (cursor.moveToNext()) {
+ val name = cursor.getString(0)
+ assertTrue("Blank counter name must not be created, found: '$name'", name.isNotEmpty())
+ }
+ }
+ }
+ }
+
+ @Test
+ fun migrate_blankCounter_notCreatedFromEmptyConditionCounterName() {
+ // Given: an ON_COUNTER_REACHED condition whose counter_name is empty string
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestEvent(1L, scenarioId)
+ db.insertTestCondition(1L, 1L, type = ConditionType.ON_COUNTER_REACHED, counterName = "")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration19to20).use { db ->
+ // Then
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ while (cursor.moveToNext()) {
+ val name = cursor.getString(0)
+ assertTrue("Blank counter name must not be created, found: '$name'", name.isNotEmpty())
+ }
+ }
+ }
+ }
+
+ @Test
+ fun migrate_blankCounter_validCountersStillCreatedAlongsideEmptyNames() {
+ // Given: a scenario with one real counter and one empty-string counter name
+ val scenarioId = 1L
+ val realCounter = "MyCounter"
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestEvent(1L, scenarioId)
+ db.insertTestAction(1L, 1L, type = ActionType.CHANGE_COUNTER, counterName = realCounter)
+ db.insertTestAction(2L, 1L, type = ActionType.CHANGE_COUNTER, counterName = "")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration19to20).use { db ->
+ // Then: only the real counter exists, no blank one
+ db.assertScenarioHasCounters(scenarioId, setOf(realCounter))
+ }
+ }
+
@Test
fun migrate_counters_creation_mixed() {
// Given
diff --git a/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22Tests.kt b/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22Tests.kt
new file mode 100644
index 000000000..e8f908527
--- /dev/null
+++ b/core/smart/database/src/test/java/com/buzbuz/smartautoclicker/core/database/migrations/Migration21to22Tests.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.database.migrations
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+
+import androidx.room.testing.MigrationTestHelper
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.buzbuz.smartautoclicker.core.database.ClickDatabase
+import com.buzbuz.smartautoclicker.core.database.COUNTERS_TABLE
+import com.buzbuz.smartautoclicker.core.database.EVENT_TABLE
+import com.buzbuz.smartautoclicker.core.database.SCENARIO_TABLE
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+/** Tests the [Migration21to22]. */
+@RunWith(AndroidJUnit4::class)
+@Config(sdk = [Build.VERSION_CODES.Q])
+class Migration21to22Tests {
+
+ private companion object {
+ private const val OLD_DB_VERSION = 21
+ private const val NEW_DB_VERSION = 22
+ }
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ ClickDatabase::class.java,
+ )
+
+ private lateinit var dbPath: String
+
+ @Before
+ fun setUp() {
+ dbPath = ApplicationProvider
+ .getApplicationContext()
+ .getDatabasePath("migration-test").path
+ }
+
+ @Test
+ fun migrate_blankCounter_isRemoved() {
+ // Given: a v21 database that contains a blank counter (empty string name) left by the
+ // Migration19to20 bug.
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestCounter(scenarioId, counterName = "")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ // Then: the blank counter must no longer exist
+ db.query("SELECT COUNT(*) FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(0, cursor.getInt(0))
+ }
+ }
+ }
+
+ @Test
+ fun migrate_whitespaceOnlyCounter_isRemoved() {
+ // Given: a counter whose name is whitespace only
+ val scenarioId = 1L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestCounter(scenarioId, counterName = " ")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ // Then
+ db.query("SELECT COUNT(*) FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(0, cursor.getInt(0))
+ }
+ }
+ }
+
+ @Test
+ fun migrate_validCounter_isPreserved() {
+ // Given: a v21 database with a normally-named counter
+ val scenarioId = 1L
+ val counterName = "MyCounter"
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestCounter(scenarioId, counterName = counterName)
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ // Then: the valid counter is untouched
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(counterName, cursor.getString(0))
+ }
+ }
+ }
+
+ @Test
+ fun migrate_mixedCounters_onlyBlankRemoved() {
+ // Given: one blank counter and one valid counter in the same scenario
+ val scenarioId = 1L
+ val validName = "RealCounter"
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId)
+ db.insertTestCounter(scenarioId, counterName = "")
+ db.insertTestCounter(scenarioId, counterName = validName)
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ // Then: only the valid counter remains
+ db.query("SELECT counterName FROM $COUNTERS_TABLE WHERE scenarioId = $scenarioId").use { cursor ->
+ assertEquals(1, cursor.count)
+ assertTrue(cursor.moveToFirst())
+ assertEquals(validName, cursor.getString(0))
+ }
+ }
+ }
+
+ @Test
+ fun migrate_multipleScenarios_blankCountersRemovedFromAll() {
+ // Given: two scenarios each containing a blank and a valid counter
+ val scenarioId1 = 1L
+ val scenarioId2 = 2L
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(scenarioId1)
+ db.insertTestScenario(scenarioId2)
+ db.insertTestCounter(scenarioId1, counterName = "")
+ db.insertTestCounter(scenarioId1, counterName = "CounterA")
+ db.insertTestCounter(scenarioId2, counterName = "")
+ db.insertTestCounter(scenarioId2, counterName = "CounterB")
+ }
+
+ // When
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ // Then: each scenario retains only its valid counter
+ db.query("SELECT COUNT(*) FROM $COUNTERS_TABLE WHERE length(trim(counterName)) = 0").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(0, cursor.getInt(0))
+ }
+ db.query("SELECT COUNT(*) FROM $COUNTERS_TABLE").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(2, cursor.getInt(0))
+ }
+ }
+ }
+
+ @Test
+ fun migrate_noCounters_succeeds() {
+ // Given: a v21 database with no counters at all
+ helper.createDatabase(dbPath, OLD_DB_VERSION).use { db ->
+ db.insertTestScenario(1L)
+ }
+
+ // When / Then: migration must succeed and counters_table must stay empty
+ helper.runMigrationsAndValidate(dbPath, NEW_DB_VERSION, true, Migration21to22).use { db ->
+ db.query("SELECT COUNT(*) FROM $COUNTERS_TABLE").use { cursor ->
+ assertTrue(cursor.moveToFirst())
+ assertEquals(0, cursor.getInt(0))
+ }
+ }
+ }
+
+ // ---- Helpers ----
+
+ private fun SupportSQLiteDatabase.insertTestScenario(id: Long) {
+ insert(SCENARIO_TABLE, 0, ContentValues().apply {
+ put("id", id)
+ put("name", "Scenario $id")
+ put("detection_quality", 1200)
+ put("compute_rate", 0.0)
+ put("randomize", 0)
+ put("keep_screen_on", 0)
+ })
+ }
+
+ private fun SupportSQLiteDatabase.insertTestCounter(
+ scenarioId: Long,
+ counterName: String,
+ startingValue: Double = 0.0,
+ ) {
+ insert(COUNTERS_TABLE, 0, ContentValues().apply {
+ put("counterName", counterName)
+ put("scenarioId", scenarioId)
+ put("startingValue", startingValue)
+ })
+ }
+}
diff --git a/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/counter/Counter.kt b/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/counter/Counter.kt
index 49a72e615..210f8805b 100644
--- a/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/counter/Counter.kt
+++ b/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/counter/Counter.kt
@@ -30,5 +30,5 @@ data class Counter(
): Completable {
override fun isComplete(): Boolean =
- counterName.isNotEmpty()
+ counterName.isNotBlank()
}
\ No newline at end of file
diff --git a/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/event/EventMapper.kt b/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/event/EventMapper.kt
index 56a3f02b7..bff3c903e 100644
--- a/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/event/EventMapper.kt
+++ b/core/smart/domain/src/main/java/com/buzbuz/smartautoclicker/core/domain/model/event/EventMapper.kt
@@ -75,7 +75,7 @@ internal fun CompleteEventEntity.toDomainScreenEvent(cleanIds: Boolean = false):
enabledOnStart = event.enabledOnStart,
keepDetecting = event.keepDetecting == true,
actions = actions.map { it.toDomain(cleanIds) }.sortedByPriority().toMutableList(),
- conditions = conditions.map { it.toDomain(cleanIds) as ScreenCondition }.sortedByPriority().toMutableList(),
+ conditions = conditions.mapNotNull { it.toDomain(cleanIds) as? ScreenCondition }.sortedByPriority().toMutableList(),
cooldownMs = event.detectionCooldownMs ?: 0L,
)
@@ -88,5 +88,5 @@ internal fun CompleteEventEntity.toDomainTriggerEvent(cleanIds: Boolean = false)
conditionOperator = event.conditionOperator,
enabledOnStart = event.enabledOnStart,
actions = actions.map { it.toDomain(cleanIds) }.sortedByPriority().toMutableList(),
- conditions = conditions.map { it.toDomain(cleanIds) as TriggerCondition }.toMutableList(),
+ conditions = conditions.mapNotNull { it.toDomain(cleanIds) as? TriggerCondition }.toMutableList(),
)
\ No newline at end of file
diff --git a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/data/DetectorEngine.kt b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/data/DetectorEngine.kt
index 4be35fed2..fa36ca819 100644
--- a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/data/DetectorEngine.kt
+++ b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/data/DetectorEngine.kt
@@ -358,18 +358,6 @@ class DetectorEngine @Inject constructor(
}
}
- /** Clear this engine. It can't be used after this call. */
- internal fun clear() {
- if (_state.value != DetectorState.CREATED) {
- Log.w(TAG, "Clearing the detector but it was still started.")
- stopScreenRecord()
- }
-
- Log.i(TAG, "clear")
-
- _state.value != DetectorState.DESTROYED
- }
-
/** Process the latest images provided by the [DisplayRecorder]. */
private suspend fun processScreenImages() {
_state.emit(DetectorState.DETECTING)
@@ -448,8 +436,6 @@ internal enum class DetectorState {
RECORDING,
/** The screen is being recorded and the detection is running. */
DETECTING,
- /** The engine is destroyed and can no longer be used. */
- DESTROYED,
/** The native lib can't be loaded and the detection can't be used. */
ERROR_NATIVE_DETECTOR_LIB_NOT_FOUND,
/** The text detection models required for this scenario are not found. */
diff --git a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/SmartProcessingRepositoryImpl.kt b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/SmartProcessingRepositoryImpl.kt
index 38fdf09f2..a2ad48cee 100644
--- a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/SmartProcessingRepositoryImpl.kt
+++ b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/SmartProcessingRepositoryImpl.kt
@@ -187,11 +187,7 @@ internal class SmartProcessingRepositoryImpl @Inject constructor(
override fun stopScreenRecord() {
projectionErrorHandler = null
-
- detectorEngine.apply {
- stopScreenRecord()
- clear()
- }
+ detectorEngine.stopScreenRecord()
_scenarioId.value = null
}
diff --git a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/model/DetectionState.kt b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/model/DetectionState.kt
index f77dd6271..dcdfee42c 100644
--- a/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/model/DetectionState.kt
+++ b/core/smart/processing/src/main/java/com/buzbuz/smartautoclicker/core/processing/domain/model/DetectionState.kt
@@ -38,7 +38,6 @@ internal fun DetectorState.toDetectionState(): DetectionState? = when (this) {
DetectorState.CREATED -> DetectionState.INACTIVE
DetectorState.RECORDING -> DetectionState.RECORDING
DetectorState.DETECTING -> DetectionState.DETECTING
- DetectorState.DESTROYED -> DetectionState.INACTIVE
DetectorState.TRANSITIONING -> null // Return null to avoid notifying state change when transitioning
DetectorState.ERROR_NATIVE_DETECTOR_LIB_NOT_FOUND -> DetectionState.ERROR_NO_NATIVE_LIB
DetectorState.ERROR_OCR_MODEL_NOT_FOUND -> DetectionState.ERROR_OCR_MODEL_NOT_FOUND
diff --git a/feature/backup/src/main/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/SmartBackupDataSource.kt b/feature/backup/src/main/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/SmartBackupDataSource.kt
index c0a38e1c1..1131d2cea 100644
--- a/feature/backup/src/main/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/SmartBackupDataSource.kt
+++ b/feature/backup/src/main/java/com/buzbuz/smartautoclicker/feature/backup/data/smart/SmartBackupDataSource.kt
@@ -21,6 +21,7 @@ import android.util.Log
import com.buzbuz.smartautoclicker.core.database.DATABASE_VERSION
import com.buzbuz.smartautoclicker.core.database.entity.CompleteScenario
+import com.buzbuz.smartautoclicker.core.database.entity.ConditionEntity
import com.buzbuz.smartautoclicker.core.database.entity.ConditionType
import com.buzbuz.smartautoclicker.core.database.entity.EventType
import com.buzbuz.smartautoclicker.feature.backup.data.base.CONDITION_BACKUP_MATCH_REGEX
@@ -94,6 +95,20 @@ internal class SmartBackupDataSource(
return null
}
+ if (event.event.type == EventType.IMAGE_EVENT) {
+ if (event.conditions.find { condition -> !condition.isScreenCondition() } != null) {
+ Log.w(TAG, "Invalid scenario, condition list is invalid.")
+ return null
+ }
+ }
+
+ if (event.event.type == EventType.TRIGGER_EVENT) {
+ if (event.conditions.find { condition -> !condition.isTriggerCondition() } != null) {
+ Log.w(TAG, "Invalid scenario, condition list is invalid.")
+ return null
+ }
+ }
+
event.conditions.forEach { condition ->
if (condition.type == ConditionType.ON_IMAGE_DETECTED && (
condition.path == null || !File(appDataDir, condition.path!!).exists())) {
@@ -115,6 +130,30 @@ internal class SmartBackupDataSource(
super.reset()
screenCompatWarning = false
}
+
+ private fun ConditionEntity.isScreenCondition(): Boolean =
+ when (type) {
+ ConditionType.ON_COLOR_DETECTED,
+ ConditionType.ON_IMAGE_DETECTED,
+ ConditionType.ON_NUMBER_DETECTED,
+ ConditionType.ON_TEXT_DETECTED -> true
+
+ ConditionType.ON_COUNTER_REACHED,
+ ConditionType.ON_BROADCAST_RECEIVED,
+ ConditionType.ON_TIMER_REACHED -> false
+ }
+
+ private fun ConditionEntity.isTriggerCondition(): Boolean =
+ when (type) {
+ ConditionType.ON_COUNTER_REACHED,
+ ConditionType.ON_BROADCAST_RECEIVED,
+ ConditionType.ON_TIMER_REACHED -> true
+
+ ConditionType.ON_COLOR_DETECTED,
+ ConditionType.ON_IMAGE_DETECTED,
+ ConditionType.ON_NUMBER_DETECTED,
+ ConditionType.ON_TEXT_DETECTED -> false
+ }
}
/** Tag for logs. */
diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/data/CountersEditor.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/data/CountersEditor.kt
index 3f1d80ab5..e5d0b2700 100644
--- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/data/CountersEditor.kt
+++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/data/CountersEditor.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.update
internal class CountersEditor {
@@ -83,12 +84,15 @@ internal class CountersEditor {
}
fun updateCounter(item: Counter) {
- val currentCounters = _editedList.value ?: emptyList()
- val toBeUpdatedIndex = currentCounters.indexOf(item)
- if (toBeUpdatedIndex !in currentCounters.indices) return
-
- _editedList.value = currentCounters.toMutableList().apply {
- set(toBeUpdatedIndex, item)
+ _editedList.update { currentCounters ->
+ val toBeUpdatedIndex = currentCounters?.indexOfFirst { counter ->
+ counter.counterName == item.counterName
+ } ?: return
+ if (toBeUpdatedIndex !in currentCounters.indices) return
+
+ currentCounters.toMutableList().apply {
+ set(toBeUpdatedIndex, item)
+ }.toList()
}
}
diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigAdapter.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigAdapter.kt
index 7792aaf1a..722f3c117 100644
--- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigAdapter.kt
+++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigAdapter.kt
@@ -16,17 +16,20 @@
*/
package com.buzbuz.smartautoclicker.feature.smart.config.ui.counter.config
+import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
+import com.buzbuz.smartautoclicker.core.ui.bindings.fields.setOnTextChangedListener
+import com.buzbuz.smartautoclicker.core.ui.bindings.fields.setText
import com.buzbuz.smartautoclicker.feature.smart.config.R
import com.buzbuz.smartautoclicker.feature.smart.config.databinding.ItemCounterConfigBinding
+import com.google.android.material.textfield.TextInputLayout
import java.util.Locale
/**
@@ -66,11 +69,20 @@ class CountersConfigAdapter(
}
private object CounterDiffCallback : DiffUtil.ItemCallback() {
+
override fun areItemsTheSame(oldItem: CounterUiItem, newItem: CounterUiItem): Boolean =
oldItem.counterName == newItem.counterName
+ // Exclude user input derived values to avoid blinking while editing
override fun areContentsTheSame(oldItem: CounterUiItem, newItem: CounterUiItem): Boolean =
- oldItem == newItem
+ oldItem.isExpanded == newItem.isExpanded &&
+ oldItem.selectedForReplacement == newItem.selectedForReplacement &&
+ oldItem.setByButtonText == newItem.setByButtonText &&
+ oldItem.setByButtonIsEmpty == newItem.setByButtonIsEmpty &&
+ oldItem.readByButtonText == newItem.readByButtonText &&
+ oldItem.readByButtonIsEmpty == newItem.readByButtonIsEmpty &&
+ oldItem.deleteButtonText == newItem.deleteButtonText &&
+ oldItem.deleteButtonEnabled == newItem.deleteButtonEnabled
}
class CountersConfigViewHolder(
@@ -88,8 +100,6 @@ class CountersConfigViewHolder(
init {
binding.apply {
- layoutStartingValue.hint = root.context.getString(R.string.field_label_counter_starting_value)
-
contentLayout.setOnClickListener { item?.let(onCounterClicked) }
buttonExpandCollapse.setOnClickListener { item?.let(onExpandCollapse) }
writtenByButton.setOnClickListener { item?.let(onSetByClick) }
@@ -97,9 +107,11 @@ class CountersConfigViewHolder(
deleteButton.setOnClickListener { item?.let(onDeleteClick) }
replaceByText.setOnClickListener { onCancelReplace() }
- textFieldStartingValue.doAfterTextChanged { text ->
- val counter = item ?: return@doAfterTextChanged
- val newValue = text?.toString()?.toDoubleOrNull() ?: return@doAfterTextChanged
+ layoutStartingValue.textLayout.hint = root.context.getString(R.string.field_label_counter_starting_value)
+ layoutStartingValue.textLayout.endIconMode = TextInputLayout.END_ICON_NONE
+ layoutStartingValue.setOnTextChangedListener { text ->
+ val counter = item ?: return@setOnTextChangedListener
+ val newValue = text.toString().toDoubleOrNull() ?: return@setOnTextChangedListener
if (newValue != counter.startingValue) {
onStartingValueChange(counter, newValue)
}
@@ -115,18 +127,16 @@ class CountersConfigViewHolder(
if (newItem.isExpanded) {
buttonExpandCollapse.setIconResource(R.drawable.ic_chevron_up)
- layoutStartingValue.visibility = View.VISIBLE
+ layoutStartingValue.root.visibility = View.VISIBLE
writtenByButton.visibility = View.VISIBLE
readByButton.visibility = View.VISIBLE
deleteButton.visibility = View.VISIBLE
val startingValueText = String.format(Locale.getDefault(), "%s", newItem.startingValue)
- if (textFieldStartingValue.text.toString() != startingValueText) {
- textFieldStartingValue.setText(startingValueText)
- }
+ layoutStartingValue.setText(startingValueText, InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL)
} else {
buttonExpandCollapse.setIconResource(R.drawable.ic_chevron_down)
- layoutStartingValue.visibility = View.GONE
+ layoutStartingValue.root.visibility = View.GONE
writtenByButton.visibility = View.GONE
readByButton.visibility = View.GONE
deleteButton.visibility = View.GONE
diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigDialog.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigDialog.kt
index 7b1b40ff2..c513a5782 100644
--- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigDialog.kt
+++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/config/CountersConfigDialog.kt
@@ -61,6 +61,7 @@ class CountersConfigDialog : OverlayDialog(R.style.ScenarioConfigTheme) {
setButtonVisibility(DialogNavigationButton.DELETE, View.GONE)
setButtonVisibility(DialogNavigationButton.SAVE, View.GONE)
setButtonVisibility(DialogNavigationButton.DISMISS, View.VISIBLE)
+ buttonDismiss.setIconResource(R.drawable.ic_back)
buttonDismiss.setDebouncedOnClickListener { back() }
}
@@ -98,11 +99,6 @@ class CountersConfigDialog : OverlayDialog(R.style.ScenarioConfigTheme) {
}
}
- override fun onDestroy() {
- viewModel.saveEditions()
- super.onDestroy()
- }
-
override fun back() {
if (viewModel.getUiState() is CountersUiState.Replacing) return
super.back()
diff --git a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModel.kt b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModel.kt
index 30eb7a788..e0d5ea8c5 100644
--- a/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModel.kt
+++ b/feature/smart-config/src/main/java/com/buzbuz/smartautoclicker/feature/smart/config/ui/counter/creation/CounterCreationViewModel.kt
@@ -67,10 +67,11 @@ class CountersCreationViewModel @Inject constructor(
}
private fun toUiState(name: String?): CounterCreationUiState {
- val isAlreadyDefined = !name.isNullOrEmpty() && editionRepository.editionState.getCounter(name) != null
+ val nameIsValid = name?.isNotBlank() == true
+ val isAlreadyDefined = nameIsValid && editionRepository.editionState.getCounter(name) != null
return CounterCreationUiState(
- canBeSaved = !name.isNullOrEmpty() && !isAlreadyDefined,
+ canBeSaved = nameIsValid && !isAlreadyDefined,
nameError = isAlreadyDefined,
)
}
diff --git a/feature/smart-config/src/main/res/layout/item_counter_config.xml b/feature/smart-config/src/main/res/layout/item_counter_config.xml
index 9510e35ab..94de6072a 100644
--- a/feature/smart-config/src/main/res/layout/item_counter_config.xml
+++ b/feature/smart-config/src/main/res/layout/item_counter_config.xml
@@ -83,28 +83,15 @@
app:icon="@drawable/ic_chevron_down"
tools:visibility="visible"/>
-
-
-
-
-
+ app:layout_constraintBottom_toTopOf="@id/written_by_button"/>
{
diff --git a/smartautoclicker/src/main/res/values-ar/strings.xml b/smartautoclicker/src/main/res/values-ar/strings.xml
index 654720364..257af67e7 100644
--- a/smartautoclicker/src/main/res/values-ar/strings.xml
+++ b/smartautoclicker/src/main/res/values-ar/strings.xml
@@ -41,6 +41,7 @@
الأحدث
استخدامات
+ لم يُستخدم قط
- منذ %1$d دقيقة
- منذ %1$d دقائق
diff --git a/smartautoclicker/src/main/res/values-es/strings.xml b/smartautoclicker/src/main/res/values-es/strings.xml
index 86255a87f..a2ee602af 100644
--- a/smartautoclicker/src/main/res/values-es/strings.xml
+++ b/smartautoclicker/src/main/res/values-es/strings.xml
@@ -43,6 +43,7 @@
Reciente
Usos
+ Nunca usado
- hace %1$d min.
- hace %1$d min.
diff --git a/smartautoclicker/src/main/res/values-fr/strings.xml b/smartautoclicker/src/main/res/values-fr/strings.xml
index a4f92b7d5..f3a48dbf6 100644
--- a/smartautoclicker/src/main/res/values-fr/strings.xml
+++ b/smartautoclicker/src/main/res/values-fr/strings.xml
@@ -43,6 +43,7 @@
Récent
Utilisations
+ Jamais utilisé
- il y a %1$d min.
- il y a %1$d min.
diff --git a/smartautoclicker/src/main/res/values-it/strings.xml b/smartautoclicker/src/main/res/values-it/strings.xml
index 84afd2a24..884e62a52 100644
--- a/smartautoclicker/src/main/res/values-it/strings.xml
+++ b/smartautoclicker/src/main/res/values-it/strings.xml
@@ -44,6 +44,7 @@
Recente
Utilizzi
+ Mai utilizzato
- %1$d min. fa
- %1$d min. fa
diff --git a/smartautoclicker/src/main/res/values-ja/strings.xml b/smartautoclicker/src/main/res/values-ja/strings.xml
index 3482f7168..787d9412a 100644
--- a/smartautoclicker/src/main/res/values-ja/strings.xml
+++ b/smartautoclicker/src/main/res/values-ja/strings.xml
@@ -33,6 +33,7 @@
最近使用
使用数
+ 一度も使用されていません
- %1$d 分前
diff --git a/smartautoclicker/src/main/res/values-pt-rBR/strings.xml b/smartautoclicker/src/main/res/values-pt-rBR/strings.xml
index 2674db342..d6326305b 100644
--- a/smartautoclicker/src/main/res/values-pt-rBR/strings.xml
+++ b/smartautoclicker/src/main/res/values-pt-rBR/strings.xml
@@ -44,6 +44,7 @@
Recentes
Usos
+ Nunca usado
- há %1$d min.
- há %1$d min.
diff --git a/smartautoclicker/src/main/res/values-ru/strings.xml b/smartautoclicker/src/main/res/values-ru/strings.xml
index d191026aa..5cc5d6e3a 100644
--- a/smartautoclicker/src/main/res/values-ru/strings.xml
+++ b/smartautoclicker/src/main/res/values-ru/strings.xml
@@ -43,6 +43,7 @@
Недавний
Использования
+ Никогда не использовался
- %1$d мин. назад
- %1$d мин. назад
diff --git a/smartautoclicker/src/main/res/values-uk/strings.xml b/smartautoclicker/src/main/res/values-uk/strings.xml
index e3c2e9863..90428fd5e 100644
--- a/smartautoclicker/src/main/res/values-uk/strings.xml
+++ b/smartautoclicker/src/main/res/values-uk/strings.xml
@@ -42,6 +42,7 @@
Останні
Використання
+ Ніколи не використовувався
- %1$d хв. тому
- %1$d хв. тому
diff --git a/smartautoclicker/src/main/res/values-zh-rCN/strings.xml b/smartautoclicker/src/main/res/values-zh-rCN/strings.xml
index d4dc160e7..4171785ff 100644
--- a/smartautoclicker/src/main/res/values-zh-rCN/strings.xml
+++ b/smartautoclicker/src/main/res/values-zh-rCN/strings.xml
@@ -33,6 +33,7 @@
最近使用
次数
+ 从未使用
- %1$d 分钟前
diff --git a/smartautoclicker/src/main/res/values-zh-rTW/strings.xml b/smartautoclicker/src/main/res/values-zh-rTW/strings.xml
index 9760e042c..a9af47f84 100644
--- a/smartautoclicker/src/main/res/values-zh-rTW/strings.xml
+++ b/smartautoclicker/src/main/res/values-zh-rTW/strings.xml
@@ -33,6 +33,7 @@
最近使用
次數
+ 從未使用
- %1$d 分鐘前
diff --git a/smartautoclicker/src/main/res/values/strings.xml b/smartautoclicker/src/main/res/values/strings.xml
index 88c04cbdc..1430f4de7 100644
--- a/smartautoclicker/src/main/res/values/strings.xml
+++ b/smartautoclicker/src/main/res/values/strings.xml
@@ -45,6 +45,7 @@
Recent
Uses
+ Never used
- %1$d min. ago
- %1$d min. ago