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