From: Damyan Ivanov Date: Sat, 21 Aug 2021 15:59:29 +0000 (+0300) Subject: working backup and restore of configuration settings X-Git-Tag: v0.20.0~12 X-Git-Url: https://git.ktnx.net/?p=mobile-ledger.git;a=commitdiff_plain;h=39eca7a036963be2e6dc4e6403961177f2c5267d working backup and restore of configuration settings all profile and template parameters are supported --- diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/63.json b/app/schemas/net.ktnx.mobileledger.db.DB/63.json new file mode 100644 index 00000000..f672f70b --- /dev/null +++ b/app/schemas/net.ktnx.mobileledger.db.DB/63.json @@ -0,0 +1,867 @@ +{ + "formatVersion": 1, + "database": { + "version": 63, + "identityHash": "3a9ba5043c6e9109219046e1e29e50e1", + "entities": [ + { + "tableName": "templates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "regularExpression", + "columnName": "regular_expression", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "testText", + "columnName": "test_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionDescription", + "columnName": "transaction_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionDescriptionMatchGroup", + "columnName": "transaction_description_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "transactionComment", + "columnName": "transaction_comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionCommentMatchGroup", + "columnName": "transaction_comment_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateYear", + "columnName": "date_year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateYearMatchGroup", + "columnName": "date_year_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateMonth", + "columnName": "date_month", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateMonthMatchGroup", + "columnName": "date_month_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateDay", + "columnName": "date_day", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateDayMatchGroup", + "columnName": "date_day_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isFallback", + "columnName": "is_fallback", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "templates_uuid_idx", + "unique": true, + "columnNames": [ + "uuid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "template_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "templateId", + "columnName": "template_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "acc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountNameMatchGroup", + "columnName": "acc_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "currencyMatchGroup", + "columnName": "currency_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "amountMatchGroup", + "columnName": "amount_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountComment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountCommentMatchGroup", + "columnName": "comment_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "negateAmount", + "columnName": "negate_amount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "fk_template_accounts_template", + "unique": false, + "columnNames": [ + "template_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)" + }, + { + "name": "fk_template_accounts_currency", + "unique": false, + "columnNames": [ + "currency" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)" + } + ], + "foreignKeys": [ + { + "table": "templates", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "template_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "currencies", + "onDelete": "RESTRICT", + "onUpdate": "RESTRICT", + "columns": [ + "currency" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "currencies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasGap", + "columnName": "has_gap", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "currency_name_idx", + "unique": true, + "columnNames": [ + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameUpper", + "columnName": "name_upper", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentName", + "columnName": "parent_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "amountsExpanded", + "columnName": "amounts_expanded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_account_name", + "unique": true, + "columnNames": [ + "profile_id", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)" + }, + { + "name": "fk_account_profile", + "unique": false, + "columnNames": [ + "profile_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)" + } + ], + "foreignKeys": [ + { + "table": "profiles", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "profile_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useAuthentication", + "columnName": "use_authentication", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authUser", + "columnName": "auth_user", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authPassword", + "columnName": "auth_password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNo", + "columnName": "order_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permitPosting", + "columnName": "permit_posting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "preferredAccountsFilter", + "columnName": "preferred_accounts_filter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "futureDates", + "columnName": "future_dates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "apiVersion", + "columnName": "api_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCommodityByDefault", + "columnName": "show_commodity_by_default", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultCommodity", + "columnName": "default_commodity", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCommentsByDefault", + "columnName": "show_comments_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "detectedVersionPre_1_19", + "columnName": "detected_version_pre_1_19", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "detectedVersionMajor", + "columnName": "detected_version_major", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "detectedVersionMinor", + "columnName": "detected_version_minor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "profiles_uuid_idx", + "unique": true, + "columnNames": [ + "uuid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "options", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profile_id", + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_values", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_account_values", + "unique": true, + "columnNames": [ + "account_id", + "currency" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)" + }, + { + "name": "fk_account_value_acc", + "unique": false, + "columnNames": [ + "account_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ledgerId", + "columnName": "ledger_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataHash", + "columnName": "data_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "descriptionUpper", + "columnName": "description_uc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_transactions_ledger_id", + "unique": true, + "columnNames": [ + "profile_id", + "ledger_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)" + }, + { + "name": "idx_transaction_description", + "unique": false, + "columnNames": [ + "description" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)" + }, + { + "name": "fk_transaction_profile", + "unique": false, + "columnNames": [ + "profile_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)" + } + ], + "foreignKeys": [ + { + "table": "profiles", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "profile_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "transaction_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transaction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderNo", + "columnName": "order_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "fk_trans_acc_trans", + "unique": false, + "columnNames": [ + "transaction_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)" + }, + { + "name": "un_transaction_accounts", + "unique": true, + "columnNames": [ + "transaction_id", + "order_no" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)" + } + ], + "foreignKeys": [ + { + "table": "transactions", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "transaction_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "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, '3a9ba5043c6e9109219046e1e29e50e1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/64.json b/app/schemas/net.ktnx.mobileledger.db.DB/64.json new file mode 100644 index 00000000..21558288 --- /dev/null +++ b/app/schemas/net.ktnx.mobileledger.db.DB/64.json @@ -0,0 +1,867 @@ +{ + "formatVersion": 1, + "database": { + "version": 64, + "identityHash": "0739ea866a6aebb4217f68a7fcda5bc6", + "entities": [ + { + "tableName": "templates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "regularExpression", + "columnName": "regular_expression", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "testText", + "columnName": "test_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionDescription", + "columnName": "transaction_description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionDescriptionMatchGroup", + "columnName": "transaction_description_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "transactionComment", + "columnName": "transaction_comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transactionCommentMatchGroup", + "columnName": "transaction_comment_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateYear", + "columnName": "date_year", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateYearMatchGroup", + "columnName": "date_year_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateMonth", + "columnName": "date_month", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateMonthMatchGroup", + "columnName": "date_month_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateDay", + "columnName": "date_day", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateDayMatchGroup", + "columnName": "date_day_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isFallback", + "columnName": "is_fallback", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "templates_uuid_idx", + "unique": true, + "columnNames": [ + "uuid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "template_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "templateId", + "columnName": "template_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "acc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountNameMatchGroup", + "columnName": "acc_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "currencyMatchGroup", + "columnName": "currency_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "amountMatchGroup", + "columnName": "amount_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountComment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountCommentMatchGroup", + "columnName": "comment_match_group", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "negateAmount", + "columnName": "negate_amount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "fk_template_accounts_template", + "unique": false, + "columnNames": [ + "template_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)" + }, + { + "name": "fk_template_accounts_currency", + "unique": false, + "columnNames": [ + "currency" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)" + } + ], + "foreignKeys": [ + { + "table": "templates", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "template_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "currencies", + "onDelete": "RESTRICT", + "onUpdate": "RESTRICT", + "columns": [ + "currency" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "currencies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasGap", + "columnName": "has_gap", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "currency_name_idx", + "unique": true, + "columnNames": [ + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameUpper", + "columnName": "name_upper", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentName", + "columnName": "parent_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "amountsExpanded", + "columnName": "amounts_expanded", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_account_name", + "unique": true, + "columnNames": [ + "profile_id", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)" + }, + { + "name": "fk_account_profile", + "unique": false, + "columnNames": [ + "profile_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)" + } + ], + "foreignKeys": [ + { + "table": "profiles", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "profile_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useAuthentication", + "columnName": "use_authentication", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authUser", + "columnName": "auth_user", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authPassword", + "columnName": "auth_password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNo", + "columnName": "order_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permitPosting", + "columnName": "permit_posting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "preferredAccountsFilter", + "columnName": "preferred_accounts_filter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "futureDates", + "columnName": "future_dates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "apiVersion", + "columnName": "api_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCommodityByDefault", + "columnName": "show_commodity_by_default", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultCommodity", + "columnName": "default_commodity", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCommentsByDefault", + "columnName": "show_comments_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "detectedVersionPre_1_19", + "columnName": "detected_version_pre_1_19", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "detectedVersionMajor", + "columnName": "detected_version_major", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "detectedVersionMinor", + "columnName": "detected_version_minor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "profiles_uuid_idx", + "unique": true, + "columnNames": [ + "uuid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "options", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profile_id", + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_values", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_account_values", + "unique": true, + "columnNames": [ + "account_id", + "currency" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)" + }, + { + "name": "fk_account_value_acc", + "unique": false, + "columnNames": [ + "account_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ledgerId", + "columnName": "ledger_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profile_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dataHash", + "columnName": "data_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "descriptionUpper", + "columnName": "description_uc", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "un_transactions_ledger_id", + "unique": true, + "columnNames": [ + "profile_id", + "ledger_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)" + }, + { + "name": "idx_transaction_description", + "unique": false, + "columnNames": [ + "description" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)" + }, + { + "name": "fk_transaction_profile", + "unique": false, + "columnNames": [ + "profile_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)" + } + ], + "foreignKeys": [ + { + "table": "profiles", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "profile_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "transaction_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionId", + "columnName": "transaction_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderNo", + "columnName": "order_no", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "generation", + "columnName": "generation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "fk_trans_acc_trans", + "unique": false, + "columnNames": [ + "transaction_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)" + }, + { + "name": "un_transaction_accounts", + "unique": true, + "columnNames": [ + "transaction_id", + "order_no" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)" + } + ], + "foreignKeys": [ + { + "table": "transactions", + "onDelete": "CASCADE", + "onUpdate": "RESTRICT", + "columns": [ + "transaction_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "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, '0739ea866a6aebb4217f68a7fcda5bc6')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d5dbbaeb..a8d20d90 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,6 +31,10 @@ android:roundIcon="@drawable/app_icon_round" android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning"> + . + */ + +package net.ktnx.mobileledger; + +import android.net.Uri; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; + +import net.ktnx.mobileledger.async.ConfigReader; +import net.ktnx.mobileledger.async.ConfigWriter; +import net.ktnx.mobileledger.databinding.FragmentBackupsBinding; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class BackupsActivity extends AppCompatActivity { + private FragmentBackupsBinding b; + private ActivityResultLauncher backupChooserLauncher; + private ActivityResultLauncher restoreChooserLauncher; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + b = FragmentBackupsBinding.inflate(getLayoutInflater()); + setContentView(b.getRoot()); + + setSupportActionBar(b.toolbar); + // Show the Up button in the action bar. + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + b.backupButton.setOnClickListener(this::backupClicked); + b.restoreButton.setOnClickListener(this::restoreClicked); + + + backupChooserLauncher = + registerForActivityResult(new ActivityResultContracts.CreateDocument(), + this::storeConfig); + restoreChooserLauncher = + registerForActivityResult(new ActivityResultContracts.OpenDocument(), + this::readConfig); + } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + + return true; + } + return super.onOptionsItemSelected(item); + } + private void storeConfig(Uri result) { + if (result == null) + return; + + try { + ConfigWriter saver = + new ConfigWriter(getBaseContext(), result, new ConfigWriter.OnErrorListener() { + @Override + public void error(Exception e) { + Snackbar.make(b.backupButton, e.toString(), + BaseTransientBottomBar.LENGTH_LONG) + .show(); + } + }, new ConfigWriter.OnDoneListener() { + public void done() { + Snackbar.make(b.backupButton, R.string.config_saved, + Snackbar.LENGTH_LONG) + .show(); + } + }); + saver.start(); + } + catch (IOException e) { + e.printStackTrace(); + } + + } + private void readConfig(Uri result) { + if (result == null) + return; + + try { + ConfigReader reader = + new ConfigReader(getBaseContext(), result, new ConfigWriter.OnErrorListener() { + @Override + public void error(Exception e) { + Snackbar.make(b.backupButton, e.toString(), + BaseTransientBottomBar.LENGTH_LONG) + .show(); + } + }, new ConfigReader.OnDoneListener() { + public void done() { + Snackbar.make(b.backupButton, R.string.config_restored, + Snackbar.LENGTH_LONG) + .show(); + } + }); + reader.start(); + } + catch (IOException e) { + e.printStackTrace(); + } + + } + private void backupClicked(View view) { + final Date now = new Date(); + DateFormat df = new SimpleDateFormat("y-MM-dd HH:mm", Locale.getDefault()); + df.format(now); + backupChooserLauncher.launch(String.format("MoLe-%s.json", df.format(now))); + } + private void restoreClicked(View view) { + restoreChooserLauncher.launch(new String[]{"application/json"}); + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java b/app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java new file mode 100644 index 00000000..43aa83ea --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe 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 terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.async; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import net.ktnx.mobileledger.utils.Misc; + +import java.io.FileNotFoundException; +import java.io.IOException; + +abstract class ConfigIO extends Thread { + protected final OnErrorListener onErrorListener; + protected ParcelFileDescriptor pfd; + ConfigIO(Context context, Uri uri, OnErrorListener onErrorListener) + throws FileNotFoundException { + this.onErrorListener = onErrorListener; + pfd = context.getContentResolver() + .openFileDescriptor(uri, getStreamMode()); + + initStream(); + } + abstract protected String getStreamMode(); + + abstract protected void initStream(); + + abstract protected void processStream() throws IOException; + @Override + public void run() { + try { + processStream(); + } + catch (Exception e) { + Log.e("cfg-json", "Error processing settings as JSON", e); + if (onErrorListener != null) + Misc.onMainThread(() -> onErrorListener.error(e)); + } + finally { + try { + pfd.close(); + } + catch (Exception e) { + Log.e("cfg-json", "Error closing file descriptor", e); + } + } + } + protected static class Keys { + static final String ACCOUNTS = "accounts"; + static final String AMOUNT = "amount"; + static final String AMOUNT_GROUP = "amountGroup"; + static final String API_VER = "apiVersion"; + static final String AUTH_PASS = "authPass"; + static final String AUTH_USER = "authUser"; + static final String CAN_POST = "permitPosting"; + static final String COLOUR = "colour"; + static final String COMMENT = "comment"; + static final String COMMENT_GROUP = "commentMatchGroup"; + static final String COMMODITIES = "commodities"; + static final String CURRENCY = "commodity"; + static final String CURRENCY_GROUP = "commodityGroup"; + static final String CURRENT_PROFILE = "currentProfile"; + static final String DATE_DAY = "dateDay"; + static final String DATE_DAY_GROUP = "dateDayMatchGroup"; + static final String DATE_MONTH = "dateMonth"; + static final String DATE_MONTH_GROUP = "dateMonthMatchGroup"; + static final String DATE_YEAR = "dateYear"; + static final String DATE_YEAR_GROUP = "dateYearMatchGroup"; + static final String DEFAULT_COMMODITY = "defaultCommodity"; + static final String FUTURE_DATES = "futureDates"; + static final String HAS_GAP = "hasGap"; + static final String IS_FALLBACK = "isFallback"; + static final String NAME = "name"; + static final String NAME_GROUP = "nameMatchGroup"; + static final String NEGATE_AMOUNT = "negateAmount"; + static final String POSITION = "position"; + static final String PREF_ACCOUNT = "preferredAccountsFilter"; + static final String PROFILES = "profiles"; + static final String REGEX = "regex"; + static final String SHOW_COMMENTS = "showCommentsByDefault"; + static final String SHOW_COMMODITY = "showCommodityByDefault"; + static final String TEMPLATES = "templates"; + static final String TEST_TEXT = "testText"; + static final String TRANSACTION = "description"; + static final String TRANSACTION_GROUP = "descriptionMatchGroup"; + static final String URL = "url"; + static final String USE_AUTH = "useAuth"; + static final String UUID = "uuid"; + } + + abstract static public class OnErrorListener { + public abstract void error(Exception e); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/async/ConfigReader.java b/app/src/main/java/net/ktnx/mobileledger/async/ConfigReader.java new file mode 100644 index 00000000..a0d66a22 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/async/ConfigReader.java @@ -0,0 +1,372 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe 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 terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.async; + +import android.content.Context; +import android.net.Uri; +import android.util.JsonReader; +import android.util.JsonToken; + +import net.ktnx.mobileledger.dao.CurrencyDAO; +import net.ktnx.mobileledger.dao.ProfileDAO; +import net.ktnx.mobileledger.dao.TemplateHeaderDAO; +import net.ktnx.mobileledger.db.Currency; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; +import net.ktnx.mobileledger.db.TemplateAccount; +import net.ktnx.mobileledger.db.TemplateHeader; +import net.ktnx.mobileledger.db.TemplateWithAccounts; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.utils.Misc; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class ConfigReader extends ConfigIO { + private final OnDoneListener onDoneListener; + private JsonReader r; + public ConfigReader(Context context, Uri uri, OnErrorListener onErrorListener, + OnDoneListener onDoneListener) throws FileNotFoundException { + super(context, uri, onErrorListener); + + this.onDoneListener = onDoneListener; + } + @Override + protected String getStreamMode() { + return "r"; + } + @Override + protected void initStream() { + r = new JsonReader(new BufferedReader( + new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))); + } + @Override + protected void processStream() throws IOException { + List commodities = null; + List profiles = null; + List templates = null; + String currentProfile = null; + r.beginObject(); + while (r.hasNext()) { + String item = r.nextName(); + switch (item) { + case Keys.COMMODITIES: + commodities = readCommodities(r); + break; + case Keys.PROFILES: + profiles = readProfiles(r); + break; + case Keys.TEMPLATES: + templates = readTemplates(r); + break; + case Keys.CURRENT_PROFILE: + currentProfile = r.nextString(); + break; + default: + throw new RuntimeException("unexpected top-level item " + item); + } + } + r.endObject(); + + restoreCommodities(commodities); + restoreProfiles(profiles); + restoreTemplates(templates); + + if (Data.getProfile() == null && currentProfile != null) { + Profile p = DB.get() + .getProfileDAO() + .getByUuidSync(currentProfile); + if (p != null) + Data.setCurrentProfile(p); + } + + if (onDoneListener != null) + Misc.onMainThread(onDoneListener::done); + } + private void restoreTemplates(List list) { + if (list == null) + return; + + TemplateHeaderDAO dao = DB.get() + .getTemplateDAO(); + + for (TemplateWithAccounts t : list) { + if (dao.getTemplateWithAccountsByUuidSync(t.header.getUuid()) == null) + dao.insertSync(t); + } + } + private void restoreProfiles(List list) { + if (list == null) + return; + + ProfileDAO dao = DB.get() + .getProfileDAO(); + + for (Profile p : list) { + if (dao.getByUuidSync(p.getUuid()) == null) + dao.insert(p); + } + } + private void restoreCommodities(List list) { + if (list == null) + return; + + CurrencyDAO dao = DB.get() + .getCurrencyDAO(); + + for (Currency c : list) { + if (dao.getByNameSync(c.getName()) == null) + dao.insert(c); + } + } + private TemplateAccount readTemplateAccount(JsonReader r) throws IOException { + r.beginObject(); + TemplateAccount result = new TemplateAccount(0L, 0L, 0L); + while (r.peek() != JsonToken.END_OBJECT) { + String item = r.nextName(); + switch (item) { + case Keys.NAME: + result.setAccountName(r.nextString()); + break; + case Keys.NAME_GROUP: + result.setAccountNameMatchGroup(r.nextInt()); + break; + case Keys.COMMENT: + result.setAccountComment(r.nextString()); + break; + case Keys.COMMENT_GROUP: + result.setAccountCommentMatchGroup(r.nextInt()); + break; + case Keys.AMOUNT: + result.setAmount((float) r.nextDouble()); + break; + case Keys.AMOUNT_GROUP: + result.setAmountMatchGroup(r.nextInt()); + break; + case Keys.NEGATE_AMOUNT: + result.setNegateAmount(r.nextBoolean()); + break; + case Keys.CURRENCY: + result.setCurrency(r.nextLong()); + break; + case Keys.CURRENCY_GROUP: + result.setCurrencyMatchGroup(r.nextInt()); + break; + + default: + throw new IllegalStateException("Unexpected template account item: " + item); + } + } + r.endObject(); + + return result; + } + private TemplateWithAccounts readTemplate(JsonReader r) throws IOException { + r.beginObject(); + String name = null; + TemplateHeader t = new TemplateHeader(0L, "", ""); + List accounts = new ArrayList<>(); + + while (r.peek() != JsonToken.END_OBJECT) { + String item = r.nextName(); + switch (item) { + case Keys.UUID: + t.setUuid(r.nextString()); + break; + case Keys.NAME: + t.setName(r.nextString()); + break; + case Keys.REGEX: + t.setRegularExpression(r.nextString()); + break; + case Keys.TEST_TEXT: + t.setTestText(r.nextString()); + break; + case Keys.DATE_YEAR: + t.setDateYear(r.nextInt()); + break; + case Keys.DATE_YEAR_GROUP: + t.setDateYearMatchGroup(r.nextInt()); + break; + case Keys.DATE_MONTH: + t.setDateMonth(r.nextInt()); + break; + case Keys.DATE_MONTH_GROUP: + t.setDateMonthMatchGroup(r.nextInt()); + break; + case Keys.DATE_DAY: + t.setDateDay(r.nextInt()); + break; + case Keys.DATE_DAY_GROUP: + t.setDateDayMatchGroup(r.nextInt()); + break; + case Keys.TRANSACTION: + t.setTransactionDescription(r.nextString()); + break; + case Keys.TRANSACTION_GROUP: + t.setTransactionDescriptionMatchGroup(r.nextInt()); + break; + case Keys.COMMENT: + t.setTransactionComment(r.nextString()); + break; + case Keys.COMMENT_GROUP: + t.setTransactionCommentMatchGroup(r.nextInt()); + break; + case Keys.IS_FALLBACK: + t.setFallback(r.nextBoolean()); + break; + case Keys.ACCOUNTS: + r.beginArray(); + while (r.peek() == JsonToken.BEGIN_OBJECT) { + accounts.add(readTemplateAccount(r)); + } + r.endArray(); + break; + default: + throw new RuntimeException("Unknown template header item: " + item); + } + } + r.endObject(); + + TemplateWithAccounts result = new TemplateWithAccounts(); + result.header = t; + result.accounts = accounts; + return result; + } + private List readTemplates(JsonReader r) throws IOException { + List list = new ArrayList<>(); + + r.beginArray(); + while (r.peek() == JsonToken.BEGIN_OBJECT) { + list.add(readTemplate(r)); + } + r.endArray(); + + return list; + } + private List readCommodities(JsonReader r) throws IOException { + List list = new ArrayList<>(); + + r.beginArray(); + while (r.peek() == JsonToken.BEGIN_OBJECT) { + Currency c = new Currency(); + + r.beginObject(); + while (r.peek() != JsonToken.END_OBJECT) { + final String item = r.nextName(); + switch (item) { + case Keys.NAME: + c.setName(r.nextString()); + break; + case Keys.POSITION: + c.setPosition(r.nextString()); + break; + case Keys.HAS_GAP: + c.setHasGap(r.nextBoolean()); + break; + default: + throw new RuntimeException("Unknown commodity key: " + item); + } + } + r.endObject(); + + if (c.getName() + .isEmpty()) + throw new RuntimeException("Missing commodity name"); + + list.add(c); + } + r.endArray(); + + return list; + } + private List readProfiles(JsonReader r) throws IOException { + List list = new ArrayList<>(); + r.beginArray(); + while (r.peek() == JsonToken.BEGIN_OBJECT) { + Profile p = new Profile(); + r.beginObject(); + while (r.peek() != JsonToken.END_OBJECT) { + String item = r.nextName(); + + switch (item) { + case Keys.UUID: + p.setUuid(r.nextString()); + break; + case Keys.NAME: + p.setName(r.nextString()); + break; + case Keys.URL: + p.setUrl(r.nextString()); + break; + case Keys.USE_AUTH: + p.setUseAuthentication(r.nextBoolean()); + break; + case Keys.AUTH_USER: + p.setAuthUser(r.nextString()); + break; + case Keys.AUTH_PASS: + p.setAuthPassword(r.nextString()); + break; + case Keys.API_VER: + p.setApiVersion(r.nextInt()); + break; + case Keys.CAN_POST: + p.setPermitPosting(r.nextBoolean()); + break; + case Keys.DEFAULT_COMMODITY: + p.setDefaultCommodity(r.nextString()); + break; + case Keys.SHOW_COMMODITY: + p.setShowCommodityByDefault(r.nextBoolean()); + break; + case Keys.SHOW_COMMENTS: + p.setShowCommentsByDefault(r.nextBoolean()); + break; + case Keys.FUTURE_DATES: + p.setFutureDates(r.nextInt()); + break; + case Keys.PREF_ACCOUNT: + p.setPreferredAccountsFilter(r.nextString()); + break; + case Keys.COLOUR: + p.setTheme(r.nextInt()); + break; + + + default: + throw new IllegalStateException("Unexpected profile item: " + item); + } + } + r.endObject(); + + list.add(p); + } + r.endArray(); + + return list; + } + abstract static public class OnDoneListener { + public abstract void done(); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/async/ConfigWriter.java b/app/src/main/java/net/ktnx/mobileledger/async/ConfigWriter.java new file mode 100644 index 00000000..3db7ecd6 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/async/ConfigWriter.java @@ -0,0 +1,228 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe 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 terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.async; + +import android.content.Context; +import android.net.Uri; +import android.util.JsonWriter; + +import net.ktnx.mobileledger.db.Currency; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; +import net.ktnx.mobileledger.db.TemplateAccount; +import net.ktnx.mobileledger.db.TemplateWithAccounts; +import net.ktnx.mobileledger.json.API; +import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.utils.Misc; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.List; + +public class ConfigWriter extends ConfigIO { + private final OnDoneListener onDoneListener; + private JsonWriter w; + public ConfigWriter(Context context, Uri uri, OnErrorListener onErrorListener, + OnDoneListener onDoneListener) throws FileNotFoundException { + super(context, uri, onErrorListener); + + this.onDoneListener = onDoneListener; + } + @Override + protected String getStreamMode() { + return "w"; + } + @Override + protected void initStream() { + w = new JsonWriter(new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(pfd.getFileDescriptor())))); + w.setIndent(" "); + } + @Override + protected void processStream() throws IOException { + w.beginObject(); + writeCommodities(w); + writeProfiles(w); + writeCurrentProfile(w); + writeConfigTemplates(w); + w.endObject(); + w.flush(); + + if (onDoneListener != null) + Misc.onMainThread(onDoneListener::done); + } + private void writeKey(JsonWriter w, String key, String value) throws IOException { + if (value != null) + w.name(key) + .value(value); + } + private void writeKey(JsonWriter w, String key, Integer value) throws IOException { + if (value != null) + w.name(key) + .value(value); + } + private void writeKey(JsonWriter w, String key, Long value) throws IOException { + if (value != null) + w.name(key) + .value(value); + } + private void writeKey(JsonWriter w, String key, Float value) throws IOException { + if (value != null) + w.name(key) + .value(value); + } + private void writeKey(JsonWriter w, String key, Boolean value) throws IOException { + if (value != null) + w.name(key) + .value(value); + } + private void writeConfigTemplates(JsonWriter w) throws IOException { + List templates = DB.get() + .getTemplateDAO() + .getAllTemplatesWithAccountsSync(); + + if (templates.isEmpty()) + return; + + w.name("templates") + .beginArray(); + for (TemplateWithAccounts t : templates) { + w.name(Keys.UUID) + .value(t.header.getUuid()); + w.name(Keys.NAME) + .value(t.header.getName()); + w.name(Keys.REGEX) + .value(t.header.getRegularExpression()); + writeKey(w, Keys.TEST_TEXT, t.header.getTestText()); + writeKey(w, Keys.DATE_YEAR, t.header.getDateYear()); + writeKey(w, Keys.DATE_YEAR_GROUP, t.header.getDateYearMatchGroup()); + writeKey(w, Keys.DATE_MONTH, t.header.getDateMonth()); + writeKey(w, Keys.DATE_MONTH_GROUP, t.header.getDateMonthMatchGroup()); + writeKey(w, Keys.DATE_DAY, t.header.getDateDay()); + writeKey(w, Keys.DATE_DAY_GROUP, t.header.getDateDayMatchGroup()); + writeKey(w, Keys.TRANSACTION, t.header.getTransactionDescription()); + writeKey(w, Keys.TRANSACTION_GROUP, t.header.getTransactionDescriptionMatchGroup()); + writeKey(w, Keys.COMMENT, t.header.getTransactionComment()); + writeKey(w, Keys.COMMENT_GROUP, t.header.getTransactionCommentMatchGroup()); + w.name(Keys.IS_FALLBACK) + .value(t.header.isFallback()); + if (t.accounts.size() > 0) { + w.name(Keys.ACCOUNTS) + .beginArray(); + for (TemplateAccount a : t.accounts) { + writeKey(w, Keys.NAME, a.getAccountName()); + writeKey(w, Keys.NAME_GROUP, a.getAccountNameMatchGroup()); + writeKey(w, Keys.COMMENT, a.getAccountComment()); + writeKey(w, Keys.COMMENT_GROUP, a.getAccountCommentMatchGroup()); + writeKey(w, Keys.AMOUNT, a.getAmount()); + writeKey(w, Keys.AMOUNT_GROUP, a.getAmountMatchGroup()); + writeKey(w, Keys.NEGATE_AMOUNT, a.getNegateAmount()); + writeKey(w, Keys.CURRENCY, a.getCurrency()); + writeKey(w, Keys.CURRENCY_GROUP, a.getCurrencyMatchGroup()); + } + w.endArray(); + } + } + w.endArray(); + } + private void writeCommodities(JsonWriter w) throws IOException { + List list = DB.get() + .getCurrencyDAO() + .getAllSync(); + if (list.isEmpty()) + return; + w.name(Keys.COMMODITIES) + .beginArray(); + for (Currency c : list) { + w.beginObject(); + writeKey(w, Keys.NAME, c.getName()); + writeKey(w, Keys.POSITION, c.getPosition()); + writeKey(w, Keys.HAS_GAP, c.getHasGap()); + w.endObject(); + } + w.endArray(); + } + private void writeProfiles(JsonWriter w) throws IOException { + List profiles = DB.get() + .getProfileDAO() + .getAllOrderedSync(); + + if (profiles.isEmpty()) + return; + + w.name(Keys.PROFILES) + .beginArray(); + for (Profile p : profiles) { + w.beginObject(); + + w.name(Keys.NAME) + .value(p.getName()); + w.name(Keys.UUID) + .value(p.getUuid()); + w.name(Keys.URL) + .value(p.getUrl()); + w.name(Keys.USE_AUTH) + .value(p.useAuthentication()); + if (p.useAuthentication()) { + w.name(Keys.AUTH_USER) + .value(p.getAuthUser()); + w.name(Keys.AUTH_PASS) + .value(p.getAuthPassword()); + } + if (p.getApiVersion() != API.auto.toInt()) + w.name(Keys.API_VER) + .value(p.getApiVersion()); + w.name(Keys.CAN_POST) + .value(p.permitPosting()); + if (p.permitPosting()) { + String defaultCommodity = p.getDefaultCommodity(); + if (!defaultCommodity.isEmpty()) + w.name(Keys.DEFAULT_COMMODITY) + .value(defaultCommodity); + w.name(Keys.SHOW_COMMODITY) + .value(p.getShowCommodityByDefault()); + w.name(Keys.SHOW_COMMENTS) + .value(p.getShowCommentsByDefault()); + w.name(Keys.FUTURE_DATES) + .value(p.getFutureDates()); + w.name(Keys.PREF_ACCOUNT) + .value(p.getPreferredAccountsFilter()); + } + w.name(Keys.COLOUR) + .value(p.getTheme()); + + w.endObject(); + } + w.endArray(); + } + private void writeCurrentProfile(JsonWriter w) throws IOException { + Profile currentProfile = Data.getProfile(); + if (currentProfile == null) + return; + + w.name(Keys.CURRENT_PROFILE) + .value(currentProfile.getUuid()); + } + + abstract static public class OnDoneListener { + public abstract void done(); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java index b9d18616..46e86358 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java @@ -43,6 +43,9 @@ public abstract class CurrencyDAO extends BaseDAO { @Query("SELECT * FROM currencies") public abstract LiveData> getAll(); + @Query("SELECT * FROM currencies") + public abstract List getAllSync(); + @Query("SELECT * FROM currencies WHERE id = :id") abstract LiveData getById(long id); @@ -52,6 +55,9 @@ public abstract class CurrencyDAO extends BaseDAO { @Query("SELECT * FROM currencies WHERE name = :name") public abstract LiveData getByName(String name); + @Query("SELECT * FROM currencies WHERE name = :name") + public abstract Currency getByNameSync(String name); + // not useful for now // @Transaction // @Query("SELECT * FROM patterns") diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java index 77697230..e61e1fb5 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java @@ -72,6 +72,12 @@ public abstract class ProfileDAO extends BaseDAO { @Query("SELECT * FROM profiles LIMIT 1") public abstract Profile getAnySync(); + @Query("SELECT * FROM profiles WHERE uuid=:uuid") + public abstract LiveData getByUuid(String uuid); + + @Query("SELECT * FROM profiles WHERE uuid=:uuid") + public abstract Profile getByUuidSync(String uuid); + @Query("SELECT MAX(order_no) FROM profiles") public abstract int getProfileCountSync(); public void updateOrderSync(List list) { diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java index f72104b3..6188f70e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java @@ -70,6 +70,9 @@ public abstract class TemplateHeaderDAO { @Query("SELECT * FROM templates WHERE id = :id") public abstract LiveData getTemplate(Long id); + @Query("SELECT * FROM templates WHERE id = :id") + public abstract TemplateHeader getTemplateSync(Long id); + public void getTemplateAsync(@NonNull Long id, @NonNull AsyncResultCallback callback) { LiveData resultReceiver = getTemplate(id); @@ -93,6 +96,14 @@ public abstract class TemplateHeaderDAO { @Query("SELECT * FROM templates WHERE id = :id") public abstract TemplateWithAccounts getTemplateWithAccountsSync(@NonNull Long id); + @Transaction + @Query("SELECT * FROM templates WHERE uuid = :uuid") + public abstract TemplateWithAccounts getTemplateWithAccountsByUuidSync(String uuid); + + @Transaction + @Query("SELECT * FROM templates") + public abstract List getAllTemplatesWithAccountsSync(); + @Transaction public void insertSync(TemplateWithAccounts templateWithAccounts) { long template_id = insertSync(templateWithAccounts.header); diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Currency.java b/app/src/main/java/net/ktnx/mobileledger/db/Currency.java index 9a541e7d..a9f799f8 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/Currency.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/Currency.java @@ -35,6 +35,12 @@ public class Currency { @NonNull @ColumnInfo(name = "has_gap") private Boolean hasGap; + public Currency() { + id = 0; + name = ""; + position = "after"; + hasGap = true; + } public Currency(long id, @NonNull String name, @NonNull String position, @NonNull Boolean hasGap) { this.id = id; diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java index 1350daf0..c3331ce6 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -51,6 +51,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.snackbar.Snackbar; +import net.ktnx.mobileledger.BackupsActivity; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.async.RetrieveTransactionsTask; import net.ktnx.mobileledger.async.TransactionAccumulator; @@ -166,8 +167,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged); if (barDrawerToggle == null) { - barDrawerToggle = new ActionBarDrawerToggle(this, b.drawerLayout, b.toolbar, - R.string.navigation_drawer_open, R.string.navigation_drawer_close); + barDrawerToggle = new ActionBarDrawerToggle(this, b.drawerLayout, b.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); b.drawerLayout.addDrawerListener(barDrawerToggle); } barDrawerToggle.syncState(); @@ -176,8 +176,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa PackageInfo pi = getApplicationContext().getPackageManager() .getPackageInfo(getPackageName(), 0); ((TextView) b.navUpper.findViewById(R.id.drawer_version_text)).setText(pi.versionName); - ((TextView) b.noProfilesLayout.findViewById(R.id.drawer_version_text)).setText( - pi.versionName); + ((TextView) b.noProfilesLayout.findViewById(R.id.drawer_version_text)).setText(pi.versionName); } catch (Exception e) { e.printStackTrace(); @@ -271,10 +270,8 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa b.navProfileList.setLayoutManager(llm); b.navProfilesStartEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles()); - b.navProfilesCancelEdit.setOnClickListener( - (v) -> mProfileListAdapter.flipEditingProfiles()); - b.navProfileListHeadButtons.setOnClickListener( - (v) -> mProfileListAdapter.flipEditingProfiles()); + b.navProfilesCancelEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles()); + b.navProfileListHeadButtons.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles()); if (drawerListener == null) { drawerListener = new DrawerLayout.SimpleDrawerListener() { @Override @@ -324,6 +321,11 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa b.navAccountSummary.setOnClickListener(this::onAccountSummaryClicked); b.navLatestTransactions.setOnClickListener(this::onLatestTransactionsClicked); b.navPatterns.setOnClickListener(this::onPatternsClick); + b.navBackupRestore.setOnClickListener(this::onBackupRestoreClick); + } + private void onBackupRestoreClick(View view) { + Intent intent = new Intent(this, BackupsActivity.class); + startActivity(intent); } private void onPatternsClick(View view) { Intent intent = new Intent(this, TemplatesActivity.class); diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml new file mode 100644 index 00000000..19918915 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml new file mode 100644 index 00000000..04dc3a56 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml new file mode 100644 index 00000000..b006f612 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fcbafd50..819f9f24 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -237,7 +237,7 @@ android:elevation="2dp" android:orientation="vertical" android:showDividers="beginning" - android:visibility="gone" + android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" > @@ -247,6 +247,14 @@ android:layout_weight="1" android:text="@string/action_settings" app:drawableStartCompat="@drawable/ic_settings_black_24dp" + android:visibility="gone" + /> + diff --git a/app/src/main/res/layout/fragment_backups.xml b/app/src/main/res/layout/fragment_backups.xml new file mode 100644 index 00000000..89344af2 --- /dev/null +++ b/app/src/main/res/layout/fragment_backups.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 9b49a7fe..39051320 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -189,4 +189,14 @@ Параметри на макета Помощна информация за пааметрите на макета Източник на валутата + Резервно копие + Резервно копие + Създаване + Възстановяване от резервно копие + Възстановяване + Резервно копие + Записване на данните за профили и макети във файл във формат JSON, който може да се използва за възстановяване на настройките на друго устройство или след нулиране. Данните за трансакциите и сметките не се записват, а следва да бъдат изтеглени от сървъра при възстановяване на настройките. + Настройките са записани + Зареждане на настройките на профилите и макетите от резервно копие, създадено по-рано. Съществуващите записи не се променят. Ако искате да върнете някой запис (профил или макет) към състоянитето от резервното копие, първо го изтрийте. + Успешно възстановяване на настройките diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49b483a0..3d7d8e69 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,4 +198,14 @@ Template parameters Show help on template parameters Commodity source + Backup/Restore + Backup + Exports all profile configuration and templates to a JSON file which can later be used to restore the settings on a different device or after a device reset. Data about transactions and accounts are not exported. Instead, these are supposed to be fetched from the remote backend when the configuration is restored. + Backup + Restore + Restores all profiles and templates from a previous backup. Entries that already exist are kept without changes. If you want to restore some entry to a previous state remove it first. + Restore + Configuration saved successfully + Backup / Restore + Configuration restored successfully