]> git.ktnx.net Git - mobile-ledger.git/commitdiff
provide cloud backup functionality
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 12 Sep 2021 11:30:56 +0000 (14:30 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 12 Sep 2021 13:07:08 +0000 (16:07 +0300)
12 files changed:
app/src/main/AndroidManifest.xml
app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java
app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/ConfigReader.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/ConfigWriter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigReader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java

index a8d20d905ba33b2b381474265ea3430482ff4a71..e3ec6ee1b34b5f6eda0282e64cc4e4acb54a428e 100644 (file)
@@ -30,6 +30,7 @@
         android:networkSecurityConfig="@xml/network_security_config"
         android:roundIcon="@drawable/app_icon_round"
         android:supportsRtl="true"
+        android:backupAgent="net.ktnx.mobileledger.backup.MobileLedgerBackupAgent"
         tools:ignore="GoogleAppIndexingWarning">
         <activity
             android:name=".BackupsActivity"
index 2f81c6e43c24685fb27f88aabcfa4e9912a3ab70..242fbb567be8802a2096ae33921d29ab432c45ef 100644 (file)
@@ -32,8 +32,8 @@ 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.backup.ConfigReader;
+import net.ktnx.mobileledger.backup.ConfigWriter;
 import net.ktnx.mobileledger.databinding.FragmentBackupsBinding;
 import net.ktnx.mobileledger.model.Data;
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java b/app/src/main/java/net/ktnx/mobileledger/async/ConfigIO.java
deleted file mode 100644 (file)
index 43aa83e..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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
deleted file mode 100644 (file)
index a894f92..0000000
+++ /dev/null
@@ -1,398 +0,0 @@
-/*
- * 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();
-            if (r.peek() == JsonToken.NULL) {
-                r.nextNull();
-                continue;
-            }
-            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) {
-            Profile p = null;
-            final ProfileDAO dao = DB.get()
-                                     .getProfileDAO();
-            if (currentProfile != null)
-                p = dao.getByUuidSync(currentProfile);
-
-            if (p == null)
-                dao.getAnySync();
-
-            if (p != null)
-                Data.postCurrentProfile(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();
-            if (r.peek() == JsonToken.NULL) {
-                r.nextNull();
-                continue;
-            }
-            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();
-            if (r.peek() == JsonToken.NULL) {
-                r.nextNull();
-                continue;
-            }
-            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();
-                if (r.peek() == JsonToken.NULL) {
-                    r.nextNull();
-                    continue;
-                }
-                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();
-                if (r.peek() == JsonToken.NULL) {
-                    r.nextNull();
-                    continue;
-                }
-
-                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
deleted file mode 100644 (file)
index f72a291..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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.beginObject();
-
-            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) {
-                    w.beginObject();
-
-                    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.endObject();
-                }
-                w.endArray();
-            }
-
-            w.endObject();
-        }
-        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();
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java
new file mode 100644 (file)
index 0000000..ec7a949
--- /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.backup;
+
+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/backup/ConfigReader.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigReader.java
new file mode 100644 (file)
index 0000000..7959da5
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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.backup;
+
+import android.content.Context;
+import android.net.Uri;
+
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class ConfigReader extends ConfigIO {
+    private final OnDoneListener onDoneListener;
+    private RawConfigReader 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() {
+        RawConfigReader r = new RawConfigReader(new FileInputStream(pfd.getFileDescriptor()));
+    }
+    @Override
+    protected void processStream() throws IOException {
+        r.readConfig();
+        r.restoreAll();
+        String currentProfile = r.getCurrentProfile();
+
+        if (Data.getProfile() == null) {
+            Profile p = null;
+            final ProfileDAO dao = DB.get()
+                                     .getProfileDAO();
+            if (currentProfile != null)
+                p = dao.getByUuidSync(currentProfile);
+
+            if (p == null)
+                dao.getAnySync();
+
+            if (p != null)
+                Data.postCurrentProfile(p);
+        }
+
+        if (onDoneListener != null)
+            Misc.onMainThread(onDoneListener::done);
+    }
+    abstract static public class OnDoneListener {
+        public abstract void done();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java
new file mode 100644 (file)
index 0000000..b1a2814
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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.backup;
+
+import android.content.Context;
+import android.net.Uri;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ConfigWriter extends ConfigIO {
+    private final OnDoneListener onDoneListener;
+    private RawConfigWriter 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 RawConfigWriter(new FileOutputStream(pfd.getFileDescriptor()));
+    }
+    @Override
+    protected void processStream() throws IOException {
+        w.writeConfig();
+
+        if (onDoneListener != null)
+            Misc.onMainThread(onDoneListener::done);
+    }
+    abstract static public class OnDoneListener {
+        public abstract void done();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java b/app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java
new file mode 100644 (file)
index 0000000..ea09e51
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.backup;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.os.ParcelFileDescriptor;
+
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+class MobileLedgerBackupAgent extends BackupAgentHelper {
+    private static final int READ_BUF_LEN = 10;
+    public static String SETTINGS_KEY = "settings";
+    @Override
+    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+                         ParcelFileDescriptor newState) throws IOException {
+        super.onBackup(oldState, data, newState);
+        backupSettings(data);
+        newState.close();
+    }
+    private void backupSettings(BackupDataOutput data) throws IOException {
+        Logger.debug ("backup", "Starting cloud backup");
+        ByteArrayOutputStream output = new ByteArrayOutputStream(4096);
+        RawConfigWriter saver = new RawConfigWriter(output);
+        saver.writeConfig();
+        byte[] bytes = output.toByteArray();
+        data.writeEntityHeader(SETTINGS_KEY, bytes.length);
+        data.writeEntityData(bytes, bytes.length);
+        Logger.debug("backup", "Done writing backup data");
+    }
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+            throws IOException {
+        Logger.debug("restore", "Starting cloud restore");
+        if (data.readNextHeader()) {
+            String key = data.getKey();
+            if (key.equals(SETTINGS_KEY)) {
+                restoreSettings(data);
+            }
+        }
+    }
+    private void restoreSettings(BackupDataInput data) throws IOException {
+        byte[] bytes = new byte[data.getDataSize()];
+        data.readEntityData(bytes, 0, bytes.length);
+        RawConfigReader reader = new RawConfigReader(new ByteArrayInputStream(bytes));
+        reader.readConfig();
+        Logger.debug("restore", "Successfully read restore data. Wiping database");
+        DB.get().deleteAllSync();
+        Logger.debug("restore", "Database wiped");
+        reader.restoreAll();
+        Logger.debug("restore", "All data restored from the cloud");
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java b/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java
new file mode 100644 (file)
index 0000000..45a593d
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ * 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.backup;
+
+import android.util.JsonReader;
+import android.util.JsonToken;
+
+import net.ktnx.mobileledger.backup.ConfigIO.Keys;
+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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RawConfigReader {
+    private final JsonReader r;
+    private List<Currency> commodities;
+    private List<Profile> profiles;
+    private List<TemplateWithAccounts> templates;
+    private String currentProfile;
+    public RawConfigReader(InputStream inputStream) {
+        r = new JsonReader(new BufferedReader(new InputStreamReader(inputStream)));
+    }
+    public List<Currency> getCommodities() {
+        return commodities;
+    }
+    public List<Profile> getProfiles() {
+        return profiles;
+    }
+    public List<TemplateWithAccounts> getTemplates() {
+        return templates;
+    }
+    public String getCurrentProfile() {
+        return currentProfile;
+    }
+    public void readConfig() throws IOException {
+        commodities = null;
+        profiles = null;
+        templates = null;
+        currentProfile = null;
+        r.beginObject();
+        while (r.hasNext()) {
+            String item = r.nextName();
+            if (r.peek() == JsonToken.NULL) {
+                r.nextNull();
+                continue;
+            }
+            switch (item) {
+                case Keys.COMMODITIES:
+                    commodities = readCommodities();
+                    break;
+                case Keys.PROFILES:
+                    profiles = readProfiles();
+                    break;
+                case Keys.TEMPLATES:
+                    templates = readTemplates();
+                    break;
+                case Keys.CURRENT_PROFILE:
+                    currentProfile = r.nextString();
+                    break;
+                default:
+                    throw new RuntimeException("unexpected top-level item " + item);
+            }
+        }
+        r.endObject();
+    }
+    private TemplateAccount readTemplateAccount() throws IOException {
+        r.beginObject();
+        TemplateAccount result = new TemplateAccount(0L, 0L, 0L);
+        while (r.peek() != JsonToken.END_OBJECT) {
+            String item = r.nextName();
+            if (r.peek() == JsonToken.NULL) {
+                r.nextNull();
+                continue;
+            }
+            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();
+            if (r.peek() == JsonToken.NULL) {
+                r.nextNull();
+                continue;
+            }
+            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.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() 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() 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();
+                if (r.peek() == JsonToken.NULL) {
+                    r.nextNull();
+                    continue;
+                }
+                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() 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();
+                if (r.peek() == JsonToken.NULL) {
+                    r.nextNull();
+                    continue;
+                }
+
+                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;
+    }
+    public void restoreAll() {
+        restoreCommodities();
+        restoreProfiles();
+        restoreTemplates();
+    }
+    private void restoreTemplates() {
+        if (templates == null)
+            return;
+
+        TemplateHeaderDAO dao = DB.get()
+                                  .getTemplateDAO();
+
+        for (TemplateWithAccounts t : templates) {
+            if (dao.getTemplateWithAccountsByUuidSync(t.header.getUuid()) == null)
+                dao.insertSync(t);
+        }
+    }
+    private void restoreProfiles() {
+        if (profiles == null)
+            return;
+
+        ProfileDAO dao = DB.get()
+                           .getProfileDAO();
+
+        for (Profile p : profiles) {
+            if (dao.getByUuidSync(p.getUuid()) == null)
+                dao.insert(p);
+        }
+    }
+    private void restoreCommodities() {
+        if (commodities == null)
+            return;
+
+        CurrencyDAO dao = DB.get()
+                            .getCurrencyDAO();
+
+        for (Currency c : commodities) {
+            if (dao.getByNameSync(c.getName()) == null)
+                dao.insert(c);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java b/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java
new file mode 100644 (file)
index 0000000..751a74a
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * 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.backup;
+
+import android.util.JsonWriter;
+
+import net.ktnx.mobileledger.backup.ConfigIO.Keys;
+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 java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class RawConfigWriter {
+    private final JsonWriter w;
+    public RawConfigWriter(OutputStream outputStream) {
+        w = new JsonWriter(new BufferedWriter(new OutputStreamWriter(outputStream)));
+        w.setIndent("  ");
+    }
+    public void writeConfig() throws IOException {
+        w.beginObject();
+        writeCommodities();
+        writeProfiles();
+        writeCurrentProfile();
+        writeConfigTemplates();
+        w.endObject();
+        w.flush();
+    }
+    private void writeKey(String key, String value) throws IOException {
+        if (value != null)
+            w.name(key)
+             .value(value);
+    }
+    private void writeKey(String key, Integer value) throws IOException {
+        if (value != null)
+            w.name(key)
+             .value(value);
+    }
+    private void writeKey(String key, Long value) throws IOException {
+        if (value != null)
+            w.name(key)
+             .value(value);
+    }
+    private void writeKey(String key, Float value) throws IOException {
+        if (value != null)
+            w.name(key)
+             .value(value);
+    }
+    private void writeKey(String key, Boolean value) throws IOException {
+        if (value != null)
+            w.name(key)
+             .value(value);
+    }
+    private void writeConfigTemplates() throws IOException {
+        List<TemplateWithAccounts> templates = DB.get()
+                                                 .getTemplateDAO()
+                                                 .getAllTemplatesWithAccountsSync();
+
+        if (templates.isEmpty())
+            return;
+
+        w.name("templates")
+         .beginArray();
+        for (TemplateWithAccounts t : templates) {
+            w.beginObject();
+
+            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(Keys.TEST_TEXT, t.header.getTestText());
+            writeKey(ConfigIO.Keys.DATE_YEAR, t.header.getDateYear());
+            writeKey(Keys.DATE_YEAR_GROUP, t.header.getDateYearMatchGroup());
+            writeKey(Keys.DATE_MONTH, t.header.getDateMonth());
+            writeKey(Keys.DATE_MONTH_GROUP, t.header.getDateMonthMatchGroup());
+            writeKey(Keys.DATE_DAY, t.header.getDateDay());
+            writeKey(Keys.DATE_DAY_GROUP, t.header.getDateDayMatchGroup());
+            writeKey(Keys.TRANSACTION, t.header.getTransactionDescription());
+            writeKey(Keys.TRANSACTION_GROUP, t.header.getTransactionDescriptionMatchGroup());
+            writeKey(Keys.COMMENT, t.header.getTransactionComment());
+            writeKey(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) {
+                    w.beginObject();
+
+                    writeKey(Keys.NAME, a.getAccountName());
+                    writeKey(Keys.NAME_GROUP, a.getAccountNameMatchGroup());
+                    writeKey(Keys.COMMENT, a.getAccountComment());
+                    writeKey(Keys.COMMENT_GROUP, a.getAccountCommentMatchGroup());
+                    writeKey(Keys.AMOUNT, a.getAmount());
+                    writeKey(Keys.AMOUNT_GROUP, a.getAmountMatchGroup());
+                    writeKey(Keys.NEGATE_AMOUNT, a.getNegateAmount());
+                    writeKey(Keys.CURRENCY, a.getCurrency());
+                    writeKey(Keys.CURRENCY_GROUP, a.getCurrencyMatchGroup());
+
+                    w.endObject();
+                }
+                w.endArray();
+            }
+
+            w.endObject();
+        }
+        w.endArray();
+    }
+    private void writeCommodities() 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(Keys.NAME, c.getName());
+            writeKey(Keys.POSITION, c.getPosition());
+            writeKey(Keys.HAS_GAP, c.getHasGap());
+            w.endObject();
+        }
+        w.endArray();
+    }
+    private void writeProfiles() 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() throws IOException {
+        Profile currentProfile = Data.getProfile();
+        if (currentProfile == null)
+            return;
+
+        w.name(Keys.CURRENT_PROFILE)
+         .value(currentProfile.getUuid());
+    }
+}
index fce5967ff6d0242ada1e59c7a6949dd4bd83cff7..ab0d07d92adecba67801d228dea989d5dbcf05b8 100644 (file)
@@ -19,6 +19,7 @@ package net.ktnx.mobileledger.ui.profiles;
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.backup.BackupManager;
 import android.graphics.Typeface;
 import android.os.Bundle;
 import android.text.Editable;
@@ -408,6 +409,8 @@ public class ProfileDetailFragment extends Fragment {
             dao.insertLast(profile, null);
         }
 
+        BackupManager.dataChanged(BuildConfig.APPLICATION_ID);
+
         Activity activity = getActivity();
         if (activity != null)
             activity.finish();