]> git.ktnx.net Git - mobile-ledger.git/commitdiff
working backup and restore of configuration settings
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 21 Aug 2021 15:59:29 +0000 (18:59 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 21 Aug 2021 15:59:29 +0000 (18:59 +0300)
all profile and template parameters are supported

19 files changed:
app/schemas/net.ktnx.mobileledger.db.DB/63.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/64.json [new file with mode: 0644]
app/src/main/AndroidManifest.xml
app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/ConfigReader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/ConfigWriter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java
app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java
app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java
app/src/main/java/net/ktnx/mobileledger/db/Currency.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml [new file with mode: 0644]
app/src/main/res/layout/activity_main.xml
app/src/main/res/layout/fragment_backups.xml [new file with mode: 0644]
app/src/main/res/values-bg/strings.xml
app/src/main/res/values/strings.xml

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 (file)
index 0000000..f672f70
--- /dev/null
@@ -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 (file)
index 0000000..2155828
--- /dev/null
@@ -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
index d5dbbaeb5046ee207012b9b5d8737ebb3d27dbf7..a8d20d905ba33b2b381474265ea3430482ff4a71 100644 (file)
         android:roundIcon="@drawable/app_icon_round"
         android:supportsRtl="true"
         tools:ignore="GoogleAppIndexingWarning">
+        <activity
+            android:name=".BackupsActivity"
+            android:label="@string/backups_activity_label"
+            android:theme="@style/AppTheme.default" />
         <activity
             android:name=".ui.templates.TemplatesActivity"
             android:label="@string/title_activity_templates"
diff --git a/app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java b/app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java
new file mode 100644 (file)
index 0000000..9a1f706
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<String> backupChooserLauncher;
+    private ActivityResultLauncher<String[]> 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 (file)
index 0000000..43aa83e
--- /dev/null
@@ -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 <https://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..a0d66a2
--- /dev/null
@@ -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 <https://www.gnu.org/licenses/>.
+ */
+
+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<Currency> commodities = null;
+        List<Profile> profiles = null;
+        List<TemplateWithAccounts> 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<TemplateWithAccounts> 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<Profile> 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<Currency> 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<TemplateAccount> 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<TemplateWithAccounts> readTemplates(JsonReader r) throws IOException {
+        List<TemplateWithAccounts> list = new ArrayList<>();
+
+        r.beginArray();
+        while (r.peek() == JsonToken.BEGIN_OBJECT) {
+            list.add(readTemplate(r));
+        }
+        r.endArray();
+
+        return list;
+    }
+    private List<Currency> readCommodities(JsonReader r) throws IOException {
+        List<Currency> 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<Profile> readProfiles(JsonReader r) throws IOException {
+        List<Profile> 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 (file)
index 0000000..3db7ecd
--- /dev/null
@@ -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 <https://www.gnu.org/licenses/>.
+ */
+
+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<TemplateWithAccounts> 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<Currency> 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<Profile> 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();
+    }
+}
index b9d186166495985d1565dd752614467b0a14ee6e..46e86358d7ffc2d2d5ab718e7b1cd209b026ca7e 100644 (file)
@@ -43,6 +43,9 @@ public abstract class CurrencyDAO extends BaseDAO<Currency> {
     @Query("SELECT * FROM currencies")
     public abstract LiveData<List<Currency>> getAll();
 
+    @Query("SELECT * FROM currencies")
+    public abstract List<Currency> getAllSync();
+
     @Query("SELECT * FROM currencies WHERE id = :id")
     abstract LiveData<Currency> getById(long id);
 
@@ -52,6 +55,9 @@ public abstract class CurrencyDAO extends BaseDAO<Currency> {
     @Query("SELECT * FROM currencies WHERE name = :name")
     public abstract LiveData<Currency> 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")
index 77697230d60fd0e97d4c6d3bd9b756cdb54f5023..e61e1fb5a5af718764b2bae12a562c5cd54ebd7a 100644 (file)
@@ -72,6 +72,12 @@ public abstract class ProfileDAO extends BaseDAO<Profile> {
     @Query("SELECT * FROM profiles LIMIT 1")
     public abstract Profile getAnySync();
 
+    @Query("SELECT * FROM profiles WHERE uuid=:uuid")
+    public abstract LiveData<Profile> 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<Profile> list) {
index f72104b31514c5a4fbbfac813662c3588cb285f2..6188f70e702cbd026cb6c9ddd315f9f5269d32ea 100644 (file)
@@ -70,6 +70,9 @@ public abstract class TemplateHeaderDAO {
     @Query("SELECT * FROM templates WHERE id = :id")
     public abstract LiveData<TemplateHeader> getTemplate(Long id);
 
+    @Query("SELECT * FROM templates WHERE id = :id")
+    public abstract TemplateHeader getTemplateSync(Long id);
+
     public void getTemplateAsync(@NonNull Long id,
                                  @NonNull AsyncResultCallback<TemplateHeader> callback) {
         LiveData<TemplateHeader> 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<TemplateWithAccounts> getAllTemplatesWithAccountsSync();
+
     @Transaction
     public void insertSync(TemplateWithAccounts templateWithAccounts) {
         long template_id = insertSync(templateWithAccounts.header);
index 9a541e7d0ecc2142bde8b6df852ae0b01f183a0d..a9f799f867f87d10af30213cc30fc01b37f7e3c6 100644 (file)
@@ -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;
index 1350daf03cec69026355282dbc4f57474539e535..c3331ce6abde448a22d9d740bf650431f411f929 100644 (file)
@@ -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 (file)
index 0000000..1991891
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright Google Inc.
+  ~
+  ~ Licensed under the Apache License, version 2.0 ("the License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the license at:
+  ~
+  ~ https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distribution under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  ~ Modified/adapted by Damyan Ivanov for MoLe
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?colorPrimary"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    >
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z"
+        />
+</vector>
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 (file)
index 0000000..04dc3a5
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright Google Inc.
+  ~
+  ~ Licensed under the Apache License, version 2.0 ("the License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the license at:
+  ~
+  ~ https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distribution under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  ~ Modified/adapted by Damyan Ivanov for MoLe
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?colorPrimary"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    >
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"
+        />
+</vector>
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 (file)
index 0000000..b006f61
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright Google Inc.
+  ~
+  ~ Licensed under the Apache License, version 2.0 ("the License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the license at:
+  ~
+  ~ https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distribution under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  ~ Modified/adapted by Damyan Ivanov for MoLe
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?colorPrimary"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    >
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"
+        />
+</vector>
index fcbafd50713054c77454a04a1d4f900733bcce4c..819f9f241334038dfc9fb035a3e0af9dd205f6ce 100644 (file)
                         android:elevation="2dp"
                         android:orientation="vertical"
                         android:showDividers="beginning"
-                        android:visibility="gone"
+                        android:visibility="visible"
                         app:layout_constraintBottom_toBottomOf="parent"
                         >
 
                             android:layout_weight="1"
                             android:text="@string/action_settings"
                             app:drawableStartCompat="@drawable/ic_settings_black_24dp"
+                            android:visibility="gone"
+                            />
+                        <TextView
+                            android:id="@+id/nav_backup_restore"
+                            style="@style/nav_button"
+                            android:layout_weight="1"
+                            android:text="@string/action_import_export"
+                            app:drawableStartCompat="@drawable/ic_baseline_backup_24"
                             />
 
                     </LinearLayout>
diff --git a/app/src/main/res/layout/fragment_backups.xml b/app/src/main/res/layout/fragment_backups.xml
new file mode 100644 (file)
index 0000000..89344af
--- /dev/null
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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 <https://www.gnu.org/licenses/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".BackupsActivity"
+    >
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true"
+        android:theme="@style/AppTheme.AppBarOverlay"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        >
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="?attr/colorPrimary"
+            app:layout_collapseMode="pin"
+            app:popupTheme="@style/AppTheme.PopupOverlay"
+            />
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <ScrollView
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/appbar"
+        >
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
+            >
+            <TextView
+                android:id="@+id/backup_header"
+                style="@style/TextAppearance.MaterialComponents.Headline4"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:text="@string/backup_header"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                />
+            <TextView
+                android:id="@+id/backup_explanation_text"
+                style="@style/TextAppearance.MaterialComponents.Body1"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:text="@string/backup_explanation"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/backup_header"
+                />
+            <TextView
+                android:id="@+id/backup_button"
+                style="@style/TextAppearance.MaterialComponents.Button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="@dimen/text_margin"
+                android:drawablePadding="@dimen/text_margin"
+                android:gravity="center_vertical"
+                android:text="@string/backup_button_label"
+                app:drawableStartCompat="@drawable/ic_baseline_backup_24"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="1"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/backup_explanation_text"
+                />
+            <TextView
+                android:id="@+id/restore_header"
+                style="@style/TextAppearance.MaterialComponents.Headline4"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/text_margin"
+                android:text="@string/restore_header"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/backup_button"
+                />
+            <TextView
+                android:id="@+id/restore_explanation_text"
+                style="@style/TextAppearance.MaterialComponents.Body1"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:text="@string/restore_explanation"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/restore_header"
+                />
+            <TextView
+                android:id="@+id/restore_button"
+                style="@style/TextAppearance.MaterialComponents.Button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="@dimen/text_margin"
+                android:drawablePadding="@dimen/text_margin"
+                android:gravity="center_vertical"
+                android:text="@string/restore_button_label"
+                app:drawableStartCompat="@drawable/ic_baseline_restore_24"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="1"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/restore_explanation_text"
+                />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </ScrollView>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index 9b49a7fe13538ea66e3d354ed386a5eb28ad97c4..390513205f8f4d24e8c54a05dbb52c869b914ce5 100644 (file)
     <string name="template_details_template_params_label">Параметри на макета</string>
     <string name="template_params_help_description">Помощна информация за пааметрите на макета</string>
     <string name="account_currency_source_label">Източник на валутата</string>
+    <string name="action_import_export">Резервно копие</string>
+    <string name="backup_header">Резервно копие</string>
+    <string name="backup_button_label">Създаване</string>
+    <string name="restore_header">Възстановяване от резервно копие</string>
+    <string name="restore_button_label">Възстановяване</string>
+    <string name="backups_activity_label">Резервно копие</string>
+    <string name="backup_explanation">Записване на данните за профили и макети във файл във формат JSON, който може да се използва за възстановяване на настройките на друго устройство или след нулиране. Данните за трансакциите и сметките не се записват, а следва да бъдат изтеглени от сървъра при възстановяване на настройките.</string>
+    <string name="config_saved">Настройките са записани</string>
+    <string name="restore_explanation">Зареждане на настройките на профилите и макетите от резервно копие, създадено по-рано. Съществуващите записи не се променят. Ако искате да върнете някой запис (профил или макет) към състоянитето от резервното копие, първо го изтрийте.</string>
+    <string name="config_restored">Успешно възстановяване на настройките</string>
 </resources>
index 49b483a019041d00a478e6a9a7d061fd096c8bc1..3d7d8e691e72880ad88c7a4596ae6a5900370fc9 100644 (file)
     <string name="template_details_template_params_label">Template parameters</string>
     <string name="template_params_help_description">Show help on template parameters</string>
     <string name="account_currency_source_label">Commodity source</string>
+    <string name="action_import_export">Backup/Restore</string>
+    <string name="backup_header">Backup</string>
+    <string name="backup_explanation">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.</string>
+    <string name="backup_button_label">Backup</string>
+    <string name="restore_header">Restore</string>
+    <string name="restore_explanation">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.</string>
+    <string name="restore_button_label">Restore</string>
+    <string name="config_saved">Configuration saved successfully</string>
+    <string name="backups_activity_label">Backup / Restore</string>
+    <string name="config_restored">Configuration restored successfully</string>
 </resources>