From 5df10dc0b58df4d4be4e9ab34f1e0f477ca46766 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Thu, 15 Apr 2021 20:07:38 +0000 Subject: [PATCH] Room-based profile management MobileLedgerProfile is gone, Room spreads gradually --- .../main/java/net/ktnx/mobileledger/App.java | 10 +- .../async/RetrieveTransactionsTask.java | 102 ++- .../async/SendTransactionTask.java | 10 +- .../async/TransactionAccumulator.java | 4 +- .../async/UpdateTransactionsTask.java | 4 +- .../net/ktnx/mobileledger/dao/AccountDAO.java | 11 +- .../ktnx/mobileledger/dao/CurrencyDAO.java | 21 +- .../net/ktnx/mobileledger/dao/ProfileDAO.java | 48 +- .../dao/TransactionAccountDAO.java | 48 ++ .../ktnx/mobileledger/dao/TransactionDAO.java | 39 +- .../db/AccountAutocompleteAdapter.java | 3 +- .../java/net/ktnx/mobileledger/db/DB.java | 3 + .../ktnx/mobileledger/db/TemplateAccount.java | 31 +- .../ktnx/mobileledger/db/TemplateHeader.java | 10 +- .../mobileledger/db/TemplateWithAccounts.java | 2 +- .../net/ktnx/mobileledger/db/Transaction.java | 6 +- .../mobileledger/db/TransactionAccount.java | 12 +- .../net/ktnx/mobileledger/model/Currency.java | 44 - .../net/ktnx/mobileledger/model/Data.java | 68 +- .../ktnx/mobileledger/model/FutureDates.java | 68 ++ .../mobileledger/model/LedgerAccount.java | 15 + .../mobileledger/model/LedgerTransaction.java | 77 +- .../model/LedgerTransactionAccount.java | 20 + .../model/MobileLedgerProfile.java | 793 ------------------ .../model/TemplateDetailsItem.java | 11 +- .../ui/CurrencySelectorFragment.java | 42 +- .../ui/CurrencySelectorModel.java | 12 +- .../CurrencySelectorRecyclerViewAdapter.java | 28 +- .../mobileledger/ui/DatePickerFragment.java | 6 +- .../net/ktnx/mobileledger/ui/MainModel.java | 47 +- .../ui/OnCurrencyLongClickListener.java | 4 +- .../ui/OnCurrencySelectedListener.java | 4 +- .../AccountSummaryFragment.java | 7 +- .../ui/activity/MainActivity.java | 112 ++- .../ui/activity/ProfileThemedActivity.java | 11 +- .../ui/activity/SplashActivity.java | 4 +- .../NewTransactionAccountRowItemHolder.java | 4 +- .../NewTransactionActivity.java | 97 +-- .../NewTransactionFragment.java | 11 +- .../NewTransactionHeaderItemHolder.java | 3 +- .../NewTransactionItemViewHolder.java | 4 +- .../NewTransactionItemsAdapter.java | 8 +- .../new_transaction/NewTransactionModel.java | 30 +- .../ui/profiles/ProfileDetailActivity.java | 64 +- .../ui/profiles/ProfileDetailFragment.java | 119 +-- .../ui/profiles/ProfileDetailModel.java | 78 +- .../profiles/ProfilesRecyclerViewAdapter.java | 169 ++-- .../templates/TemplateDetailsViewModel.java | 8 +- .../TransactionListAdapter.java | 4 +- .../TransactionListFragment.java | 6 +- .../net/ktnx/mobileledger/utils/Colors.java | 11 +- .../ktnx/mobileledger/utils/NetworkUtil.java | 6 +- .../model/MobileLedgerProfileTest.java | 92 -- 53 files changed, 847 insertions(+), 1604 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java create mode 100644 app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java delete mode 100644 app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java delete mode 100644 app/src/test/java/net/ktnx/mobileledger/model/MobileLedgerProfileTest.java diff --git a/app/src/main/java/net/ktnx/mobileledger/App.java b/app/src/main/java/net/ktnx/mobileledger/App.java index a781ccf1..d6197c56 100644 --- a/app/src/main/java/net/ktnx/mobileledger/App.java +++ b/app/src/main/java/net/ktnx/mobileledger/App.java @@ -64,11 +64,9 @@ public class App extends Application { profileModel = null; } public static void storeStartupProfileAndTheme(long currentProfileId, int currentTheme) { - SharedPreferences prefs = - instance.getSharedPreferences(PREF_NAME, MODE_PRIVATE); + SharedPreferences prefs = instance.getSharedPreferences(PREF_NAME, MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(PREF_PROFILE_ID, - currentProfileId); + editor.putLong(PREF_PROFILE_ID, currentProfileId); editor.putInt(PREF_THEME_HUE, currentTheme); editor.apply(); } @@ -90,7 +88,7 @@ public class App extends Application { if (profileModel != null) return profileModel.getAuthUserName(); return Data.getProfile() - .getAuthUserName(); + .getAuthUser(); } private String getAuthPassword() { if (profileModel != null) @@ -102,7 +100,7 @@ public class App extends Application { if (profileModel != null) return profileModel.getUseAuthentication(); return Data.getProfile() - .isAuthEnabled(); + .useAuthentication(); } @Override public void onCreate() { diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java index aa47d73d..8b795ac1 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -23,10 +23,22 @@ import android.os.AsyncTask; import android.os.OperationCanceledException; import androidx.annotation.NonNull; +import androidx.room.Transaction; import com.fasterxml.jackson.databind.RuntimeJsonMappingException; import net.ktnx.mobileledger.App; +import net.ktnx.mobileledger.dao.AccountDAO; +import net.ktnx.mobileledger.dao.AccountValueDAO; +import net.ktnx.mobileledger.dao.TransactionAccountDAO; +import net.ktnx.mobileledger.dao.TransactionDAO; +import net.ktnx.mobileledger.db.Account; +import net.ktnx.mobileledger.db.AccountWithAmounts; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Option; +import net.ktnx.mobileledger.db.Profile; +import net.ktnx.mobileledger.db.TransactionAccount; +import net.ktnx.mobileledger.db.TransactionWithAccounts; import net.ktnx.mobileledger.err.HTTPException; import net.ktnx.mobileledger.json.API; import net.ktnx.mobileledger.json.AccountListParser; @@ -36,9 +48,9 @@ import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.utils.Logger; +import net.ktnx.mobileledger.utils.MLDB; import net.ktnx.mobileledger.utils.NetworkUtil; import java.io.BufferedReader; @@ -52,6 +64,7 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -81,15 +94,11 @@ public class RetrieveTransactionsTask extends private final Pattern reAccountValue = Pattern.compile( "\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?"); private final MainModel mainModel; - private final MobileLedgerProfile profile; - private final List prevAccounts; + private final Profile profile; private int expectedPostingsCount = -1; - public RetrieveTransactionsTask(@NonNull MainModel mainModel, - @NonNull MobileLedgerProfile profile, - List accounts) { + public RetrieveTransactionsTask(@NonNull MainModel mainModel, @NonNull Profile profile) { this.mainModel = mainModel; this.profile = profile; - this.prevAccounts = accounts; } private static void L(String msg) { //debug("transaction-parser", msg); @@ -325,7 +334,7 @@ public class RetrieveTransactionsTask extends state = ParserState.EXPECTING_TRANSACTION; L(String.format("transaction %s parsed → expecting transaction", - transaction.getId())); + transaction.getLedgerId())); // sounds like a good idea, but transaction-1 may not be the first one chronologically // for example, when you add the initial seeding transaction after entering some others @@ -340,8 +349,9 @@ public class RetrieveTransactionsTask extends LedgerTransactionAccount lta = parseTransactionAccountLine(line); if (lta != null) { transaction.addAccount(lta); - L(String.format(Locale.ENGLISH, "%d: %s = %s", transaction.getId(), - lta.getAccountName(), lta.getAmount())); + L(String.format(Locale.ENGLISH, "%d: %s = %s", + transaction.getLedgerId(), lta.getAccountName(), + lta.getAmount())); } else throw new IllegalStateException( @@ -384,7 +394,7 @@ public class RetrieveTransactionsTask extends } private List retrieveAccountList() throws IOException, HTTPException, ApiNotSupportedException { - final API apiVersion = profile.getApiVersion(); + final API apiVersion = API.valueOf(profile.getApiVersion()); if (apiVersion.equals(API.auto)) { return retrieveAccountListAnyVersion(); } @@ -430,9 +440,6 @@ public class RetrieveTransactionsTask extends SQLiteDatabase db = App.getDatabase(); ArrayList list = new ArrayList<>(); HashMap map = new HashMap<>(); - HashMap currentMap = new HashMap<>(); - for (LedgerAccount acc : prevAccounts) - currentMap.put(acc.getName(), acc); throwIfCancelled(); try (InputStream resp = http.getInputStream()) { throwIfCancelled(); @@ -452,20 +459,11 @@ public class RetrieveTransactionsTask extends throwIfCancelled(); } - // the current account tree may have changed, update the new-to be tree to match - for (LedgerAccount acc : list) { - LedgerAccount prevData = currentMap.get(acc.getName()); - if (prevData != null) { - acc.setExpanded(prevData.isExpanded()); - acc.setAmountsExpanded(prevData.amountsExpanded()); - } - } - return list; } private List retrieveTransactionList() throws ParseException, HTTPException, IOException, ApiNotSupportedException { - final API apiVersion = profile.getApiVersion(); + final API apiVersion = API.valueOf(profile.getApiVersion()); if (apiVersion.equals(API.auto)) { return retrieveTransactionListAnyVersion(); } @@ -552,7 +550,7 @@ public class RetrieveTransactionsTask extends .compareTo(o1.getDate()); if (res != 0) return res; - return Long.compare(o2.getId(), o1.getId()); + return Long.compare(o2.getLedgerId(), o1.getLedgerId()); }); return trList; } @@ -579,7 +577,10 @@ public class RetrieveTransactionsTask extends transactions = new ArrayList<>(); retrieveTransactionListLegacy(accounts, transactions); } - mainModel.setAndStoreAccountAndTransactionListFromWeb(accounts, transactions); + + storeAccountsAndTransactions(accounts, transactions); + + mainModel.updateDisplayedTransactionsFromWeb(transactions); return new Result(accounts, transactions); } @@ -616,6 +617,55 @@ public class RetrieveTransactionsTask extends Data.backgroundTaskFinished(); } } + @Transaction + private void storeAccountsAndTransactions(List accounts, + List transactions) { + AccountDAO accDao = DB.get() + .getAccountDAO(); + TransactionDAO trDao = DB.get() + .getTransactionDAO(); + TransactionAccountDAO trAccDao = DB.get() + .getTransactionAccountDAO(); + AccountValueDAO valDao = DB.get() + .getAccountValueDAO(); + + final List list = new ArrayList<>(); + for (LedgerAccount acc : accounts) { + final AccountWithAmounts a = acc.toDBOWithAmounts(); + Account existing = accDao.getByNameSync(profile.getId(), acc.getName()); + if (existing != null) { + a.account.setExpanded(existing.isExpanded()); + a.account.setAmountsExpanded(existing.isAmountsExpanded()); + a.account.setId( + existing.getId()); // not strictly needed, but since we have it anyway... + } + + list.add(a); + } + accDao.storeAccountsSync(list, profile.getId()); + + long trGen = trDao.getGenerationSync(profile.getId()); + for (LedgerTransaction tr : transactions) { + TransactionWithAccounts tran = tr.toDBO(); + tran.transaction.setGeneration(trGen); + tran.transaction.setProfileId(profile.getId()); + + tran.transaction.setId(trDao.insertSync(tran.transaction)); + + for (TransactionAccount trAcc : tran.accounts) { + trAcc.setGeneration(trGen); + trAcc.setTransactionId(tran.transaction.getId()); + trAcc.setId(trAccDao.insertSync(trAcc)); + } + } + + trDao.purgeOldTransactionsSync(profile.getId(), trGen); + + DB.get() + .getOptionDAO() + .insertSync(new Option(profile.getId(), MLDB.OPT_LAST_SCRAPE, + String.valueOf((new Date()).getTime()))); + } public void throwIfCancelled() { if (isCancelled()) throw new OperationCanceledException(null); diff --git a/app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java b/app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java index dbe4bc88..f7370adb 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java @@ -20,12 +20,12 @@ package net.ktnx.mobileledger.async; import android.os.AsyncTask; import android.util.Log; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.json.API; import net.ktnx.mobileledger.json.ApiNotSupportedException; import net.ktnx.mobileledger.json.Gateway; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.utils.Globals; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.NetworkUtil; @@ -56,20 +56,20 @@ import static net.ktnx.mobileledger.utils.Logger.debug; public class SendTransactionTask extends AsyncTask { private final TaskCallback taskCallback; - private final MobileLedgerProfile mProfile; + private final Profile mProfile; private final boolean simulate; protected String error; private String token; private String session; private LedgerTransaction transaction; - public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile, + public SendTransactionTask(TaskCallback callback, Profile profile, boolean simulate) { taskCallback = callback; mProfile = profile; this.simulate = simulate; } - public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile) { + public SendTransactionTask(TaskCallback callback, Profile profile) { taskCallback = callback; mProfile = profile; simulate = false; @@ -253,7 +253,7 @@ public class SendTransactionTask extends AsyncTask { protected String doInBackground(MainModel[] model) { - final MobileLedgerProfile profile = Data.getProfile(); + final Profile profile = Data.getProfile(); long profile_id = profile.getId(); Data.backgroundTaskStarted(); diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java index 7bca44ce..f37b940b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java @@ -59,16 +59,7 @@ public abstract class AccountDAO extends BaseDAO { final AccountValueDAO valueDAO = DB.get() .getAccountValueDAO(); Account account = accountWithAmounts.account; - Account existingAccount = getByNameSync(account.getProfileId(), account.getName()); - if (existingAccount != null) { - existingAccount.setGeneration(account.getGeneration()); - account = existingAccount; - updateSync(account); - } - else { - long accountId = insertSync(account); - account.setId(accountId); - } + account.setId(insertSync(account)); for (AccountValue value : accountWithAmounts.amounts) { value.setAccountId(account.getId()); value.setGeneration(account.getGeneration()); diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java index f650ff2d..b9d18616 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java @@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; import androidx.room.Update; @@ -29,21 +30,27 @@ import net.ktnx.mobileledger.db.Currency; import java.util.List; @Dao -public interface CurrencyDAO { - @Insert - void insert(Currency... items); +public abstract class CurrencyDAO extends BaseDAO { + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract long insertSync(Currency item); @Update - void update(Currency... items); + abstract void updateSync(Currency item); @Delete - void delete(Currency item); + public abstract void deleteSync(Currency item); @Query("SELECT * FROM currencies") - LiveData> getCurrencies(); + public abstract LiveData> getAll(); @Query("SELECT * FROM currencies WHERE id = :id") - LiveData getCurrencyById(Long id); + abstract LiveData getById(long id); + + @Query("SELECT * FROM currencies WHERE id = :id") + public abstract Currency getByIdSync(long id); + + @Query("SELECT * FROM currencies WHERE name = :name") + public abstract LiveData getByName(String name); // not useful for now // @Transaction diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java index 52071745..77697230 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java @@ -17,20 +17,40 @@ package net.ktnx.mobileledger.dao; +import android.os.AsyncTask; + import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; +import androidx.room.Transaction; import androidx.room.Update; import net.ktnx.mobileledger.db.Profile; +import java.util.List; + @Dao public abstract class ProfileDAO extends BaseDAO { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract long insertSync(Profile item); + @Transaction + public long insertLastSync(Profile item) { + int count = getProfileCountSync(); + item.setOrderNo(count + 1); + return insertSync(item); + } + public void insertLast(Profile item, OnInsertedReceiver onInsertedReceiver) { + AsyncTask.execute(() -> { + long id = insertLastSync(item); + if (onInsertedReceiver != null) + onInsertedReceiver.onInsert(id); + }); + } + @Update abstract void updateSync(Profile item); @@ -43,6 +63,32 @@ public abstract class ProfileDAO extends BaseDAO { @Query("SELECT * FROM profiles WHERE id=:profileId") public abstract LiveData getById(long profileId); + @Query("SELECT * FROM profiles ORDER BY order_no") + public abstract List getAllOrderedSync(); + + @Query("SELECT * FROM profiles ORDER BY order_no") + public abstract LiveData> getAllOrdered(); + @Query("SELECT * FROM profiles LIMIT 1") public abstract Profile getAnySync(); + + @Query("SELECT MAX(order_no) FROM profiles") + public abstract int getProfileCountSync(); + public void updateOrderSync(List list) { + if (list == null) + list = getAllOrderedSync(); + int order = 1; + for (Profile p : list) { + p.setOrderNo(order++); + updateSync(p); + } + } + public void updateOrder(List list, Runnable onDone) { + AsyncTask.execute(() -> { + updateOrderSync(list); + if (onDone != null) + onDone.run(); + + }); + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java new file mode 100644 index 00000000..eae26262 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import net.ktnx.mobileledger.db.TransactionAccount; + +import java.util.List; + +@Dao +public abstract class TransactionAccountDAO extends BaseDAO { + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract long insertSync(TransactionAccount item); + + @Update + public abstract void updateSync(TransactionAccount item); + + @Delete + public abstract void deleteSync(TransactionAccount item); + + @Delete + public abstract void deleteSync(List items); + + @Query("SELECT * FROM transaction_accounts WHERE id = :id") + public abstract LiveData getById(long id); +} diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java index 485f65f1..c03da12b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java +++ b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java @@ -23,6 +23,7 @@ import androidx.room.ColumnInfo; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; import androidx.room.Update; @@ -42,7 +43,7 @@ public abstract class TransactionDAO extends BaseDAO { return result; } - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) public abstract long insertSync(Transaction item); @Update @@ -68,6 +69,10 @@ public abstract class TransactionDAO extends BaseDAO { @Query("SELECT * FROM transactions WHERE id = :transactionId") public abstract LiveData getByIdWithAccounts(long transactionId); + @androidx.room.Transaction + @Query("SELECT * FROM transactions WHERE id = :transactionId") + public abstract TransactionWithAccounts getByIdWithAccountsSync(long transactionId); + @Query("SELECT DISTINCT description, CASE WHEN description_upper LIKE :term||'%%' THEN 1 " + " WHEN description_upper LIKE '%%:'||:term||'%%' THEN 2 " + " WHEN description_upper LIKE '%% '||:term||'%%' THEN 3 " + @@ -76,9 +81,41 @@ public abstract class TransactionDAO extends BaseDAO { "ORDER BY ordering, description_upper, rowid ") public abstract List lookupDescriptionSync(@NonNull String term); + @androidx.room.Transaction + @Query("SELECT * from transactions WHERE description = :description ORDER BY year desc, month" + + " desc, day desc LIMIT 1") + public abstract TransactionWithAccounts getFirstByDescriptionSync(@NonNull String description); + + @androidx.room.Transaction + @Query("SELECT * from transactions tr JOIN transaction_accounts t_a ON t_a.transaction_id = " + + "tr.id WHERE tr.description = :description AND t_a.account_name LIKE " + + "'%'||:accountTerm||'%' ORDER BY year desc, month desc, day desc LIMIT 1") + public abstract TransactionWithAccounts getFirstByDescriptionHavingAccountSync( + @NonNull String description, @NonNull String accountTerm); + @Query("SELECT * from transactions WHERE profile_id = :profileId") public abstract List allForProfileSync(long profileId); + @Query("SELECT generation FROM transactions WHERE profile_id = :profileId LIMIT 1") + protected abstract TransactionGenerationContainer getGenerationPOJOSync(long profileId); + public long getGenerationSync(long profileId) { + TransactionGenerationContainer result = getGenerationPOJOSync(profileId); + + if (result == null) + return 0; + return result.generation; + } + @Query("DELETE FROM transactions WHERE profile_id = :profileId AND generation <> " + + ":currentGeneration") + public abstract void purgeOldTransactionsSync(long profileId, long currentGeneration); + static class TransactionGenerationContainer { + @ColumnInfo + long generation; + public TransactionGenerationContainer(long generation) { + this.generation = generation; + } + } + static public class DescriptionContainer { @ColumnInfo public String description; diff --git a/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java b/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java index 42bf4c6b..9b6d41ba 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java @@ -24,7 +24,6 @@ import android.widget.Filter; import androidx.annotation.NonNull; import net.ktnx.mobileledger.dao.AccountDAO; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.utils.Logger; import java.util.ArrayList; @@ -38,7 +37,7 @@ public class AccountAutocompleteAdapter extends ArrayAdapter { public AccountAutocompleteAdapter(Context context) { super(context, android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); } - public AccountAutocompleteAdapter(Context context, @NonNull MobileLedgerProfile profile) { + public AccountAutocompleteAdapter(Context context, @NonNull Profile profile) { this(context); profileId = profile.getId(); } diff --git a/app/src/main/java/net/ktnx/mobileledger/db/DB.java b/app/src/main/java/net/ktnx/mobileledger/db/DB.java index 683d54b5..41b6f7c0 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/DB.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/DB.java @@ -37,6 +37,7 @@ import net.ktnx.mobileledger.dao.OptionDAO; import net.ktnx.mobileledger.dao.ProfileDAO; import net.ktnx.mobileledger.dao.TemplateAccountDAO; import net.ktnx.mobileledger.dao.TemplateHeaderDAO; +import net.ktnx.mobileledger.dao.TransactionAccountDAO; import net.ktnx.mobileledger.dao.TransactionDAO; import net.ktnx.mobileledger.utils.Logger; @@ -206,6 +207,8 @@ abstract public class DB extends RoomDatabase { public abstract TransactionDAO getTransactionDAO(); + public abstract TransactionAccountDAO getTransactionAccountDAO(); + public abstract OptionDAO getOptionDAO(); public abstract DescriptionHistoryDAO getDescriptionHistoryDAO(); diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java index b1725091..e8f24486 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java @@ -38,11 +38,9 @@ import org.jetbrains.annotations.NotNull; }) public class TemplateAccount extends TemplateBase { @PrimaryKey(autoGenerate = true) - @NotNull - private Long id; - @NonNull + private long id; @ColumnInfo(name = "template_id") - private Long templateId; + private long templateId; @ColumnInfo(name = "acc") private String accountName; @ColumnInfo(name = "position") @@ -50,8 +48,8 @@ public class TemplateAccount extends TemplateBase { private Long position; @ColumnInfo(name = "acc_match_group") private Integer accountNameMatchGroup; - @ColumnInfo(name = "currency") - private Integer currency; + @ColumnInfo + private Long currency; @ColumnInfo(name = "currency_match_group") private Integer currencyMatchGroup; @ColumnInfo(name = "amount") @@ -83,10 +81,10 @@ public class TemplateAccount extends TemplateBase { accountCommentMatchGroup = o.accountCommentMatchGroup; negateAmount = o.negateAmount; } - public Long getId() { + public long getId() { return id; } - public void setId(Long id) { + public void setId(long id) { this.id = id; } public Boolean getNegateAmount() { @@ -124,12 +122,19 @@ public class TemplateAccount extends TemplateBase { public void setAccountNameMatchGroup(Integer accountNameMatchGroup) { this.accountNameMatchGroup = accountNameMatchGroup; } - public Integer getCurrency() { + public Long getCurrency() { return currency; } - public void setCurrency(Integer currency) { + public void setCurrency(Long currency) { this.currency = currency; } + public Currency getCurrencyObject() { + if (currency == null || currency <= 0) + return null; + return DB.get() + .getCurrencyDAO() + .getByIdSync(currency); + } public Integer getCurrencyMatchGroup() { return currencyMatchGroup; } @@ -160,10 +165,10 @@ public class TemplateAccount extends TemplateBase { public void setAccountCommentMatchGroup(Integer accountCommentMatchGroup) { this.accountCommentMatchGroup = accountCommentMatchGroup; } - public TemplateAccount createDuplicate() { + public TemplateAccount createDuplicate(TemplateHeader header) { TemplateAccount dup = new TemplateAccount(this); - dup.id = null; - dup.templateId = null; + dup.id = 0; + dup.templateId = header.getId(); return dup; } diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java index b5afb89e..9d355d09 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java @@ -30,8 +30,7 @@ import org.jetbrains.annotations.NotNull; @Entity(tableName = "templates") public class TemplateHeader extends TemplateBase { @PrimaryKey(autoGenerate = true) - @NonNull - private Long id; + private long id; @ColumnInfo(name = "name") @NonNull private String name; @@ -127,11 +126,10 @@ public class TemplateHeader extends TemplateBase { public void setDateDay(Integer dateDay) { this.dateDay = dateDay; } - @NonNull - public Long getId() { + public long getId() { return id; } - public void setId(@NonNull Long id) { + public void setId(long id) { this.id = id; } @NonNull @@ -203,7 +201,7 @@ public class TemplateHeader extends TemplateBase { } public TemplateHeader createDuplicate() { TemplateHeader dup = new TemplateHeader(this); - dup.id = null; + dup.id = 0; return dup; } diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java index 8572167d..a96236e6 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java @@ -47,7 +47,7 @@ public class TemplateWithAccounts { result.header = header.createDuplicate(); result.accounts = new ArrayList<>(); for (TemplateAccount acc : accounts) { - result.accounts.add(acc.createDuplicate()); + result.accounts.add(acc.createDuplicate(result.header)); } return result; diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java b/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java index 05e63d8c..d1757a2b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java @@ -64,7 +64,7 @@ public class Transaction { @ColumnInfo private String comment; @ColumnInfo - private int generation = 0; + private long generation = 0; public long getLedgerId() { return ledgerId; } @@ -119,10 +119,10 @@ public class Transaction { public void setComment(String comment) { this.comment = comment; } - public int getGeneration() { + public long getGeneration() { return generation; } - public void setGeneration(int generation) { + public void setGeneration(long generation) { this.generation = generation; } diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java index 80b185be..d556731f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java +++ b/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java @@ -37,7 +37,7 @@ public class TransactionAccount { @PrimaryKey(autoGenerate = true) private long id; @ColumnInfo(name = "transaction_id") - private int transactionId; + private long transactionId; @ColumnInfo(name = "order_no") private int orderNo; @ColumnInfo(name = "account_name") @@ -51,7 +51,7 @@ public class TransactionAccount { @ColumnInfo private String comment; @ColumnInfo(defaultValue = "0") - private int generation = 0; + private long generation = 0; public long getId() { return id; } @@ -59,10 +59,10 @@ public class TransactionAccount { this.id = id; } @NonNull - public int getTransactionId() { + public long getTransactionId() { return transactionId; } - public void setTransactionId(int transactionId) { + public void setTransactionId(long transactionId) { this.transactionId = transactionId; } public int getOrderNo() { @@ -97,10 +97,10 @@ public class TransactionAccount { public void setComment(String comment) { this.comment = comment; } - public int getGeneration() { + public long getGeneration() { return generation; } - public void setGeneration(int generation) { + public void setGeneration(long generation) { this.generation = generation; } } diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Currency.java b/app/src/main/java/net/ktnx/mobileledger/model/Currency.java index 1c7dbf7f..bfe84ea0 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/Currency.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/Currency.java @@ -17,31 +17,9 @@ package net.ktnx.mobileledger.model; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; - -import net.ktnx.mobileledger.App; import net.ktnx.mobileledger.utils.Misc; public class Currency { - public static final DiffUtil.ItemCallback DIFF_CALLBACK = - new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull Currency oldItem, - @NonNull Currency newItem) { - return oldItem.id == newItem.id; - } - @Override - public boolean areContentsTheSame(@NonNull Currency oldItem, - @NonNull Currency newItem) { - return oldItem.name.equals(newItem.name) && - oldItem.position.equals(newItem.position) && - (oldItem.hasGap == newItem.hasGap); - } - }; private final int id; private String name; private Position position; @@ -58,28 +36,6 @@ public class Currency { this.position = position; this.hasGap = hasGap; } - public Currency(MobileLedgerProfile profile, String name, Position position, boolean hasGap) { - SQLiteDatabase db = App.getDatabase(); - - try (Cursor c = db.rawQuery("select max(rowid) from currencies", null)) { - c.moveToNext(); - this.id = c.getInt(0) + 1; - } - db.execSQL("insert into currencies(id, name, position, has_gap) values(?, ?, ?, ?)", - new Object[]{this.id, name, position.toString(), hasGap}); - - this.name = name; - this.position = position; - this.hasGap = hasGap; - } - public static Currency loadByName(String name) { - MobileLedgerProfile profile = Data.getProfile(); - return profile.loadCurrencyByName(name); - } - public static Currency loadById(int id) { - MobileLedgerProfile profile = Data.getProfile(); - return profile.loadCurrencyById(id); - } static public boolean equal(Currency left, Currency right) { if (left == null) { return right == null; diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Data.java b/app/src/main/java/net/ktnx/mobileledger/model/Data.java index 15d78ac0..35f39358 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/Data.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/Data.java @@ -18,20 +18,20 @@ package net.ktnx.mobileledger.model; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import net.ktnx.mobileledger.async.RetrieveTransactionsTask; -import net.ktnx.mobileledger.utils.LockHolder; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.utils.Locker; import net.ktnx.mobileledger.utils.Logger; import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; @@ -45,8 +45,7 @@ public final class Data { new MutableLiveData<>(false); public static final MutableLiveData backgroundTaskProgress = new MutableLiveData<>(); - public static final MutableLiveData> profiles = - new MutableLiveData<>(null); + public static final LiveData> profiles = DB.get().getProfileDAO().getAllOrdered(); public static final MutableLiveData currencySymbolPosition = new MutableLiveData<>(); public static final MutableLiveData currencyGap = new MutableLiveData<>(true); @@ -59,7 +58,7 @@ public final class Data { public static final MutableLiveData lastTransactionsUpdateText = new MutableLiveData<>(); public static final MutableLiveData lastAccountsUpdateText = new MutableLiveData<>(); - private static final MutableLiveData profile = + private static final MutableLiveData profile = new InertMutableLiveData<>(); private static final AtomicInteger backgroundTaskCount = new AtomicInteger(0); private static final Locker profilesLocker = new Locker(); @@ -70,7 +69,7 @@ public final class Data { } @NonNull - public static MobileLedgerProfile getProfile() { + public static Profile getProfile() { return Objects.requireNonNull(profile.getValue()); } public static void backgroundTaskStarted() { @@ -87,61 +86,12 @@ public final class Data { cnt)); backgroundTasksRunning.postValue(cnt > 0); } - public static void setCurrentProfile(@NonNull MobileLedgerProfile newProfile) { + public static void setCurrentProfile(@NonNull Profile newProfile) { profile.setValue(newProfile); } - public static void postCurrentProfile(@NonNull MobileLedgerProfile newProfile) { + public static void postCurrentProfile(@NonNull Profile newProfile) { profile.postValue(newProfile); } - public static int getProfileIndex(MobileLedgerProfile profile) { - try (LockHolder ignored = profilesLocker.lockForReading()) { - List prList = profiles.getValue(); - if (prList == null) - throw new AssertionError(); - for (int i = 0; i < prList.size(); i++) { - MobileLedgerProfile p = prList.get(i); - if (p.equals(profile)) - return i; - } - - return -1; - } - } - @SuppressWarnings("WeakerAccess") - public static int getProfileIndex(long profileId) { - try (LockHolder ignored = profilesLocker.lockForReading()) { - List prList = profiles.getValue(); - if (prList == null) - throw new AssertionError(); - for (int i = 0; i < prList.size(); i++) { - MobileLedgerProfile p = prList.get(i); - if (p.getId() == profileId) - return i; - } - - return -1; - } - } - @Nullable - public static MobileLedgerProfile getProfile(long profileId) { - MobileLedgerProfile profile; - try (LockHolder readLock = profilesLocker.lockForReading()) { - List prList = profiles.getValue(); - if ((prList == null) || prList.isEmpty()) { - readLock.close(); - try (LockHolder ignored = profilesLocker.lockForWriting()) { - profile = MobileLedgerProfile.loadAllFromDB(profileId); - } - } - else { - int i = getProfileIndex(profileId); - if (i == -1) - i = 0; - profile = prList.get(i); - } - } - return profile; - } public static void refreshCurrencyData(Locale locale) { NumberFormat formatter = NumberFormat.getCurrencyInstance(locale); java.util.Currency currency = formatter.getCurrency(); @@ -186,7 +136,7 @@ public final class Data { return numberFormatter.format(number); } public static void observeProfile(LifecycleOwner lifecycleOwner, - Observer observer) { + Observer observer) { profile.observe(lifecycleOwner, observer); } public static void removeProfileObservers(LifecycleOwner owner) { diff --git a/app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java b/app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java new file mode 100644 index 00000000..db80db18 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.model; + +import android.content.res.Resources; +import android.util.SparseArray; + +import net.ktnx.mobileledger.R; + +public enum FutureDates { + None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90), + SixMonths(180), OneYear(365), All(-1); + private static final SparseArray map = new SparseArray<>(); + + static { + for (FutureDates item : FutureDates.values()) { + map.put(item.value, item); + } + } + + private final int value; + FutureDates(int value) { + this.value = value; + } + public static FutureDates valueOf(int i) { + return map.get(i, None); + } + public int toInt() { + return this.value; + } + public String getText(Resources resources) { + switch (value) { + case 7: + return resources.getString(R.string.future_dates_7); + case 14: + return resources.getString(R.string.future_dates_14); + case 30: + return resources.getString(R.string.future_dates_30); + case 60: + return resources.getString(R.string.future_dates_60); + case 90: + return resources.getString(R.string.future_dates_90); + case 180: + return resources.getString(R.string.future_dates_180); + case 365: + return resources.getString(R.string.future_dates_365); + case -1: + return resources.getString(R.string.future_dates_all); + default: + return resources.getString(R.string.future_dates_none); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java index c76f2d56..1e2a1a4f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java @@ -212,6 +212,21 @@ public class LedgerAccount { return dbo; } + @NonNull + public AccountWithAmounts toDBOWithAmounts() { + AccountWithAmounts dbo = new AccountWithAmounts(); + dbo.account = toDBO(); + + dbo.amounts = new ArrayList<>(); + for (LedgerAmount amt : getAmounts()) { + AccountValue val = new AccountValue(); + val.setCurrency(amt.getCurrency()); + val.setValue(amt.getAmount()); + dbo.amounts.add(val); + } + + return dbo; + } public long getId() { return dbId; } diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java index 001c107e..b9c6021f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java @@ -24,6 +24,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import net.ktnx.mobileledger.App; +import net.ktnx.mobileledger.db.Profile; +import net.ktnx.mobileledger.db.Transaction; +import net.ktnx.mobileledger.db.TransactionAccount; +import net.ktnx.mobileledger.db.TransactionWithAccounts; import net.ktnx.mobileledger.utils.Digest; import net.ktnx.mobileledger.utils.Globals; import net.ktnx.mobileledger.utils.SimpleDate; @@ -53,38 +57,77 @@ public class LedgerTransaction { return Float.compare(o1.getAmount(), o2.getAmount()); }; private final long profile; - private final long id; + private final long ledgerId; private final List accounts; + private long dbId; private SimpleDate date; private String description; private String comment; private String dataHash; private boolean dataLoaded; - public LedgerTransaction(long id, String dateString, String description) throws ParseException { - this(id, Globals.parseLedgerDate(dateString), description); + public LedgerTransaction(long ledgerId, String dateString, String description) + throws ParseException { + this(ledgerId, Globals.parseLedgerDate(dateString), description); } - public LedgerTransaction(long id, SimpleDate date, String description, - MobileLedgerProfile profile) { + public LedgerTransaction(TransactionWithAccounts dbo) { + this(dbo.transaction.getLedgerId(), dbo.transaction.getProfileId()); + dbId = dbo.transaction.getId(); + date = new SimpleDate(dbo.transaction.getYear(), dbo.transaction.getMonth(), + dbo.transaction.getDay()); + description = dbo.transaction.getDescription(); + comment = dbo.transaction.getComment(); + dataHash = dbo.transaction.getDataHash(); + if (dbo.accounts != null) + for (TransactionAccount acc : dbo.accounts) { + accounts.add(new LedgerTransactionAccount(acc)); + } + dataLoaded = true; + } + public TransactionWithAccounts toDBO() { + TransactionWithAccounts o = new TransactionWithAccounts(); + o.transaction = new Transaction(); + o.transaction.setId(dbId); + o.transaction.setProfileId(profile); + o.transaction.setLedgerId(ledgerId); + o.transaction.setYear(date.year); + o.transaction.setMonth(date.month); + o.transaction.setDay(date.day); + o.transaction.setDescription(description); + o.transaction.setComment(comment); + fillDataHash(); + o.transaction.setDataHash(dataHash); + + o.accounts = new ArrayList<>(); + int orderNo = 1; + for (LedgerTransactionAccount acc : accounts) { + TransactionAccount a = acc.toDBO(); + a.setOrderNo(orderNo++); + a.setTransactionId(dbId); + o.accounts.add(a); + } + return o; + } + public LedgerTransaction(long ledgerId, SimpleDate date, String description, Profile profile) { this.profile = profile.getId(); - this.id = id; + this.ledgerId = ledgerId; this.date = date; this.description = description; this.accounts = new ArrayList<>(); this.dataHash = null; dataLoaded = false; } - public LedgerTransaction(long id, SimpleDate date, String description) { - this(id, date, description, Data.getProfile()); + public LedgerTransaction(long ledgerId, SimpleDate date, String description) { + this(ledgerId, date, description, Data.getProfile()); } public LedgerTransaction(SimpleDate date, String description) { this(0, date, description); } - public LedgerTransaction(int id) { - this(id, (SimpleDate) null, null); + public LedgerTransaction(int ledgerId) { + this(ledgerId, (SimpleDate) null, null); } - public LedgerTransaction(int id, long profileId) { + public LedgerTransaction(long ledgerId, long profileId) { this.profile = profileId; - this.id = id; + this.ledgerId = ledgerId; this.date = null; this.description = null; this.accounts = new ArrayList<>(); @@ -125,8 +168,8 @@ public class LedgerTransaction { public void setComment(String comment) { this.comment = comment; } - public long getId() { - return id; + public long getLedgerId() { + return ledgerId; } protected void fillDataHash() { loadData(App.getDatabase()); @@ -137,7 +180,7 @@ public class LedgerTransaction { StringBuilder data = new StringBuilder(); data.append("ver1"); data.append(profile); - data.append(getId()); + data.append(getLedgerId()); data.append('\0'); data.append(getDescription()); data.append('\0'); @@ -169,7 +212,7 @@ public class LedgerTransaction { try (Cursor cTr = db.rawQuery( "SELECT year, month, day, description, comment from transactions WHERE id=?", - new String[]{String.valueOf(id)})) + new String[]{String.valueOf(ledgerId)})) { if (cTr.moveToFirst()) { date = new SimpleDate(cTr.getInt(0), cTr.getInt(1), cTr.getInt(2)); @@ -181,7 +224,7 @@ public class LedgerTransaction { try (Cursor cAcc = db.rawQuery( "SELECT account_name, amount, currency, comment FROM " + "transaction_accounts WHERE transaction_id = ?", - new String[]{String.valueOf(id)})) + new String[]{String.valueOf(ledgerId)})) { while (cAcc.moveToNext()) { // debug("transactions", diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java index 1791cfbc..53b6a65f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java @@ -19,6 +19,7 @@ package net.ktnx.mobileledger.model; import androidx.annotation.NonNull; +import net.ktnx.mobileledger.db.TransactionAccount; import net.ktnx.mobileledger.utils.Misc; import java.util.Locale; @@ -31,6 +32,7 @@ public class LedgerTransactionAccount { private String currency; private String comment; private boolean amountValid = true; + private long dbId; public LedgerTransactionAccount(String accountName, float amount, String currency, String comment) { this.setAccountName(accountName); @@ -56,6 +58,13 @@ public class LedgerTransactionAccount { amountValid = origin.amountValid; currency = origin.getCurrency(); } + public LedgerTransactionAccount(TransactionAccount dbo) { + this(dbo.getAccountName(), dbo.getAmount(), Misc.emptyIsNull(dbo.getCurrency()), + Misc.emptyIsNull(dbo.getComment())); + amountSet = true; + amountValid = true; + dbId = dbo.getId(); + } public String getComment() { return comment; } @@ -114,4 +123,15 @@ public class LedgerTransactionAccount { return sb.toString(); } + public TransactionAccount toDBO() { + TransactionAccount dbo = new TransactionAccount(); + dbo.setAccountName(accountName); + if (amountSet) + dbo.setAmount(amount); + dbo.setComment(comment); + dbo.setCurrency(currency); + dbo.setId(dbId); + + return dbo; + } } \ No newline at end of file diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java deleted file mode 100644 index 8cdc9c91..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java +++ /dev/null @@ -1,793 +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 . - */ - -package net.ktnx.mobileledger.model; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.SparseArray; - -import androidx.annotation.Nullable; -import androidx.room.Transaction; - -import net.ktnx.mobileledger.App; -import net.ktnx.mobileledger.R; -import net.ktnx.mobileledger.async.DbOpQueue; -import net.ktnx.mobileledger.dao.AccountDAO; -import net.ktnx.mobileledger.dao.DescriptionHistoryDAO; -import net.ktnx.mobileledger.dao.OptionDAO; -import net.ktnx.mobileledger.dao.ProfileDAO; -import net.ktnx.mobileledger.dao.TransactionDAO; -import net.ktnx.mobileledger.db.AccountValue; -import net.ktnx.mobileledger.db.AccountWithAmounts; -import net.ktnx.mobileledger.db.DB; -import net.ktnx.mobileledger.db.Profile; -import net.ktnx.mobileledger.json.API; -import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity; -import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment; -import net.ktnx.mobileledger.utils.Logger; -import net.ktnx.mobileledger.utils.Misc; -import net.ktnx.mobileledger.utils.SimpleDate; - -import org.jetbrains.annotations.Contract; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static net.ktnx.mobileledger.utils.Logger.debug; - -public final class MobileLedgerProfile { - // N.B. when adding new fields, update the copy-constructor below - private final long id; - private String name; - private boolean permitPosting; - private boolean showCommentsByDefault; - private boolean showCommodityByDefault; - private String defaultCommodity; - private String preferredAccountsFilter; - private String url; - private boolean authEnabled; - private String authUserName; - private String authPassword; - private int themeHue; - private int orderNo = -1; - private API apiVersion = API.auto; - private FutureDates futureDates = FutureDates.None; - private boolean accountsLoaded; - private boolean transactionsLoaded; - private HledgerVersion detectedVersion; - // N.B. when adding new fields, update the copy-constructor below - transient private AccountAndTransactionListSaver accountAndTransactionListSaver; - public MobileLedgerProfile(long id) { - this.id = id; - } - public MobileLedgerProfile(MobileLedgerProfile origin) { - id = origin.id; - name = origin.name; - permitPosting = origin.permitPosting; - showCommentsByDefault = origin.showCommentsByDefault; - showCommodityByDefault = origin.showCommodityByDefault; - preferredAccountsFilter = origin.preferredAccountsFilter; - url = origin.url; - authEnabled = origin.authEnabled; - authUserName = origin.authUserName; - authPassword = origin.authPassword; - themeHue = origin.themeHue; - orderNo = origin.orderNo; - futureDates = origin.futureDates; - apiVersion = origin.apiVersion; - defaultCommodity = origin.defaultCommodity; - accountsLoaded = origin.accountsLoaded; - transactionsLoaded = origin.transactionsLoaded; - if (origin.detectedVersion != null) - detectedVersion = new HledgerVersion(origin.detectedVersion); - } - // loads all profiles into Data.profiles - // returns the profile with the given UUID - public static MobileLedgerProfile loadAllFromDB(long currentProfileId) { - MobileLedgerProfile result = null; - ArrayList list = new ArrayList<>(); - SQLiteDatabase db = App.getDatabase(); - try (Cursor cursor = db.rawQuery("SELECT id, name, url, use_authentication, auth_user, " + - "auth_password, permit_posting, theme, order_no, " + - "preferred_accounts_filter, future_dates, api_version, " + - "show_commodity_by_default, default_commodity, " + - "show_comments_by_default, detected_version_pre_1_19, " + - "detected_version_major, detected_version_minor FROM " + - "profiles order by order_no", null)) - { - while (cursor.moveToNext()) { - MobileLedgerProfile item = new MobileLedgerProfile(cursor.getLong(0)); - item.setName(cursor.getString(1)); - item.setUrl(cursor.getString(2)); - item.setAuthEnabled(cursor.getInt(3) == 1); - item.setAuthUserName(cursor.getString(4)); - item.setAuthPassword(cursor.getString(5)); - item.setPostingPermitted(cursor.getInt(6) == 1); - item.setThemeId(cursor.getInt(7)); - item.orderNo = cursor.getInt(8); - item.setPreferredAccountsFilter(cursor.getString(9)); - item.setFutureDates(cursor.getInt(10)); - item.setApiVersion(cursor.getInt(11)); - item.setShowCommodityByDefault(cursor.getInt(12) == 1); - item.setDefaultCommodity(cursor.getString(13)); - item.setShowCommentsByDefault(cursor.getInt(14) == 1); - { - boolean pre_1_20 = cursor.getInt(15) == 1; - int major = cursor.getInt(16); - int minor = cursor.getInt(17); - - if (!pre_1_20 && major == 0 && minor == 0) { - item.detectedVersion = null; - } - else if (pre_1_20) { - item.detectedVersion = new HledgerVersion(true); - } - else { - item.detectedVersion = new HledgerVersion(major, minor); - } - } - list.add(item); - if (item.getId() == currentProfileId) - result = item; - } - } - Data.profiles.postValue(list); - return result; - } - public static void storeProfilesOrder() { - SQLiteDatabase db = App.getDatabase(); - db.beginTransactionNonExclusive(); - try { - int orderNo = 0; - for (MobileLedgerProfile p : Objects.requireNonNull(Data.profiles.getValue())) { - db.execSQL("update profiles set order_no=? where id=?", - new Object[]{orderNo, p.getId()}); - p.orderNo = orderNo; - orderNo++; - } - db.setTransactionSuccessful(); - } - finally { - db.endTransaction(); - } - } - static public void startEditProfileActivity(Context context, MobileLedgerProfile profile) { - Intent intent = new Intent(context, ProfileDetailActivity.class); - Bundle args = new Bundle(); - if (profile != null) { - int index = Data.getProfileIndex(profile); - if (index != -1) - intent.putExtra(ProfileDetailFragment.ARG_ITEM_ID, index); - } - intent.putExtras(args); - context.startActivity(intent, args); - } - public static MobileLedgerProfile fromDBO(Profile newProfile) { - MobileLedgerProfile p = new MobileLedgerProfile(newProfile.getId()); - p.setDetectedVersion(new HledgerVersion(newProfile.getDetectedVersionMajor(), - newProfile.getDetectedVersionMinor())); - p.setApiVersion(newProfile.getApiVersion()); - p.setAuthEnabled(newProfile.useAuthentication()); - p.setAuthUserName(newProfile.getAuthUser()); - p.setAuthPassword(newProfile.getAuthPassword()); - p.setDefaultCommodity(newProfile.getDefaultCommodity()); - p.setFutureDates(newProfile.getFutureDates()); - p.setName(newProfile.getName()); - p.setPostingPermitted(newProfile.permitPosting()); - p.setPreferredAccountsFilter(newProfile.getPreferredAccountsFilter()); - p.setShowCommentsByDefault(newProfile.getShowCommentsByDefault()); - p.setShowCommodityByDefault(newProfile.getShowCommodityByDefault()); - p.setUrl(newProfile.getUrl()); - p.setThemeId(newProfile.getTheme()); - - return p; - } - public HledgerVersion getDetectedVersion() { - return detectedVersion; - } - public void setDetectedVersion(HledgerVersion detectedVersion) { - this.detectedVersion = detectedVersion; - } - @Contract(value = "null -> false", pure = true) - @Override - public boolean equals(@Nullable Object obj) { - if (obj == null) - return false; - if (obj == this) - return true; - if (obj.getClass() != this.getClass()) - return false; - - MobileLedgerProfile p = (MobileLedgerProfile) obj; - if (id != p.id) - return false; - if (!name.equals(p.name)) - return false; - if (permitPosting != p.permitPosting) - return false; - if (showCommentsByDefault != p.showCommentsByDefault) - return false; - if (showCommodityByDefault != p.showCommodityByDefault) - return false; - if (!Objects.equals(defaultCommodity, p.defaultCommodity)) - return false; - if (!Objects.equals(preferredAccountsFilter, p.preferredAccountsFilter)) - return false; - if (!Objects.equals(url, p.url)) - return false; - if (authEnabled != p.authEnabled) - return false; - if (!Objects.equals(authUserName, p.authUserName)) - return false; - if (!Objects.equals(authPassword, p.authPassword)) - return false; - if (themeHue != p.themeHue) - return false; - if (apiVersion != p.apiVersion) - return false; - if (!Objects.equals(detectedVersion, p.detectedVersion)) - return false; - return futureDates == p.futureDates; - } - public boolean getShowCommentsByDefault() { - return showCommentsByDefault; - } - public void setShowCommentsByDefault(boolean newValue) { - this.showCommentsByDefault = newValue; - } - public boolean getShowCommodityByDefault() { - return showCommodityByDefault; - } - public void setShowCommodityByDefault(boolean showCommodityByDefault) { - this.showCommodityByDefault = showCommodityByDefault; - } - public String getDefaultCommodity() { - return defaultCommodity; - } - public void setDefaultCommodity(String defaultCommodity) { - this.defaultCommodity = defaultCommodity; - } - public void setDefaultCommodity(CharSequence defaultCommodity) { - if (defaultCommodity == null) - this.defaultCommodity = null; - else - this.defaultCommodity = String.valueOf(defaultCommodity); - } - public API getApiVersion() { - return apiVersion; - } - public void setApiVersion(API apiVersion) { - this.apiVersion = apiVersion; - } - public void setApiVersion(int apiVersion) { - this.apiVersion = API.valueOf(apiVersion); - } - public FutureDates getFutureDates() { - return futureDates; - } - public void setFutureDates(int anInt) { - futureDates = FutureDates.valueOf(anInt); - } - public void setFutureDates(FutureDates futureDates) { - this.futureDates = futureDates; - } - public String getPreferredAccountsFilter() { - return preferredAccountsFilter; - } - public void setPreferredAccountsFilter(String preferredAccountsFilter) { - this.preferredAccountsFilter = preferredAccountsFilter; - } - public void setPreferredAccountsFilter(CharSequence preferredAccountsFilter) { - setPreferredAccountsFilter(String.valueOf(preferredAccountsFilter)); - } - public boolean isPostingPermitted() { - return permitPosting; - } - public void setPostingPermitted(boolean permitPosting) { - this.permitPosting = permitPosting; - } - public long getId() { - return id; - } - public String getName() { - return name; - } - public void setName(CharSequence text) { - setName(String.valueOf(text)); - } - public void setName(String name) { - this.name = name; - } - public String getUrl() { - return url; - } - public void setUrl(CharSequence text) { - setUrl(String.valueOf(text)); - } - public void setUrl(String url) { - this.url = url; - } - public boolean isAuthEnabled() { - return authEnabled; - } - public void setAuthEnabled(boolean authEnabled) { - this.authEnabled = authEnabled; - } - public String getAuthUserName() { - return authUserName; - } - public void setAuthUserName(CharSequence text) { - setAuthUserName(String.valueOf(text)); - } - public void setAuthUserName(String authUserName) { - this.authUserName = authUserName; - } - public String getAuthPassword() { - return authPassword; - } - public void setAuthPassword(CharSequence text) { - setAuthPassword(String.valueOf(text)); - } - public void setAuthPassword(String authPassword) { - this.authPassword = authPassword; - } - public void storeInDB() { - SQLiteDatabase db = App.getDatabase(); - db.beginTransactionNonExclusive(); - try { -// debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " + -// "url=%s, permit_posting=%s, authEnabled=%s, " + -// "themeHue=%d", uuid, name, url, -// permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeHue)); - db.execSQL("REPLACE INTO profiles(id, name, permit_posting, url, " + - "use_authentication, auth_user, auth_password, theme, order_no, " + - "preferred_accounts_filter, future_dates, api_version, " + - "show_commodity_by_default, default_commodity, show_comments_by_default," + - "detected_version_pre_1_19, detected_version_major, " + - "detected_version_minor) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - new Object[]{id, name, permitPosting, url, authEnabled, - authEnabled ? authUserName : null, - authEnabled ? authPassword : null, themeHue, orderNo, - preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(), - showCommodityByDefault, defaultCommodity, showCommentsByDefault, - (detectedVersion != null) && detectedVersion.isPre_1_20_1(), - (detectedVersion == null) ? 0 : detectedVersion.getMajor(), - (detectedVersion == null) ? 0 : detectedVersion.getMinor() - }); - db.setTransactionSuccessful(); - } - finally { - db.endTransaction(); - } - } - public void storeAccount(SQLiteDatabase db, int generation, LedgerAccount acc, - boolean storeUiFields) { - // replace into is a bad idea because it would reset hidden to its default value - // we like the default, but for new accounts only - String sql = "update accounts set generation = ?"; - List params = new ArrayList<>(); - params.add(generation); - if (storeUiFields) { - sql += ", expanded=?"; - params.add(acc.isExpanded() ? 1 : 0); - } - sql += " where profile_id=? and name=?"; - params.add(id); - params.add(acc.getName()); - db.execSQL(sql, params.toArray()); - - db.execSQL("insert into accounts(profile_id, name, name_upper, parent_name, level, " + - "expanded, generation) select ?,?,?,?,?,0,? where (select changes() = 0)", - new Object[]{id, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(), - acc.getLevel(), generation - }); -// debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid)); - } - public void storeTransaction(SQLiteDatabase db, int generation, LedgerTransaction tr) { - tr.fillDataHash(); -// Logger.debug("storeTransaction", String.format(Locale.US, "ID %d", tr.getId())); - SimpleDate d = tr.getDate(); - db.execSQL("UPDATE transactions SET year=?, month=?, day=?, description=?, comment=?, " + - "data_hash=?, generation=? WHERE profile_id=? AND ledger_id=?", - new Object[]{d.year, d.month, d.day, tr.getDescription(), tr.getComment(), - tr.getDataHash(), generation, id, tr.getId() - }); - db.execSQL( - "INSERT INTO transactions(profile_id, ledger_id, year, month, day, description, " + - "comment, data_hash, generation) " + - "select ?,?,?,?,?,?,?,?,? WHERE (select changes() = 0)", - new Object[]{id, tr.getId(), tr.getDate().year, tr.getDate().month, - tr.getDate().day, tr.getDescription(), tr.getComment(), - tr.getDataHash(), generation - }); - - int accountOrderNo = 1; - for (LedgerTransactionAccount item : tr.getAccounts()) { - db.execSQL("UPDATE transaction_accounts SET account_name=?, amount=?, currency=?, " + - "comment=?, generation=? " + "WHERE transaction_id=? AND order_no=?", - new Object[]{item.getAccountName(), item.getAmount(), - Misc.nullIsEmpty(item.getCurrency()), item.getComment(), - generation, tr.getId(), accountOrderNo - }); - db.execSQL("INSERT INTO transaction_accounts(transaction_id, " + - "order_no, account_name, amount, currency, comment, generation) " + - "select ?, ?, ?, ?, ?, ?, ? WHERE (select changes() = 0)", - new Object[]{tr.getId(), accountOrderNo, item.getAccountName(), - item.getAmount(), Misc.nullIsEmpty(item.getCurrency()), - item.getComment(), generation - }); - - accountOrderNo++; - } -// debug("profile", String.format("Transaction %d stored", tr.getId())); - } - public String getOption(String name, String default_value) { - SQLiteDatabase db = App.getDatabase(); - try (Cursor cursor = db.rawQuery( - "select value from options where profile_id = ? and name=?", - new String[]{String.valueOf(id), name})) - { - if (cursor.moveToFirst()) { - String result = cursor.getString(0); - - if (result == null) { - debug("profile", "returning default value for " + name); - result = default_value; - } - else - debug("profile", String.format("option %s=%s", name, result)); - - return result; - } - else - return default_value; - } - catch (Exception e) { - debug("db", "returning default value for " + name, e); - return default_value; - } - } - public long getLongOption(String name, long default_value) { - long longResult; - String result = getOption(name, ""); - if ((result == null) || result.isEmpty()) { - debug("profile", String.format("Returning default value for option %s", name)); - longResult = default_value; - } - else { - try { - longResult = Long.parseLong(result); - debug("profile", String.format("option %s=%s", name, result)); - } - catch (Exception e) { - debug("profile", String.format("Returning default value for option %s", name), e); - longResult = default_value; - } - } - - return longResult; - } - public void setOption(String name, String value) { - debug("profile", String.format("setting option %s=%s", name, value)); - DbOpQueue.add("insert or replace into options(profile_id, name, value) values(?, ?, ?);", - new String[]{String.valueOf(id), name, value}); - } - public void setLongOption(String name, long value) { - setOption(name, String.valueOf(value)); - } - public void removeFromDB() { - ProfileDAO dao = DB.get() - .getProfileDAO(); - AsyncTask.execute(() -> dao.deleteSync(dao.getByIdSync(id))); - } - public LedgerTransaction loadTransaction(int transactionId) { - LedgerTransaction tr = new LedgerTransaction(transactionId, this.id); - tr.loadData(App.getDatabase()); - - return tr; - } - public int getThemeHue() { -// debug("profile", String.format("Profile.getThemeHue() returning %d", themeHue)); - return this.themeHue; - } - public void setThemeHue(Object o) { - setThemeId(Integer.parseInt(String.valueOf(o))); - } - public void setThemeId(int themeHue) { -// debug("profile", String.format("Profile.setThemeHue(%d) called", themeHue)); - this.themeHue = themeHue; - } - public int getNextTransactionsGeneration(SQLiteDatabase db) { - try (Cursor c = db.rawQuery( - "SELECT generation FROM transactions WHERE profile_id=? LIMIT 1", - new String[]{String.valueOf(id)})) - { - if (c.moveToFirst()) - return c.getInt(0) + 1; - } - return 1; - } - private void deleteNotPresentTransactions(SQLiteDatabase db, int generation) { - Logger.debug("db/benchmark", "Deleting obsolete transactions"); - db.execSQL( - "DELETE FROM transaction_accounts WHERE (select t.profile_id from transactions t " + - "where t.id=transaction_accounts.transaction_id)=? AND generation" + " <> ?", - new Object[]{id, generation}); - db.execSQL("DELETE FROM transactions WHERE profile_id=? AND generation <> ?", - new Object[]{id, generation}); - Logger.debug("db/benchmark", "Done deleting obsolete transactions"); - } - @Transaction - public void wipeAllDataSync() { - OptionDAO optDao = DB.get() - .getOptionDAO(); - optDao.deleteSync(optDao.allForProfileSync(id)); - - AccountDAO accDao = DB.get() - .getAccountDAO(); - accDao.deleteSync(accDao.allForProfileSync(id)); - - TransactionDAO trnDao = DB.get() - .getTransactionDAO(); - trnDao.deleteSync(trnDao.allForProfileSync(id)); - - DescriptionHistoryDAO descDao = DB.get() - .getDescriptionHistoryDAO(); - descDao.sweepSync(); - } - public void wipeAllData() { - AsyncTask.execute(this::wipeAllDataSync); - } - public List getCurrencies() { - SQLiteDatabase db = App.getDatabase(); - - ArrayList result = new ArrayList<>(); - - try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c", - new String[]{})) - { - while (c.moveToNext()) { - Currency currency = new Currency(c.getInt(0), c.getString(1), - Currency.Position.valueOf(c.getString(2)), c.getInt(3) == 1); - result.add(currency); - } - } - - return result; - } - Currency loadCurrencyByName(String name) { - SQLiteDatabase db = App.getDatabase(); - Currency result = tryLoadCurrencyByName(db, name); - if (result == null) - throw new RuntimeException(String.format("Unable to load currency '%s'", name)); - return result; - } - private Currency tryLoadCurrencyByName(SQLiteDatabase db, String name) { - try (Cursor cursor = db.rawQuery( - "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.name=?", - new String[]{name})) - { - if (cursor.moveToFirst()) { - return new Currency(cursor.getInt(0), cursor.getString(1), - Currency.Position.valueOf(cursor.getString(2)), cursor.getInt(3) == 1); - } - return null; - } - } - public void storeAccountAndTransactionListAsync(List accounts, - List transactions) { - if (accountAndTransactionListSaver != null) - accountAndTransactionListSaver.interrupt(); - - accountAndTransactionListSaver = - new AccountAndTransactionListSaver(this, accounts, transactions); - accountAndTransactionListSaver.start(); - } - private Currency tryLoadCurrencyById(SQLiteDatabase db, int id) { - try (Cursor cursor = db.rawQuery( - "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.id=?", - new String[]{String.valueOf(id)})) - { - if (cursor.moveToFirst()) { - return new Currency(cursor.getInt(0), cursor.getString(1), - Currency.Position.valueOf(cursor.getString(2)), cursor.getInt(3) == 1); - } - return null; - } - } - public Currency loadCurrencyById(int id) { - SQLiteDatabase db = App.getDatabase(); - Currency result = tryLoadCurrencyById(db, id); - if (result == null) - throw new RuntimeException(String.format("Unable to load currency with id '%d'", id)); - return result; - } - - public enum FutureDates { - None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90), - SixMonths(180), OneYear(365), All(-1); - private static final SparseArray map = new SparseArray<>(); - - static { - for (FutureDates item : FutureDates.values()) { - map.put(item.value, item); - } - } - - private final int value; - FutureDates(int value) { - this.value = value; - } - public static FutureDates valueOf(int i) { - return map.get(i, None); - } - public int toInt() { - return this.value; - } - public String getText(Resources resources) { - switch (value) { - case 7: - return resources.getString(R.string.future_dates_7); - case 14: - return resources.getString(R.string.future_dates_14); - case 30: - return resources.getString(R.string.future_dates_30); - case 60: - return resources.getString(R.string.future_dates_60); - case 90: - return resources.getString(R.string.future_dates_90); - case 180: - return resources.getString(R.string.future_dates_180); - case 365: - return resources.getString(R.string.future_dates_365); - case -1: - return resources.getString(R.string.future_dates_all); - default: - return resources.getString(R.string.future_dates_none); - } - } - } - - private static class AccountAndTransactionListSaver extends Thread { - private final MobileLedgerProfile profile; - private final List accounts; - private final List transactions; - AccountAndTransactionListSaver(MobileLedgerProfile profile, List accounts, - List transactions) { - this.accounts = accounts; - this.transactions = transactions; - this.profile = profile; - } - public int getNextDescriptionsGeneration(SQLiteDatabase db) { - int generation = 1; - try (Cursor c = db.rawQuery("SELECT generation FROM description_history LIMIT 1", - null)) - { - if (c.moveToFirst()) { - generation = c.getInt(0) + 1; - } - } - return generation; - } - void deleteNotPresentDescriptions(SQLiteDatabase db, int generation) { - Logger.debug("db/benchmark", "Deleting obsolete descriptions"); - db.execSQL("DELETE FROM description_history WHERE generation <> ?", - new Object[]{generation}); - db.execSQL("DELETE FROM description_history WHERE generation <> ?", - new Object[]{generation}); - Logger.debug("db/benchmark", "Done deleting obsolete descriptions"); - } - @Override - public void run() { - SQLiteDatabase db = App.getDatabase(); - db.beginTransactionNonExclusive(); - try { - int transactionsGeneration = profile.getNextTransactionsGeneration(db); - if (isInterrupted()) - return; - - for (LedgerTransaction tr : transactions) { - profile.storeTransaction(db, transactionsGeneration, tr); - if (isInterrupted()) - return; - } - - profile.deleteNotPresentTransactions(db, transactionsGeneration); - if (isInterrupted()) { - return; - } - - Map unique = new HashMap<>(); - - debug("descriptions", "Starting refresh"); - int descriptionsGeneration = getNextDescriptionsGeneration(db); - try (Cursor c = db.rawQuery("SELECT distinct description from transactions", - null)) - { - while (c.moveToNext()) { - String description = c.getString(0); - String descriptionUpper = description.toUpperCase(); - if (unique.containsKey(descriptionUpper)) - continue; - - storeDescription(db, descriptionsGeneration, description, descriptionUpper); - - unique.put(descriptionUpper, true); - } - } - deleteNotPresentDescriptions(db, descriptionsGeneration); - - db.setTransactionSuccessful(); - } - finally { - db.endTransaction(); - } - - AsyncTask.execute(() -> { - List list = new ArrayList<>(); - - final AccountDAO dao = DB.get() - .getAccountDAO(); - - for (LedgerAccount acc : accounts) { - AccountWithAmounts rec = new AccountWithAmounts(); - rec.account = acc.toDBO(); - - if (isInterrupted()) - return; - - rec.amounts = new ArrayList<>(); - for (LedgerAmount amt : acc.getAmounts()) { - AccountValue av = new AccountValue(); - av.setCurrency(amt.getCurrency()); - av.setValue(amt.getAmount()); - - rec.amounts.add(av); - } - - list.add(rec); - } - - if (isInterrupted()) - return; - - dao.storeAccountsSync(list, profile.getId()); - }); - } - private void storeDescription(SQLiteDatabase db, int generation, String description, - String descriptionUpper) { - db.execSQL("UPDATE description_history SET description=?, generation=? WHERE " + - "description_upper=?", new Object[]{description, generation, descriptionUpper - }); - db.execSQL( - "INSERT INTO description_history(description, description_upper, generation) " + - "select ?,?,? WHERE (select changes() = 0)", - new Object[]{description, descriptionUpper, generation - }); - } - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java index 74080681..67d6a4f3 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java @@ -119,9 +119,7 @@ abstract public class TemplateDetailsItem { acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup()); if (pa.getCurrencyMatchGroup() == null) { - final Integer currencyId = pa.getCurrency(); - if (currencyId != null && currencyId > 0) - acc.setCurrency(Currency.loadById(currencyId)); + acc.setCurrency(pa.getCurrencyObject()); } else acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup()); @@ -300,7 +298,8 @@ abstract public class TemplateDetailsItem { PossiblyMatchedValue.withLiteralString(""); private final PossiblyMatchedValue amount = PossiblyMatchedValue.withLiteralFloat(null); - private final PossiblyMatchedValue currency = new PossiblyMatchedValue<>(); + private final PossiblyMatchedValue currency = + new PossiblyMatchedValue<>(); private boolean negateAmount; public AccountRow() { super(Type.ACCOUNT_ITEM); @@ -339,10 +338,10 @@ abstract public class TemplateDetailsItem { public void setCurrencyMatchGroup(int group) { currency.setMatchGroup(group); } - public Currency getCurrency() { + public net.ktnx.mobileledger.db.Currency getCurrency() { return currency.getValue(); } - public void setCurrency(Currency currency) { + public void setCurrency(net.ktnx.mobileledger.db.Currency currency) { this.currency.setValue(currency); } public int getAccountNameMatchGroup() { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java index e86d5015..47f2353e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java @@ -35,16 +35,15 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.switchmaterial.SwitchMaterial; -import net.ktnx.mobileledger.App; import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.dao.CurrencyDAO; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Currency; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.concurrent.CopyOnWriteArrayList; /** * A fragment representing a list of Items. @@ -109,11 +108,19 @@ public class CurrencySelectorFragment extends AppCompatDialogFragment model = new ViewModelProvider(this).get(CurrencySelectorModel.class); if (onCurrencySelectedListener != null) model.setOnCurrencySelectedListener(onCurrencySelectedListener); - MobileLedgerProfile profile = Objects.requireNonNull(Data.getProfile()); + Profile profile = Data.getProfile(); - model.currencies.setValue(new CopyOnWriteArrayList<>(profile.getCurrencies())); CurrencySelectorRecyclerViewAdapter adapter = new CurrencySelectorRecyclerViewAdapter(); - model.currencies.observe(this, adapter::submitList); + DB.get() + .getCurrencyDAO() + .getAll() + .observe(this, list -> { + List strings = new ArrayList<>(); + for (net.ktnx.mobileledger.db.Currency c : list) { + strings.add(c.getName()); + } + adapter.submitList(strings); + }); recyclerView.setAdapter(adapter); adapter.setCurrencySelectedListener(this); @@ -146,11 +153,11 @@ public class CurrencySelectorFragment extends AppCompatDialogFragment String currName = String.valueOf(tvNewCurrName.getText()); if (!currName.isEmpty()) { - List list = new ArrayList<>(model.currencies.getValue()); + DB.get() + .getCurrencyDAO() + .insert(new net.ktnx.mobileledger.db.Currency(null, + String.valueOf(tvNewCurrName.getText()), "after", false), null); // FIXME hardcoded position and gap setting - list.add(new Currency(profile, String.valueOf(tvNewCurrName.getText()), - Currency.Position.after, false)); - model.currencies.setValue(list); } tvNewCurrName.setVisibility(View.GONE); @@ -210,19 +217,18 @@ public class CurrencySelectorFragment extends AppCompatDialogFragment model.resetOnCurrencySelectedListener(); } @Override - public void onCurrencySelected(Currency item) { + public void onCurrencySelected(String item) { model.triggerOnCurrencySelectedListener(item); dismiss(); } @Override - public void onCurrencyLongClick(Currency item) { - ArrayList list = new ArrayList<>(model.currencies.getValue()); - App.getDatabase() - .execSQL("delete from currencies where id=?", new Object[]{item.getId()}); - list.remove(item); - model.currencies.setValue(list); + public void onCurrencyLongClick(String item) { + CurrencyDAO dao = DB.get() + .getCurrencyDAO(); + dao.getByName(item) + .observe(this, dao::deleteSync); } public void showPositionAndPadding() { deferredShowPositionAndPadding = true; diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java index 4b346cdf..0fca9d6c 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java @@ -22,18 +22,10 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; -import net.ktnx.mobileledger.model.Currency; - -import java.util.ArrayList; -import java.util.List; - public class CurrencySelectorModel extends ViewModel { - public final MutableLiveData> currencies; private final MutableLiveData positionAndPaddingVisible = new MutableLiveData<>(true); private OnCurrencySelectedListener selectionListener; - public CurrencySelectorModel() { - this.currencies = new MutableLiveData<>(new ArrayList<>()); - } + public CurrencySelectorModel() { } public void showPositionAndPadding() { positionAndPaddingVisible.postValue(true); } @@ -50,7 +42,7 @@ public class CurrencySelectorModel extends ViewModel { void resetOnCurrencySelectedListener() { selectionListener = null; } - void triggerOnCurrencySelectedListener(Currency c) { + void triggerOnCurrencySelectedListener(String c) { if (selectionListener != null) selectionListener.onCurrencySelected(c); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java index b7d90a3a..7d4c60be 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java @@ -22,6 +22,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; @@ -35,12 +37,24 @@ import org.jetbrains.annotations.NotNull; * specified {@link OnCurrencySelectedListener}. */ public class CurrencySelectorRecyclerViewAdapter - extends ListAdapter { + extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) { + return oldItem.equals(newItem); + } + @Override + public boolean areContentsTheSame(@NonNull String oldItem, + @NonNull String newItem) { + return true; + } + }; private OnCurrencySelectedListener currencySelectedListener; private OnCurrencyLongClickListener currencyLongClickListener; public CurrencySelectorRecyclerViewAdapter() { - super(Currency.DIFF_CALLBACK); + super(DIFF_CALLBACK); } @NotNull @Override @@ -60,7 +74,7 @@ public class CurrencySelectorRecyclerViewAdapter public void resetCurrencySelectedListener() { currencySelectedListener = null; } - public void notifyCurrencySelected(Currency currency) { + public void notifyCurrencySelected(String currency) { if (null != currencySelectedListener) currencySelectedListener.onCurrencySelected(currency); } @@ -68,14 +82,14 @@ public class CurrencySelectorRecyclerViewAdapter this.currencyLongClickListener = listener; } public void resetCurrencyLockClickListener() { currencyLongClickListener = null; } - private void notifyCurrencyLongClicked(Currency mItem) { + private void notifyCurrencyLongClicked(String mItem) { if (null != currencyLongClickListener) currencyLongClickListener.onCurrencyLongClick(mItem); } public class ViewHolder extends RecyclerView.ViewHolder { private final TextView mNameView; - private Currency mItem; + private String mItem; ViewHolder(View view) { super(view); @@ -93,9 +107,9 @@ public class CurrencySelectorRecyclerViewAdapter public String toString() { return super.toString() + " '" + mNameView.getText() + "'"; } - void bindTo(Currency item) { + void bindTo(String item) { mItem = item; - mNameView.setText(item.getName()); + mNameView.setText(item); } } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java index f5fe08cb..54a20932 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java @@ -26,7 +26,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDialogFragment; import net.ktnx.mobileledger.R; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.model.FutureDates; import net.ktnx.mobileledger.utils.SimpleDate; import java.util.Calendar; @@ -54,8 +54,8 @@ public class DatePickerFragment extends AppCompatDialogFragment else this.maxDate = maxDate.toDate().getTime(); } - public void setFutureDates(MobileLedgerProfile.FutureDates futureDates) { - if (futureDates == MobileLedgerProfile.FutureDates.All) { + public void setFutureDates(FutureDates futureDates) { + if (futureDates == FutureDates.All) { maxDate = Long.MAX_VALUE; } else { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java index eb7a0641..c860e41b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java @@ -27,47 +27,30 @@ import androidx.lifecycle.ViewModel; import net.ktnx.mobileledger.async.RetrieveTransactionsTask; import net.ktnx.mobileledger.async.TransactionAccumulator; import net.ktnx.mobileledger.async.UpdateTransactionsTask; -import net.ktnx.mobileledger.model.AccountListItem; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; import net.ktnx.mobileledger.model.LedgerTransaction; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.model.TransactionListItem; -import net.ktnx.mobileledger.utils.Locker; import net.ktnx.mobileledger.utils.Logger; -import net.ktnx.mobileledger.utils.MLDB; import net.ktnx.mobileledger.utils.SimpleDate; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Locale; -import static net.ktnx.mobileledger.utils.Logger.debug; - public class MainModel extends ViewModel { public final MutableLiveData foundTransactionItemIndex = new MutableLiveData<>(null); private final MutableLiveData updatingFlag = new MutableLiveData<>(false); private final MutableLiveData accountFilter = new MutableLiveData<>(); private final MutableLiveData> displayedTransactions = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> displayedAccounts = - new MutableLiveData<>(); - private final Locker accountsLocker = new Locker(); private final MutableLiveData updateError = new MutableLiveData<>(); - private MobileLedgerProfile profile; - private final List allAccounts = new ArrayList<>(); private SimpleDate firstTransactionDate; private SimpleDate lastTransactionDate; transient private RetrieveTransactionsTask retrieveTransactionsTask; transient private Thread displayedAccountsUpdater; private TransactionsDisplayedFilter displayedTransactionsUpdater; - private void setLastUpdateStamp(long transactionCount) { - debug("db", "Updating transaction value stamp"); - Date now = new Date(); - profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime()); - Data.lastUpdateDate.postValue(now); - } public void scheduleTransactionListReload() { UpdateTransactionsTask task = new UpdateTransactionsTask(); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); @@ -78,16 +61,12 @@ public class MainModel extends ViewModel { public LiveData getUpdateError() { return updateError; } - public void setProfile(MobileLedgerProfile profile) { - stopTransactionsRetrieval(); - this.profile = profile; - } public LiveData> getDisplayedTransactions() { return displayedTransactions; } - public void setDisplayedTransactions(List list, int transactionCount) { + public void setDisplayedTransactions(List list) { displayedTransactions.postValue(list); - Data.lastUpdateTransactionCount.postValue(transactionCount); + Data.lastUpdateTransactionCount.postValue(list.size()); } public SimpleDate getFirstTransactionDate() { return firstTransactionDate; @@ -124,9 +103,9 @@ public class MainModel extends ViewModel { Logger.debug("db", "Ignoring request for transaction retrieval - already active"); return; } - MobileLedgerProfile profile = Data.getProfile(); + Profile profile = Data.getProfile(); - retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile, allAccounts); + retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile); Logger.debug("db", "Created a background transaction retrieval task"); retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -138,21 +117,6 @@ public class MainModel extends ViewModel { public void transactionRetrievalDone() { retrieveTransactionsTask = null; } - public synchronized Locker lockAccountsForWriting() { - accountsLocker.lockForWriting(); - return accountsLocker; - } - public LiveData> getDisplayedAccounts() { - return displayedAccounts; - } - public synchronized void setAndStoreAccountAndTransactionListFromWeb( - List accounts, List transactions) { - profile.storeAccountAndTransactionListAsync(accounts, transactions); - - setLastUpdateStamp(transactions.size()); - - updateDisplayedTransactionsFromWeb(transactions); - } synchronized public void updateDisplayedTransactionsFromWeb(List list) { if (displayedTransactionsUpdater != null) { displayedTransactionsUpdater.interrupt(); @@ -163,7 +127,6 @@ public class MainModel extends ViewModel { public void clearUpdateError() { updateError.postValue(null); } - public void clearAccounts() { displayedAccounts.postValue(new ArrayList<>()); } public void clearTransactions() { displayedTransactions.setValue(new ArrayList<>()); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java index f681f999..e464e73e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java @@ -17,8 +17,6 @@ package net.ktnx.mobileledger.ui; -import net.ktnx.mobileledger.model.Currency; - /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated @@ -30,5 +28,5 @@ import net.ktnx.mobileledger.model.Currency; * >Communicating with Other Fragments for more information. */ public interface OnCurrencyLongClickListener { - void onCurrencyLongClick(Currency item); + void onCurrencyLongClick(String item); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java index c12f32a2..94e417e2 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java @@ -17,8 +17,6 @@ package net.ktnx.mobileledger.ui; -import net.ktnx.mobileledger.model.Currency; - /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated @@ -30,5 +28,5 @@ import net.ktnx.mobileledger.model.Currency; * >Communicating with Other Fragments for more information. */ public interface OnCurrencySelectedListener { - void onCurrencySelected(Currency item); + void onCurrencySelected(String item); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java index 9d9c2c64..d5964e4b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.RecyclerView; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.db.AccountWithAmounts; import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.AccountListItem; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; @@ -104,10 +105,12 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { model.scheduleTransactionListRetrieval(); }); + Data.observeProfile(this, this::onProfileChanged); + } + private void onProfileChanged(Profile profile) { DB.get() .getAccountDAO() - .getAllWithAmounts(Data.getProfile() - .getId()) + .getAllWithAmounts(profile.getId()) .observe(getViewLifecycleOwner(), list -> AsyncTask.execute(() -> { List adapterList = new ArrayList<>(); adapterList.add(new AccountListItem.Header(Data.lastAccountsUpdateText)); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java index 28a1af84..945e81c5 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -34,6 +34,7 @@ import android.view.animation.AnimationUtils; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AlertDialog; import androidx.core.view.GravityCompat; @@ -51,12 +52,14 @@ import com.google.android.material.snackbar.Snackbar; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.async.RetrieveTransactionsTask; import net.ktnx.mobileledger.databinding.ActivityMainBinding; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment; import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity; +import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity; import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter; import net.ktnx.mobileledger.ui.templates.TemplatesActivity; import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment; @@ -89,7 +92,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa private DrawerLayout.SimpleDrawerListener drawerListener; private ActionBarDrawerToggle barDrawerToggle; private ViewPager2.OnPageChangeCallback pageChangeCallback; - private MobileLedgerProfile profile; + private Profile profile; private MainModel mainModel; private ActivityMainBinding b; private int fabVerticalOffset; @@ -150,6 +153,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa Data.observeProfile(this, this::onProfileChanged); Data.profiles.observe(this, this::onProfileListChanged); + Data.backgroundTaskProgress.observe(this, this::onRetrieveProgress); Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged); @@ -208,13 +212,11 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa .setValue(savedInstanceState.getString(STATE_ACC_FILTER, null)); } - b.btnNoProfilesAdd.setOnClickListener( - v -> MobileLedgerProfile.startEditProfileActivity(this, null)); + b.btnNoProfilesAdd.setOnClickListener(v -> ProfileDetailActivity.start(this, null)); b.btnAddTransaction.setOnClickListener(this::fabNewTransactionClicked); - b.navNewProfileButton.setOnClickListener( - v -> MobileLedgerProfile.startEditProfileActivity(this, null)); + b.navNewProfileButton.setOnClickListener(v -> ProfileDetailActivity.start(this, null)); b.transactionListCancelDownload.setOnClickListener(this::onStopTransactionRefreshClick); @@ -332,18 +334,18 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa mainModel.scheduleTransactionListRetrieval(); } } - private void createShortcuts(List list) { + private void createShortcuts(List list) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return; ShortcutManager sm = getSystemService(ShortcutManager.class); List shortcuts = new ArrayList<>(); int i = 0; - for (MobileLedgerProfile p : list) { + for (Profile p : list) { if (shortcuts.size() >= sm.getMaxShortcutCountPerActivity()) break; - if (!p.isPostingPermitted()) + if (!p.permitPosting()) continue; final ShortcutInfo.Builder builder = @@ -356,7 +358,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa ProfileThemedActivity.PARAM_PROFILE_ID, p.getId()) .putExtra( ProfileThemedActivity.PARAM_THEME, - p.getThemeHue())) + p.getTheme())) .setRank(i) .build(); shortcuts.add(si); @@ -364,7 +366,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa } sm.setDynamicShortcuts(shortcuts); } - private void onProfileListChanged(List newList) { + private void onProfileListChanged(List newList) { if ((newList == null) || newList.isEmpty()) { b.noProfilesLayout.setVisibility(View.VISIBLE); b.mainAppLayout.setVisibility(View.GONE); @@ -378,35 +380,31 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa (int) (getResources().getDimension(R.dimen.thumb_row_height) * newList.size())); Logger.debug("profiles", "profile list changed"); - mProfileListAdapter.notifyDataSetChanged(); + mProfileListAdapter.setProfileList(newList); createShortcuts(newList); } /** * called when the current profile has changed */ - private void onProfileChanged(MobileLedgerProfile profile) { + private void onProfileChanged(@Nullable Profile newProfile) { if (this.profile == null) { - if (profile == null) + if (newProfile == null) return; } else { - if (this.profile.equals(profile)) + if (this.profile.equals(newProfile)) return; } - boolean haveProfile = profile != null; + boolean haveProfile = newProfile != null; if (haveProfile) - setTitle(profile.getName()); + setTitle(newProfile.getName()); else setTitle(R.string.app_name); - mainModel.setProfile(profile); - - this.profile = profile; - - int newProfileTheme = haveProfile ? profile.getThemeHue() : -1; + int newProfileTheme = haveProfile ? newProfile.getTheme() : -1; if (newProfileTheme != Colors.profileThemeId) { Logger.debug("profiles", String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId, @@ -418,19 +416,18 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa return; } + final boolean sameProfileId = (newProfile != null) && (this.profile != null) && + this.profile.getId() == newProfile.getId(); + + this.profile = newProfile; + b.noProfilesLayout.setVisibility(haveProfile ? View.GONE : View.VISIBLE); b.pagerLayout.setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE); mProfileListAdapter.notifyDataSetChanged(); - mainModel.clearAccounts(); - mainModel.clearTransactions(); - if (haveProfile) { - Logger.debug("transactions", "requesting list reload"); - mainModel.scheduleTransactionListReload(); - - if (profile.isPostingPermitted()) { + if (newProfile.permitPosting()) { b.toolbar.setSubtitle(null); b.btnAddTransaction.show(); } @@ -445,6 +442,21 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa } updateLastUpdateTextFromDB(); + + if (sameProfileId) { + Logger.debug(TAG, String.format(Locale.ROOT, "Short-cut profile 'changed' to %d", + newProfile.getId())); + return; + } + + mainModel.stopTransactionsRetrieval(); + + mainModel.clearTransactions(); + + if (haveProfile) { + Logger.debug("transactions", "requesting list reload"); + mainModel.scheduleTransactionListReload(); + } } private void profileThemeChanged() { // un-hook all observed LiveData @@ -460,7 +472,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa public void fabNewTransactionClicked(View view) { Intent intent = new Intent(this, NewTransactionActivity.class); intent.putExtra(ProfileThemedActivity.PARAM_PROFILE_ID, profile.getId()); - intent.putExtra(ProfileThemedActivity.PARAM_THEME, profile.getThemeHue()); + intent.putExtra(ProfileThemedActivity.PARAM_THEME, profile.getTheme()); startActivity(intent); overridePendingTransition(R.anim.slide_in_up, R.anim.dummy); } @@ -523,18 +535,30 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa if (profile == null) return; - long lastUpdate = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L); - - Logger.debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", lastUpdate)); - if (lastUpdate == 0) { - Data.lastUpdateDate.postValue(null); - } - else { - Data.lastUpdateDate.postValue(new Date(lastUpdate)); - } - - scheduleDataRetrievalIfStale(lastUpdate); - + DB.get() + .getOptionDAO() + .load(profile.getId(), MLDB.OPT_LAST_SCRAPE) + .observe(this, opt -> { + long lastUpdate = 0; + if (opt != null) { + try { + lastUpdate = Long.parseLong(opt.getValue()); + } + catch (NumberFormatException ex) { + Logger.debug(TAG, String.format("Error parsing '%s' as long", opt.getValue()), + ex); + } + } + + if (lastUpdate == 0) { + Data.lastUpdateDate.postValue(null); + } + else { + Data.lastUpdateDate.postValue(new Date(lastUpdate)); + } + + scheduleDataRetrievalIfStale(lastUpdate); + }); } private void refreshLastUpdateInfo() { final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | @@ -601,7 +625,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa builder.setMessage(error); builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> { Logger.debug("error", "will start profile editor"); - MobileLedgerProfile.startEditProfileActivity(this, profile); + ProfileDetailActivity.start(this, profile); }); builder.create() .show(); @@ -643,7 +667,7 @@ public class MainActivity extends ProfileThemedActivity implements FabManager.Fa } } public void fabShouldShow() { - if ((profile != null) && profile.isPostingPermitted() && !b.drawerLayout.isOpen()) + if ((profile != null) && profile.permitPosting() && !b.drawerLayout.isOpen()) fabManager.showFab(); } @Override diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileThemedActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileThemedActivity.java index 4aa60241..43776add 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileThemedActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileThemedActivity.java @@ -28,7 +28,6 @@ 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.model.MobileLedgerProfile; import net.ktnx.mobileledger.utils.Colors; import net.ktnx.mobileledger.utils.Logger; @@ -39,7 +38,7 @@ public class ProfileThemedActivity extends CrashReportingActivity { public static final String TAG = "prf-thm-act"; protected static final String PARAM_PROFILE_ID = "profile-id"; protected static final String PARAM_THEME = "theme"; - protected MobileLedgerProfile mProfile; + protected Profile mProfile; private boolean themeSetUp = false; private boolean mIgnoreProfileChange; private int mThemeHue; @@ -81,7 +80,7 @@ public class ProfileThemedActivity extends CrashReportingActivity { } mProfile = profile; - int hue = profile.getThemeHue(); + int hue = profile.getTheme(); if (hue != mThemeHue) { storeProfilePref(profile); @@ -91,8 +90,8 @@ public class ProfileThemedActivity extends CrashReportingActivity { super.onCreate(savedInstanceState); } - public void storeProfilePref(MobileLedgerProfile profile) { - App.storeStartupProfileAndTheme(profile.getId(), profile.getThemeHue()); + public void storeProfilePref(Profile profile) { + App.storeStartupProfileAndTheme(profile.getId(), profile.getTheme()); } protected void initProfile() { long profileId = App.getStartupProfile(); @@ -119,6 +118,6 @@ public class ProfileThemedActivity extends CrashReportingActivity { profile = dao.getAnySync(); } - Data.postCurrentProfile(MobileLedgerProfile.fromDBO(profile)); + Data.postCurrentProfile(profile); } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java index 9b35b947..495a328f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java @@ -25,7 +25,7 @@ import android.os.Handler; import androidx.annotation.Nullable; import net.ktnx.mobileledger.R; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.db.DB; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.MobileLedgerDatabase; @@ -99,7 +99,7 @@ public class SplashActivity extends CrashReportingActivity { private static class DatabaseInitTask extends AsyncTask { @Override protected Void doInBackground(Void... voids) { - MobileLedgerProfile.loadAllFromDB(0); + long ignored = DB.get().getProfileDAO().getProfileCountSync(); return null; } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java index 870cf7ee..4e3a6cf8 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java @@ -177,8 +177,8 @@ class NewTransactionAccountRowItemHolder extends NewTransactionItemViewHolder { b.currencyButton.setOnClickListener(v -> { CurrencySelectorFragment cpf = new CurrencySelectorFragment(); cpf.showPositionAndPadding(); - cpf.setOnCurrencySelectedListener(c -> adapter.setItemCurrency(getAdapterPosition(), - (c == null) ? null : c.getName())); + cpf.setOnCurrencySelectedListener( + c -> adapter.setItemCurrency(getAdapterPosition(), c)); cpf.show(activity.getSupportFragmentManager(), "currency-selector"); }); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java index 11d014d3..791bbcef 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java @@ -20,17 +20,15 @@ package net.ktnx.mobileledger.ui.new_transaction; import android.content.Context; import android.content.Intent; import android.database.AbstractCursor; -import android.database.Cursor; +import android.os.AsyncTask; import android.os.Bundle; import android.os.ParcelFormatException; -import android.text.TextUtils; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.NonNull; import androidx.core.view.MenuCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; @@ -38,7 +36,6 @@ import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; import net.ktnx.mobileledger.BuildConfig; import net.ktnx.mobileledger.R; @@ -46,9 +43,11 @@ import net.ktnx.mobileledger.async.AsyncCrasher; import net.ktnx.mobileledger.async.DescriptionSelectedCallback; import net.ktnx.mobileledger.async.SendTransactionTask; import net.ktnx.mobileledger.async.TaskCallback; +import net.ktnx.mobileledger.dao.TransactionDAO; import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding; import net.ktnx.mobileledger.db.DB; import net.ktnx.mobileledger.db.TemplateHeader; +import net.ktnx.mobileledger.db.TransactionWithAccounts; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.MatchedTemplate; @@ -57,7 +56,6 @@ import net.ktnx.mobileledger.ui.QR; import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity; import net.ktnx.mobileledger.ui.templates.TemplatesActivity; import net.ktnx.mobileledger.utils.Logger; -import net.ktnx.mobileledger.utils.MLDB; import net.ktnx.mobileledger.utils.Misc; import java.util.ArrayList; @@ -349,86 +347,25 @@ public class NewTransactionActivity extends ProfileThemedActivity if (!model.accountListIsEmpty()) return; - String accFilter = mProfile.getPreferredAccountsFilter(); + AsyncTask.execute(() -> { + String accFilter = mProfile.getPreferredAccountsFilter(); - ArrayList params = new ArrayList<>(); - StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t"); + TransactionDAO trDao = DB.get() + .getTransactionDAO(); - if (!TextUtils.isEmpty(accFilter)) { - sb.append(" JOIN transaction_accounts ta") - .append(" ON ta.profile = t.profile") - .append(" AND ta.transaction_id = t.id"); - } - - sb.append(" WHERE t.description=?"); - params.add(description); - - if (!TextUtils.isEmpty(accFilter)) { - sb.append(" AND ta.account_name LIKE '%'||?||'%'"); - params.add(accFilter); - } + TransactionWithAccounts tr; - sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1"); - - final String sql = sb.toString(); - debug("description", sql); - debug("description", params.toString()); - - // FIXME: handle exceptions? - MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() { - @Override - public void onStart() { - model.incrementBusyCounter(); - } - @Override - public void onDone() { - model.decrementBusyCounter(); - } - @Override - public boolean onRow(@NonNull Cursor cursor) { - final long profileId = cursor.getLong(0); - final int transactionId = cursor.getInt(1); - runOnUiThread(() -> model.loadTransactionIntoModel(profileId, transactionId)); - return false; // limit 1, by the way - } - @Override - public void onNoRows() { - if (TextUtils.isEmpty(accFilter)) + if (Misc.emptyIsNull(accFilter) != null) { + tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter); + if (tr != null) { + model.loadTransactionIntoModel(tr); return; - - debug("description", "Trying transaction search without preferred account filter"); - - final String broaderSql = - "select t.profile, t.id from transactions t where t.description=?" + - " ORDER BY year desc, month desc, day desc LIMIT 1"; - params.remove(1); - debug("description", broaderSql); - debug("description", description); - - runOnUiThread(() -> Snackbar.make(b.newTransactionNav, - R.string.ignoring_preferred_account, Snackbar.LENGTH_INDEFINITE) - .show()); - - MLDB.queryInBackground(broaderSql, new String[]{description}, - new MLDB.CallbackHelper() { - @Override - public void onStart() { - model.incrementBusyCounter(); - } - @Override - public boolean onRow(@NonNull Cursor cursor) { - final long profileId = cursor.getLong(0); - final int transactionId = cursor.getInt(1); - runOnUiThread(() -> model.loadTransactionIntoModel(profileId, - transactionId)); - return false; - } - @Override - public void onDone() { - model.decrementBusyCounter(); - } - }); + } } + + tr = trDao.getFirstByDescriptionSync(description); + if (tr != null) + model.loadTransactionIntoModel(tr); }); } private void onFabPressed() { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java index c54c1179..7741d18a 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java @@ -41,12 +41,13 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.snackbar.Snackbar; import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.json.API; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransaction; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.QR; +import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity; import net.ktnx.mobileledger.utils.Logger; import org.jetbrains.annotations.NotNull; @@ -64,7 +65,7 @@ public class NewTransactionFragment extends Fragment { private NewTransactionItemsAdapter listAdapter; private NewTransactionModel viewModel; private OnNewTransactionFragmentInteractionListener mListener; - private MobileLedgerProfile mProfile; + private Profile mProfile; public NewTransactionFragment() { // Required empty public constructor setHasOptionsMenu(true); @@ -163,15 +164,15 @@ public class NewTransactionFragment extends Fragment { .append("\n\n") .append(error) .append("\n\n"); - if (mProfile.getApiVersion() - .equals(API.auto)) + if (API.valueOf(mProfile.getApiVersion()) + .equals(API.auto)) message.append( resources.getString(R.string.err_json_send_error_unsupported)); else { message.append(resources.getString(R.string.err_json_send_error_tail)); builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> { Logger.debug("error", "will start profile editor"); - MobileLedgerProfile.startEditProfileActivity(context, mProfile); + ProfileDetailActivity.start(context, mProfile); }); } builder.setMessage(message); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java index 81afa05d..feaf7459 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java @@ -34,6 +34,7 @@ import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding; import net.ktnx.mobileledger.db.TransactionDescriptionAutocompleteAdapter; import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.model.FutureDates; import net.ktnx.mobileledger.ui.DatePickerFragment; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; @@ -282,7 +283,7 @@ class NewTransactionHeaderItemHolder extends NewTransactionItemViewHolder } private void pickTransactionDate() { DatePickerFragment picker = new DatePickerFragment(); - picker.setFutureDates(mProfile.getFutureDates()); + picker.setFutureDates(FutureDates.valueOf(mProfile.getFutureDates())); picker.setOnDatePickedListener(this); picker.setCurrentDateFromText(b.newTransactionDate.getText()); picker.show(((NewTransactionActivity) b.getRoot() diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java index 38c2796a..f78fdbe8 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java @@ -22,12 +22,12 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; abstract class NewTransactionItemViewHolder extends RecyclerView.ViewHolder { final NewTransactionItemsAdapter mAdapter; - final MobileLedgerProfile mProfile; + final Profile mProfile; public NewTransactionItemViewHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) { super(itemView); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java index 3760d70f..73ddc8fe 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java @@ -28,7 +28,7 @@ import androidx.recyclerview.widget.RecyclerView; import net.ktnx.mobileledger.databinding.NewTransactionAccountRowBinding; import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.utils.Logger; import java.util.List; @@ -61,9 +61,9 @@ class NewTransactionItemsAdapter extends RecyclerView.Adapter simulateSave = new InertMutableLiveData<>(false); private final AtomicInteger busyCounter = new AtomicInteger(0); private final MutableLiveData busyFlag = new InertMutableLiveData<>(false); - private final Observer profileObserver = profile -> { + private final Observer profileObserver = profile -> { showCurrency.postValue(profile.getShowCommodityByDefault()); showComments.postValue(profile.getShowCommentsByDefault()); }; @@ -461,23 +462,19 @@ public class NewTransactionModel extends ViewModel { return tr; } - void loadTransactionIntoModel(long profileId, int transactionId) { + void loadTransactionIntoModel(@NonNull TransactionWithAccounts tr) { List newList = new ArrayList<>(); Item.resetIdDispenser(); - LedgerTransaction tr; - MobileLedgerProfile profile = Data.getProfile(profileId); - if (profile == null) - throw new RuntimeException(String.format( - "Unable to find profile %s, which is supposed to contain transaction %d", - profileId, transactionId)); - tr = profile.loadTransaction(transactionId); - TransactionHead head = new TransactionHead(tr.getDescription()); - head.setComment(tr.getComment()); + TransactionHead head = new TransactionHead(tr.transaction.getDescription()); + head.setComment(tr.transaction.getComment()); newList.add(head); - List accounts = tr.getAccounts(); + List accounts = new ArrayList<>(); + for (net.ktnx.mobileledger.db.TransactionAccount acc : tr.accounts) { + accounts.add(new LedgerTransactionAccount(acc)); + } TransactionAccount firstNegative = null; TransactionAccount firstPositive = null; @@ -526,9 +523,10 @@ public class NewTransactionModel extends ViewModel { moveItemLast(newList, singlePositiveIndex); } - setItems(newList); - - noteFocusChanged(1, FocusedElement.Amount); + new Handler(Looper.getMainLooper()).post(() -> { + setItems(newList); + noteFocusChanged(1, FocusedElement.Amount); + }); } /** * A transaction is submittable if: diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java index d860a678..d389dd8a 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java @@ -17,25 +17,28 @@ package net.ktnx.mobileledger.ui.profiles; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.appbar.CollapsingToolbarLayout; + import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.activity.CrashReportingActivity; import net.ktnx.mobileledger.utils.Colors; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Locale; - import static net.ktnx.mobileledger.utils.Logger.debug; /** @@ -45,40 +48,41 @@ import static net.ktnx.mobileledger.utils.Logger.debug; * in a ProfileListActivity (not really). */ public class ProfileDetailActivity extends CrashReportingActivity { - private MobileLedgerProfile profile = null; private ProfileDetailFragment mFragment; + public static void start(Context context, @Nullable Profile profile) { + Intent starter = new Intent(context, ProfileDetailActivity.class); + if (profile != null) { + starter.putExtra(ProfileDetailFragment.ARG_ITEM_ID, profile.getId()); + starter.putExtra(ProfileDetailFragment.ARG_ITEM_ID, profile.getTheme()); + } + context.startActivity(starter); + } @NotNull private ProfileDetailModel getModel() { return new ViewModelProvider(this).get(ProfileDetailModel.class); } @Override protected void onCreate(Bundle savedInstanceState) { - final int index = getIntent().getIntExtra(ProfileDetailFragment.ARG_ITEM_ID, -1); - - if (index != -1) { - ArrayList profiles = Data.profiles.getValue(); - if (profiles != null) { - profile = profiles.get(index); - if (profile == null) - throw new AssertionError( - String.format("Can't get profile " + "(index:%d) from the global list", - index)); - - debug("profiles", String.format(Locale.ENGLISH, "Editing profile %s (%s); hue=%d", - profile.getName(), profile.getId(), profile.getThemeHue())); - } - } + final long id = getIntent().getLongExtra(ProfileDetailFragment.ARG_ITEM_ID, -1); + + if (id == -1) + throw new RuntimeException("Invalid or missing profile ID"); + + DB.get() + .getProfileDAO() + .getById(id) + .observe(this, this::setProfile); + + int themeHue = getIntent().getIntExtra(ProfileDetailFragment.ARG_HUE, -1); super.onCreate(savedInstanceState); - int themeHue; - if (profile != null) - themeHue = profile.getThemeHue(); - else { + if (themeHue == -1) { themeHue = Colors.getNewProfileThemeHue(Data.profiles.getValue()); } Colors.setupTheme(this, themeHue); final ProfileDetailModel model = getModel(); model.initialThemeHue = themeHue; + model.setThemeId(themeHue); setContentView(R.layout.activity_profile_detail); Toolbar toolbar = findViewById(R.id.detail_toolbar); setSupportActionBar(toolbar); @@ -103,7 +107,6 @@ public class ProfileDetailActivity extends CrashReportingActivity { // Create the detail fragment and add it to the activity // using a fragment transaction. Bundle arguments = new Bundle(); - arguments.putInt(ProfileDetailFragment.ARG_ITEM_ID, index); arguments.putInt(ProfileDetailFragment.ARG_HUE, themeHue); mFragment = new ProfileDetailFragment(); mFragment.setArguments(arguments); @@ -112,6 +115,17 @@ public class ProfileDetailActivity extends CrashReportingActivity { .commit(); } } + private void setProfile(Profile profile) { + ProfileDetailModel model = new ViewModelProvider(this).get(ProfileDetailModel.class); + CollapsingToolbarLayout appBarLayout = findViewById(R.id.toolbar_layout); + if (appBarLayout != null) { + if (profile != null) + appBarLayout.setTitle(profile.getName()); + else + appBarLayout.setTitle(getResources().getString(R.string.new_profile_title)); + } + model.setValuesFromProfile(profile); + } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java index f5b48aef..10091895 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java @@ -40,16 +40,18 @@ import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; -import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.textfield.TextInputLayout; import net.ktnx.mobileledger.BuildConfig; import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.dao.ProfileDAO; import net.ktnx.mobileledger.databinding.ProfileDetailBinding; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.json.API; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.model.FutureDates; import net.ktnx.mobileledger.ui.CurrencySelectorFragment; import net.ktnx.mobileledger.ui.HueRingDialog; import net.ktnx.mobileledger.utils.Colors; @@ -60,8 +62,7 @@ import org.jetbrains.annotations.NotNull; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Objects; +import java.util.List; import static net.ktnx.mobileledger.utils.Logger.debug; @@ -79,7 +80,7 @@ public class ProfileDetailFragment extends Fragment { public static final String ARG_HUE = "hue"; @NonNls - private MobileLedgerProfile mProfile; + private Profile mProfile; private boolean defaultCommoditySet; private boolean syncingModelFromUI = false; private ProfileDetailBinding binding; @@ -97,7 +98,7 @@ public class ProfileDetailFragment extends Fragment { inflater.inflate(R.menu.profile_details, menu); final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete); menuDeleteProfile.setOnMenuItemClickListener(item -> onDeleteProfile()); - final ArrayList profiles = Data.profiles.getValue(); + final List profiles = Data.profiles.getValue(); if (BuildConfig.DEBUG) { final MenuItem menuWipeProfileData = menu.findItem(R.id.menuWipeData); @@ -111,17 +112,9 @@ public class ProfileDetailFragment extends Fragment { builder.setMessage(R.string.remove_profile_dialog_message); builder.setPositiveButton(R.string.Remove, (dialog, which) -> { debug("profiles", String.format("[fragment] removing profile %s", mProfile.getId())); - mProfile.removeFromDB(); - ArrayList oldList = Data.profiles.getValue(); - if (oldList == null) - throw new AssertionError(); - ArrayList newList = new ArrayList<>(oldList); - newList.remove(mProfile); - Data.profiles.setValue(newList); - if (mProfile.equals(Data.getProfile())) { - debug("profiles", "[fragment] setting current profile to 0"); - Data.setCurrentProfile(newList.get(0)); - } + ProfileDAO dao = DB.get() + .getProfileDAO(); + dao.delete(mProfile, () -> dao.updateOrderSync(dao.getAllOrderedSync())); final FragmentActivity activity = getActivity(); if (activity != null) @@ -133,24 +126,8 @@ public class ProfileDetailFragment extends Fragment { private boolean onWipeDataMenuClicked() { // this is a development option, so no confirmation mProfile.wipeAllData(); - if (mProfile.equals(Data.getProfile())) - triggerProfileChange(); return true; } - private void triggerProfileChange() { - int index = Data.getProfileIndex(mProfile); - MobileLedgerProfile newProfile = new MobileLedgerProfile(mProfile); - final ArrayList profiles = - Objects.requireNonNull(Data.profiles.getValue()); - profiles.set(index, newProfile); - - ProfilesRecyclerViewAdapter viewAdapter = ProfilesRecyclerViewAdapter.getInstance(); - if (viewAdapter != null) - viewAdapter.notifyItemChanged(index); - - if (mProfile.equals(Data.getProfile())) - Data.setCurrentProfile(newProfile); - } private void hookTextChangeSyncRoutine(TextView view, TextChangeSyncRoutine syncRoutine) { view.addTextChangedListener(new TextWatcher() { @Override @@ -176,30 +153,12 @@ public class ProfileDetailFragment extends Fragment { if (context == null) return; - if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) { - int index = getArguments().getInt(ARG_ITEM_ID, -1); - ArrayList profiles = Data.profiles.getValue(); - if ((profiles != null) && (index != -1) && (index < profiles.size())) - mProfile = profiles.get(index); - - Activity activity = this.getActivity(); - if (activity == null) - throw new AssertionError(); - CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout); - if (appBarLayout != null) { - if (mProfile != null) - appBarLayout.setTitle(mProfile.getName()); - else - appBarLayout.setTitle(getResources().getString(R.string.new_profile_title)); - } - } - final LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner(); final ProfileDetailModel model = getModel(); model.observeDefaultCommodity(viewLifecycleOwner, c -> { if (c != null) - setDefaultCommodity(c.getName()); + setDefaultCommodity(c); else resetDefaultCommodity(); }); @@ -325,11 +284,6 @@ public class ProfileDetailFragment extends Fragment { hookClearErrorOnFocusListener(binding.authUserName, binding.authUserNameLayout); hookClearErrorOnFocusListener(binding.password, binding.passwordLayout); - if (savedInstanceState == null) { - model.setValuesFromProfile(mProfile, getArguments().getInt(ARG_HUE, -1)); - } - checkInsecureSchemeWithAuth(); - binding.url.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -380,32 +334,32 @@ public class ProfileDetailFragment extends Fragment { }); menu.show(); } - private MobileLedgerProfile.FutureDates futureDatesSettingFromMenuItemId(int itemId) { + private FutureDates futureDatesSettingFromMenuItemId(int itemId) { if (itemId == R.id.menu_future_dates_7) { - return MobileLedgerProfile.FutureDates.OneWeek; + return FutureDates.OneWeek; } else if (itemId == R.id.menu_future_dates_14) { - return MobileLedgerProfile.FutureDates.TwoWeeks; + return FutureDates.TwoWeeks; } else if (itemId == R.id.menu_future_dates_30) { - return MobileLedgerProfile.FutureDates.OneMonth; + return FutureDates.OneMonth; } else if (itemId == R.id.menu_future_dates_60) { - return MobileLedgerProfile.FutureDates.TwoMonths; + return FutureDates.TwoMonths; } else if (itemId == R.id.menu_future_dates_90) { - return MobileLedgerProfile.FutureDates.ThreeMonths; + return FutureDates.ThreeMonths; } else if (itemId == R.id.menu_future_dates_180) { - return MobileLedgerProfile.FutureDates.SixMonths; + return FutureDates.SixMonths; } else if (itemId == R.id.menu_future_dates_365) { - return MobileLedgerProfile.FutureDates.OneYear; + return FutureDates.OneYear; } else if (itemId == R.id.menu_future_dates_all) { - return MobileLedgerProfile.FutureDates.All; + return FutureDates.All; } - return MobileLedgerProfile.FutureDates.None; + return FutureDates.None; } @NotNull private ProfileDetailModel getModel() { @@ -416,40 +370,19 @@ public class ProfileDetailFragment extends Fragment { return; ProfileDetailModel model = getModel(); - final ArrayList profiles = - Objects.requireNonNull(Data.profiles.getValue()); + ProfileDAO dao = DB.get() + .getProfileDAO(); if (mProfile != null) { - int pos = Data.profiles.getValue() - .indexOf(mProfile); - mProfile = new MobileLedgerProfile(mProfile); model.updateProfile(mProfile); - mProfile.storeInDB(); + dao.update(mProfile, null); debug("profiles", "profile stored in DB"); - profiles.set(pos, mProfile); // debug("profiles", String.format("Selected item is %d", mProfile.getThemeHue())); - - final MobileLedgerProfile currentProfile = Data.getProfile(); - if (mProfile.getId() == currentProfile.getId()) { - Data.setCurrentProfile(mProfile); - } - - ProfilesRecyclerViewAdapter viewAdapter = ProfilesRecyclerViewAdapter.getInstance(); - if (viewAdapter != null) - viewAdapter.notifyItemChanged(pos); } else { - mProfile = new MobileLedgerProfile(0); + mProfile = new Profile(); model.updateProfile(mProfile); - mProfile.storeInDB(); - final ArrayList newList = new ArrayList<>(profiles); - newList.add(mProfile); - Data.profiles.setValue(newList); - MobileLedgerProfile.storeProfilesOrder(); - - // first profile ever? - if (newList.size() == 1) - Data.setCurrentProfile(mProfile); + dao.insertLast(mProfile, null); } Activity activity = getActivity(); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java index 92d3ab50..e1f956ac 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java @@ -25,10 +25,10 @@ import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; import net.ktnx.mobileledger.App; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.json.API; -import net.ktnx.mobileledger.model.Currency; +import net.ktnx.mobileledger.model.FutureDates; import net.ktnx.mobileledger.model.HledgerVersion; -import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.utils.Colors; import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; @@ -48,9 +48,9 @@ public class ProfileDetailModel extends ViewModel { private static final String HTTPS_URL_START = "https://"; private final MutableLiveData profileName = new MutableLiveData<>(); private final MutableLiveData postingPermitted = new MutableLiveData<>(true); - private final MutableLiveData defaultCommodity = new MutableLiveData<>(null); - private final MutableLiveData futureDates = - new MutableLiveData<>(MobileLedgerProfile.FutureDates.None); + private final MutableLiveData defaultCommodity = new MutableLiveData<>(null); + private final MutableLiveData futureDates = + new MutableLiveData<>(FutureDates.None); private final MutableLiveData showCommodityByDefault = new MutableLiveData<>(false); private final MutableLiveData showCommentsByDefault = new MutableLiveData<>(true); private final MutableLiveData useAuthentication = new MutableLiveData<>(false); @@ -97,24 +97,24 @@ public class ProfileDetailModel extends ViewModel { void observeShowCommentsByDefault(LifecycleOwner lfo, Observer o) { showCommentsByDefault.observe(lfo, o); } - MobileLedgerProfile.FutureDates getFutureDates() { + FutureDates getFutureDates() { return futureDates.getValue(); } - void setFutureDates(MobileLedgerProfile.FutureDates newValue) { + void setFutureDates(FutureDates newValue) { if (newValue != futureDates.getValue()) futureDates.setValue(newValue); } - void observeFutureDates(LifecycleOwner lfo, Observer o) { + void observeFutureDates(LifecycleOwner lfo, Observer o) { futureDates.observe(lfo, o); } - Currency getDefaultCommodity() { + String getDefaultCommodity() { return defaultCommodity.getValue(); } - void setDefaultCommodity(Currency newValue) { - if (newValue != defaultCommodity.getValue()) + void setDefaultCommodity(String newValue) { + if (!Misc.equalStrings(newValue, defaultCommodity.getValue())) defaultCommodity.setValue(newValue); } - void observeDefaultCommodity(LifecycleOwner lfo, Observer o) { + void observeDefaultCommodity(LifecycleOwner lfo, Observer o) { defaultCommodity.observe(lfo, o); } Boolean getShowCommodityByDefault() { @@ -223,11 +223,10 @@ public class ProfileDetailModel extends ViewModel { void observeDetectingHledgerVersion(LifecycleOwner lfo, Observer o) { detectingHledgerVersion.observe(lfo, o); } - void setValuesFromProfile(MobileLedgerProfile mProfile, int newProfileHue) { - final int profileThemeId; + void setValuesFromProfile(Profile mProfile) { if (mProfile != null) { profileName.setValue(mProfile.getName()); - postingPermitted.setValue(mProfile.isPostingPermitted()); + postingPermitted.setValue(mProfile.permitPosting()); showCommentsByDefault.setValue(mProfile.getShowCommentsByDefault()); showCommodityByDefault.setValue(mProfile.getShowCommodityByDefault()); { @@ -235,17 +234,21 @@ public class ProfileDetailModel extends ViewModel { if (TextUtils.isEmpty(comm)) setDefaultCommodity(null); else - setDefaultCommodity(new Currency(-1, comm)); + setDefaultCommodity(comm); } - futureDates.setValue(mProfile.getFutureDates()); - apiVersion.setValue(mProfile.getApiVersion()); + futureDates.setValue( + FutureDates.valueOf(mProfile.getFutureDates())); + apiVersion.setValue(API.valueOf(mProfile.getApiVersion())); url.setValue(mProfile.getUrl()); - useAuthentication.setValue(mProfile.isAuthEnabled()); - authUserName.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : ""); - authPassword.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : ""); + useAuthentication.setValue(mProfile.useAuthentication()); + authUserName.setValue(mProfile.useAuthentication() ? mProfile.getAuthUser() : ""); + authPassword.setValue(mProfile.useAuthentication() ? mProfile.getAuthPassword() : ""); preferredAccountsFilter.setValue(mProfile.getPreferredAccountsFilter()); - themeId.setValue(mProfile.getThemeHue()); - detectedVersion.setValue(mProfile.getDetectedVersion()); + themeId.setValue(mProfile.getTheme()); + detectedVersion.setValue(mProfile.detectedVersionPre_1_19() ? new HledgerVersion(true) + : new HledgerVersion( + mProfile.getDetectedVersionMajor(), + mProfile.getDetectedVersionMinor())); } else { profileName.setValue(null); @@ -253,32 +256,35 @@ public class ProfileDetailModel extends ViewModel { postingPermitted.setValue(true); showCommentsByDefault.setValue(true); showCommodityByDefault.setValue(false); - setFutureDates(MobileLedgerProfile.FutureDates.None); + setFutureDates(FutureDates.None); setApiVersion(API.auto); useAuthentication.setValue(false); authUserName.setValue(""); authPassword.setValue(""); preferredAccountsFilter.setValue(null); - themeId.setValue(newProfileHue); detectedVersion.setValue(null); } } - void updateProfile(MobileLedgerProfile mProfile) { + void updateProfile(Profile mProfile) { mProfile.setName(profileName.getValue()); mProfile.setUrl(url.getValue()); - mProfile.setPostingPermitted(postingPermitted.getValue()); + mProfile.setPermitPosting(postingPermitted.getValue()); mProfile.setShowCommentsByDefault(showCommentsByDefault.getValue()); - Currency c = defaultCommodity.getValue(); - mProfile.setDefaultCommodity((c == null) ? null : c.getName()); + mProfile.setDefaultCommodity(defaultCommodity.getValue()); mProfile.setShowCommodityByDefault(showCommodityByDefault.getValue()); mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getValue()); - mProfile.setAuthEnabled(useAuthentication.getValue()); - mProfile.setAuthUserName(authUserName.getValue()); + mProfile.setUseAuthentication(useAuthentication.getValue()); + mProfile.setAuthUser(authUserName.getValue()); mProfile.setAuthPassword(authPassword.getValue()); - mProfile.setThemeHue(themeId.getValue()); - mProfile.setFutureDates(futureDates.getValue()); - mProfile.setApiVersion(apiVersion.getValue()); - mProfile.setDetectedVersion(detectedVersion.getValue()); + mProfile.setTheme(themeId.getValue()); + mProfile.setFutureDates(futureDates.getValue() + .toInt()); + mProfile.setApiVersion(apiVersion.getValue() + .toInt()); + HledgerVersion version = detectedVersion.getValue(); + mProfile.setDetectedVersionPre_1_19(version.isPre_1_20_1()); + mProfile.setDetectedVersionMajor(version.getMajor()); + mProfile.setDetectedVersionMinor(version.getMinor()); } synchronized public void triggerVersionDetection() { if (versionDetectionThread != null) @@ -297,7 +303,7 @@ public class ProfileDetailModel extends ViewModel { } private HledgerVersion detectVersion() { App.setAuthenticationDataFromProfileModel(model); - HttpURLConnection http = null; + HttpURLConnection http; try { http = NetworkUtil.prepareConnection(model.getUrl(), "version", model.getUseAuthentication()); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java index e7346b1b..688cd6d4 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java @@ -17,8 +17,6 @@ package net.ktnx.mobileledger.ui.profiles; -import android.content.Context; -import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.view.LayoutInflater; import android.view.View; @@ -30,39 +28,48 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.lifecycle.MutableLiveData; +import androidx.recyclerview.widget.AsyncListDiffer; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import net.ktnx.mobileledger.R; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.model.Data; -import net.ktnx.mobileledger.model.MobileLedgerProfile; -import net.ktnx.mobileledger.ui.activity.MainActivity; import net.ktnx.mobileledger.utils.Colors; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import static net.ktnx.mobileledger.utils.Logger.debug; public class ProfilesRecyclerViewAdapter extends RecyclerView.Adapter { - private static WeakReference instanceRef; public final MutableLiveData editingProfiles = new MutableLiveData<>(false); - private final View.OnClickListener mOnClickListener = view -> { - MobileLedgerProfile profile = (MobileLedgerProfile) ((View) view.getParent()).getTag(); - editProfile(view, profile); - }; private final ItemTouchHelper rearrangeHelper; + private final AsyncListDiffer listDiffer; private RecyclerView recyclerView; private boolean animationsEnabled = true; + public ProfilesRecyclerViewAdapter() { - instanceRef = new WeakReference<>(this); debug("flow", "ProfilesRecyclerViewAdapter.new()"); + setHasStableIds(true); + listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull Profile oldItem, @NonNull Profile newItem) { + return oldItem.getId() == newItem.getId(); + } + @Override + public boolean areContentsTheSame(@NonNull Profile oldItem, @NonNull Profile newItem) { + return oldItem.equals(newItem); + } + }); + ItemTouchHelper.Callback cb = new ItemTouchHelper.Callback() { @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @@ -73,13 +80,13 @@ public class ProfilesRecyclerViewAdapter public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - final ArrayList profiles = Data.profiles.getValue(); - if (profiles == null) - throw new AssertionError(); + final List profiles = new ArrayList<>(listDiffer.getCurrentList()); Collections.swap(profiles, viewHolder.getAdapterPosition(), target.getAdapterPosition()); - MobileLedgerProfile.storeProfilesOrder(); - notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + DB.get() + .getProfileDAO() + .updateOrder(profiles, null); +// notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition()); return true; } @Override @@ -88,9 +95,14 @@ public class ProfilesRecyclerViewAdapter }; rearrangeHelper = new ItemTouchHelper(cb); } - public static @Nullable - ProfilesRecyclerViewAdapter getInstance() { - return instanceRef.get(); + @Override + public long getItemId(int position) { + return listDiffer.getCurrentList() + .get(position) + .getId(); + } + public void setProfileList(List list) { + listDiffer.submitList(list); } public void setAnimationsEnabled(boolean animationsEnabled) { this.animationsEnabled = animationsEnabled; @@ -132,74 +144,22 @@ public class ProfilesRecyclerViewAdapter else startEditingProfiles(); } - private void editProfile(View view, MobileLedgerProfile profile) { - int index = Data.getProfileIndex(profile); - Context context = view.getContext(); - Intent intent = new Intent(context, ProfileDetailActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); - if (index != -1) - intent.putExtra(ProfileDetailFragment.ARG_ITEM_ID, index); - - context.startActivity(intent); - } - private void onProfileRowClicked(View v) { - if (editingProfiles()) - return; - MobileLedgerProfile profile = (MobileLedgerProfile) v.getTag(); - if (profile == null) - throw new IllegalStateException("Profile row without associated profile"); - debug("profiles", "Setting profile to " + profile.getName()); - if (Data.getProfile() != profile) { - Data.drawerOpen.setValue(false); - ((MainActivity) v.getContext()).storeProfilePref(profile); - } - Data.setCurrentProfile(profile); - } @NonNull @Override public ProfileListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.profile_list_content, parent, false); - ProfileListViewHolder holder = new ProfileListViewHolder(view); - - holder.mRow.setOnClickListener(this::onProfileRowClicked); - holder.mTitle.setOnClickListener(v -> { - View row = (View) v.getParent(); - onProfileRowClicked(row); - }); - holder.mColorTag.setOnClickListener(v -> { - View row = (View) v.getParent() - .getParent(); - onProfileRowClicked(row); - }); - holder.mTitle.setOnLongClickListener(v -> { - flipEditingProfiles(); - return true; - }); - - View.OnTouchListener dragStarter = (v, event) -> { - if (rearrangeHelper != null && editingProfiles()) { - rearrangeHelper.startDrag(holder); - return true; - } - return false; - }; - - holder.tagAndHandleLayout.setOnTouchListener(dragStarter); - return holder; + return new ProfileListViewHolder(view); } @Override public void onBindViewHolder(@NonNull final ProfileListViewHolder holder, int position) { - final ArrayList profiles = Data.profiles.getValue(); - if (profiles == null) - throw new AssertionError(); - final MobileLedgerProfile profile = profiles.get(position); - final MobileLedgerProfile currentProfile = Data.getProfile(); + final Profile profile = listDiffer.getCurrentList() + .get(position); + final Profile currentProfile = Data.getProfile(); // debug("profiles", String.format(Locale.ENGLISH, "pos %d: %s, current: %s", position, // profile.getUuid(), currentProfile.getUuid())); - holder.itemView.setTag(profile); - int hue = profile.getThemeHue(); + int hue = profile.getTheme(); if (hue == -1) holder.mColorTag.setBackgroundColor( Colors.getPrimaryColorForHue(Colors.DEFAULT_HUE_DEG)); @@ -209,9 +169,13 @@ public class ProfilesRecyclerViewAdapter holder.mTitle.setText(profile.getName()); // holder.mSubTitle.setText(profile.getUrl()); - holder.mEditButton.setOnClickListener(mOnClickListener); + holder.mEditButton.setOnClickListener(view -> { + Profile p = listDiffer.getCurrentList() + .get(holder.getAdapterPosition()); + ProfileDetailActivity.start(view.getContext(), p); + }); - final boolean sameProfile = currentProfile.equals(profile); + final boolean sameProfile = currentProfile.getId() == profile.getId(); holder.itemView.setBackground( sameProfile ? new ColorDrawable(Colors.tableRowDarkBG) : null); if (editingProfiles()) { @@ -239,10 +203,10 @@ public class ProfilesRecyclerViewAdapter } @Override public int getItemCount() { - final ArrayList profiles = Data.profiles.getValue(); - return profiles != null ? profiles.size() : 0; + return listDiffer.getCurrentList() + .size(); } - static class ProfileListViewHolder extends RecyclerView.ViewHolder { + class ProfileListViewHolder extends RecyclerView.ViewHolder { final TextView mEditButton; final TextView mTitle, mColorTag; final LinearLayout tagAndHandleLayout; @@ -257,6 +221,47 @@ public class ProfilesRecyclerViewAdapter mRearrangeHandle = view.findViewById(R.id.profile_list_rearrange_handle); tagAndHandleLayout = view.findViewById(R.id.handle_and_tag); mRow = (ConstraintLayout) view; + + + mRow.setOnClickListener(this::onProfileRowClicked); + mTitle.setOnClickListener(v -> { + View row = (View) v.getParent(); + onProfileRowClicked(row); + }); + mColorTag.setOnClickListener(v -> { + View row = (View) v.getParent() + .getParent(); + onProfileRowClicked(row); + }); + mTitle.setOnLongClickListener(v -> { + flipEditingProfiles(); + return true; + }); + + View.OnTouchListener dragStarter = (v, event) -> { + if (rearrangeHelper != null && editingProfiles()) { + rearrangeHelper.startDrag(this); + return true; + } + return false; + }; + + tagAndHandleLayout.setOnTouchListener(dragStarter); + } + private void onProfileRowClicked(View v) { + if (editingProfiles()) + return; + Profile profile = listDiffer.getCurrentList() + .get(getAdapterPosition()); + if (Data.getProfile() != profile) { + debug("profiles", "Setting profile to " + profile.getName()); + Data.drawerOpen.setValue(false); + Data.setCurrentProfile(profile); + } + else + debug("profiles", + "Not setting profile to the current profile " + profile.getName()); } + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java index 8d0e29e6..c49e44d0 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java @@ -218,7 +218,7 @@ public class TemplateDetailsViewModel extends ViewModel { .getTemplateDAO(); TemplateHeader dbHeader = modelHeader.toDBO(); if (newPattern) { - dbHeader.setId(null); + dbHeader.setId(0L); dbHeader.setId(mPatternId = headerDAO.insertSync(dbHeader)); } else @@ -239,7 +239,7 @@ public class TemplateDetailsViewModel extends ViewModel { dbAccount.setTemplateId(mPatternId); dbAccount.setPosition(i); if (dbAccount.getId() < 0) { - dbAccount.setId(null); + dbAccount.setId(0); dbAccount.setId(taDAO.insertSync(dbAccount)); } else @@ -255,6 +255,10 @@ public class TemplateDetailsViewModel extends ViewModel { } private ArrayList copyItems() { List oldList = items.getValue(); + + if (oldList == null) + return new ArrayList<>(); + ArrayList result = new ArrayList<>(oldList.size()); for (TemplateDetailsItem item : oldList) { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java index 98555658..67a4922b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java @@ -75,8 +75,8 @@ public class TransactionListAdapter extends RecyclerView.Adapter { // debug("tmp", "direct onItemClick"); diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/Colors.java b/app/src/main/java/net/ktnx/mobileledger/utils/Colors.java index adefa6f3..fa56c19f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/Colors.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/Colors.java @@ -28,12 +28,13 @@ import androidx.lifecycle.MutableLiveData; import net.ktnx.mobileledger.BuildConfig; import net.ktnx.mobileledger.R; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.db.Profile; import net.ktnx.mobileledger.ui.HueRing; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -151,7 +152,7 @@ public class Colors { } return colors; } - public static int getNewProfileThemeHue(ArrayList profiles) { + public static int getNewProfileThemeHue(List profiles) { if ((profiles == null) || (profiles.size() == 0)) return DEFAULT_HUE_DEG; @@ -159,14 +160,14 @@ public class Colors { if (profiles.size() == 1) { int opposite = profiles.get(0) - .getThemeHue() + 180; + .getTheme() + 180; opposite %= 360; chosenHue = opposite; } else { ArrayList hues = new ArrayList<>(); - for (MobileLedgerProfile p : profiles) { - int hue = p.getThemeHue(); + for (Profile p : profiles) { + int hue = p.getTheme(); if (hue == -1) hue = DEFAULT_HUE_DEG; hues.add(hue); diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java index c9f0151b..97c09911 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java @@ -19,7 +19,7 @@ package net.ktnx.mobileledger.utils; import androidx.annotation.NonNull; -import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.db.Profile; import org.jetbrains.annotations.NotNull; @@ -32,9 +32,9 @@ import static net.ktnx.mobileledger.utils.Logger.debug; public final class NetworkUtil { private static final int thirtySeconds = 30000; @NotNull - public static HttpURLConnection prepareConnection(@NonNull MobileLedgerProfile profile, + public static HttpURLConnection prepareConnection(@NonNull Profile profile, @NonNull String path) throws IOException { - return prepareConnection(profile.getUrl(), path, profile.isAuthEnabled()); + return prepareConnection(profile.getUrl(), path, profile.useAuthentication()); } public static HttpURLConnection prepareConnection(@NonNull String url, @NonNull String path, boolean authEnabled) throws IOException { diff --git a/app/src/test/java/net/ktnx/mobileledger/model/MobileLedgerProfileTest.java b/app/src/test/java/net/ktnx/mobileledger/model/MobileLedgerProfileTest.java deleted file mode 100644 index edbd5a74..00000000 --- a/app/src/test/java/net/ktnx/mobileledger/model/MobileLedgerProfileTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright © 2020 Damyan Ivanov. - * This file is part of MoLe. - * MoLe is free software: you can distribute it and/or modify it - * under the term of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your opinion), any later version. - * - * MoLe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License terms for details. - * - * You should have received a copy of the GNU General Public License - * along with MoLe. If not, see . - */ - -package net.ktnx.mobileledger.model; - -import org.junit.Test; -import org.junit.internal.ArrayComparisonFailure; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; - -public class MobileLedgerProfileTest { - private List listFromArray(LedgerAccount[] array) { - ArrayList result = new ArrayList<>(); - Collections.addAll(result, array); - - return result; - } - private void aTest(LedgerAccount[] oldList, LedgerAccount[] newList, - LedgerAccount[] expectedResult) { - List result = - MobileLedgerProfile.mergeAccountListsFromWeb(listFromArray(oldList), - listFromArray(newList)); - assertArrayEquals(expectedResult, result.toArray()); - } - private void negTest(LedgerAccount[] oldList, LedgerAccount[] newList, - LedgerAccount[] expectedResult) { - List result = - MobileLedgerProfile.mergeAccountListsFromWeb(listFromArray(oldList), - listFromArray(newList)); - assertThrows(ArrayComparisonFailure.class, - () -> assertArrayEquals(expectedResult, result.toArray())); - } - private LedgerAccount[] emptyArray() { - return new LedgerAccount[]{}; - } - @Test - public void mergeEmptyLists() { - aTest(emptyArray(), emptyArray(), emptyArray()); - } - @Test - public void mergeIntoEmptyLists() { - LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null); - aTest(emptyArray(), new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1}); - } - @Test - public void mergeEmptyList() { - LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null); - aTest(new LedgerAccount[]{acc1}, emptyArray(), emptyArray()); - } - @Test - public void mergeEqualLists() { - LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null); - aTest(new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1}); - } - @Test - public void mergeFlags() { - LedgerAccount acc1a = new LedgerAccount(null, "Acc1", null); - LedgerAccount acc1b = new LedgerAccount(null, "Acc1", null); - acc1b.setExpanded(true); - acc1b.setAmountsExpanded(true); - List merged = MobileLedgerProfile.mergeAccountListsFromWeb( - listFromArray(new LedgerAccount[]{acc1a}), - listFromArray(new LedgerAccount[]{acc1b})); - assertArrayEquals(new LedgerAccount[]{acc1b}, merged.toArray()); - assertSame(merged.get(0), acc1a); - // restore original values, modified by the merge - acc1a.setExpanded(false); - acc1a.setAmountsExpanded(false); - negTest(new LedgerAccount[]{acc1a}, new LedgerAccount[]{acc1b}, - new LedgerAccount[]{new LedgerAccount(null, "Acc1", null)}); - } -} \ No newline at end of file -- 2.39.2