]> git.ktnx.net Git - mobile-ledger.git/commitdiff
convert the last update global header to a list header
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 18 Sep 2020 15:54:30 +0000 (18:54 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 18 Sep 2020 15:54:30 +0000 (18:54 +0300)
to avoid taking up precious space, and to help animation of transactions
coming in the front of the list -- before they were silently inserted
above the first, in above the visible scroller window. now there is
an item fixed at the first position

15 files changed:
app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java
app/src/main/java/net/ktnx/mobileledger/async/TransactionDateFinder.java
app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/Data.java
app/src/main/java/net/ktnx/mobileledger/model/TransactionListItem.java
app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolder.java
app/src/main/res/layout/account_summary_row.xml
app/src/main/res/layout/last_update_layout.xml [new file with mode: 0644]
app/src/main/res/layout/main_app_layout.xml
app/src/main/res/layout/transaction_list_row.xml

index 1b96425681642a25580c13c7a8d2518e035051ae..43d36e1f9af3ba80d48fb533c955023b02f8d1bf 100644 (file)
@@ -36,7 +36,10 @@ public class TransactionAccumulator {
     public void put(LedgerTransaction transaction, SimpleDate date) {
         if (done)
             throw new IllegalStateException("Can't put new items after done()");
+
+        // first item
         if (null == latestDate) {
+            list.add(new TransactionListItem());
             latestDate = date;
             list.add(new TransactionListItem(date, SimpleDate.today().month != date.month));
         }
index 6c261f681d05557ca936b3ad60aeff65da5d2cab..608b477198af04f99c6a7bf49cc05b6f310892d7 100644 (file)
@@ -24,6 +24,8 @@ import net.ktnx.mobileledger.ui.MainModel;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.SimpleDate;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -61,7 +63,7 @@ public class TransactionDateFinder extends AsyncTask<TransactionDateFinder.Param
     public static class Params {
         public final MainModel model;
         public final SimpleDate date;
-        public Params(MainModel model, SimpleDate date) {
+        public Params(@NotNull MainModel model, @NotNull SimpleDate date) {
             this.model = model;
             this.date = date;
         }
@@ -69,7 +71,11 @@ public class TransactionDateFinder extends AsyncTask<TransactionDateFinder.Param
 
     static class TransactionListItemComparator implements Comparator<TransactionListItem> {
         @Override
-        public int compare(TransactionListItem a, TransactionListItem b) {
+        public int compare(@NotNull TransactionListItem a, @NotNull TransactionListItem b) {
+            if (a.getType() == TransactionListItem.Type.HEADER)
+                return +1;
+            if (b.getType() == TransactionListItem.Type.HEADER)
+                return -1;
             final SimpleDate aDate = a.getDate();
             final SimpleDate bDate = b.getDate();
             int res = aDate.compareTo(bDate);
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java b/app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java
new file mode 100644 (file)
index 0000000..5fd9517
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import androidx.annotation.NonNull;
+
+import org.jetbrains.annotations.NotNull;
+
+public class AccountListItem {
+    private final Type type;
+    private LedgerAccount account;
+    public AccountListItem(@NotNull LedgerAccount account) {
+        this.type = Type.ACCOUNT;
+        this.account = account;
+    }
+    public AccountListItem() {
+        this.type = Type.HEADER;
+    }
+    @NonNull
+    public Type getType() {
+        return type;
+    }
+    @NotNull
+    public LedgerAccount getAccount() {
+        if (type != Type.ACCOUNT)
+            throw new IllegalStateException(
+                    String.format("Item type is not %s, but %s", Type.ACCOUNT, type));
+        return account;
+    }
+    public enum Type {ACCOUNT, HEADER}
+}
index 74e274d672d82e78d3d48b6b9975702294bc3283..66738cb0ba3c2d9303dfd1a5533be836dfb56b5e 100644 (file)
@@ -32,9 +32,11 @@ import net.ktnx.mobileledger.utils.LockHolder;
 import net.ktnx.mobileledger.utils.Locker;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.ObservableValue;
 
 import java.text.NumberFormat;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
@@ -54,6 +56,8 @@ public final class Data {
     public static final MutableLiveData<Boolean> currencyGap = new MutableLiveData<>(true);
     public static final MutableLiveData<Locale> locale = new MutableLiveData<>();
     public static final MutableLiveData<Boolean> drawerOpen = new MutableLiveData<>(false);
+    public static final MutableLiveData<Date> lastUpdateLiveData = new MutableLiveData<>(null);
+    public static final ObservableValue<Long> lastUpdate = new ObservableValue<>();
     private static final MutableLiveData<MobileLedgerProfile> profile =
             new InertMutableLiveData<>();
     private static final AtomicInteger backgroundTaskCount = new AtomicInteger(0);
index 6f1ee9ebcb8eabfdeaa7844fd02f4c476ad41191..1e62ff1320cdded3cae4e50ab2c627a68ad51f61 100644 (file)
@@ -22,20 +22,25 @@ import androidx.annotation.NonNull;
 import net.ktnx.mobileledger.App;
 import net.ktnx.mobileledger.utils.SimpleDate;
 
+import org.jetbrains.annotations.NotNull;
+
 public class TransactionListItem {
     private final Type type;
     private SimpleDate date;
     private boolean monthShown;
     private LedgerTransaction transaction;
-    public TransactionListItem(SimpleDate date, boolean monthShown) {
+    public TransactionListItem(@NotNull SimpleDate date, boolean monthShown) {
         this.type = Type.DELIMITER;
         this.date = date;
         this.monthShown = monthShown;
     }
-    public TransactionListItem(LedgerTransaction transaction) {
+    public TransactionListItem(@NotNull LedgerTransaction transaction) {
         this.type = Type.TRANSACTION;
         this.transaction = transaction;
     }
+    public TransactionListItem() {
+        this.type = Type.HEADER;
+    }
     @NonNull
     public Type getType() {
         return type;
@@ -44,14 +49,20 @@ public class TransactionListItem {
     public SimpleDate getDate() {
         if (date != null)
             return date;
+        if (type == Type.HEADER)
+            throw new IllegalStateException("Header item has no date");
         transaction.loadData(App.getDatabase());
         return transaction.getDate();
     }
     public boolean isMonthShown() {
         return monthShown;
     }
+    @NotNull
     public LedgerTransaction getTransaction() {
+        if (type != Type.TRANSACTION)
+            throw new IllegalStateException(
+                    String.format("Item type is not %s, but %s", Type.TRANSACTION, type));
         return transaction;
     }
-    public enum Type {TRANSACTION, DELIMITER}
+    public enum Type {TRANSACTION, DELIMITER, HEADER}
 }
index 5bc2554c4ff9b4f0db96824a6d017aca01891e00..6d640d622d5bca660f578cd14b0b8e212f67fd34 100644 (file)
@@ -32,6 +32,7 @@ import net.ktnx.mobileledger.App;
 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.model.Data;
 import net.ktnx.mobileledger.model.LedgerAccount;
 import net.ktnx.mobileledger.model.LedgerTransaction;
@@ -55,17 +56,17 @@ import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class MainModel extends ViewModel {
     public final MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
-    public final MutableLiveData<Date> lastUpdateDate = new MutableLiveData<>(null);
     private final MutableLiveData<Boolean> updatingFlag = new MutableLiveData<>(false);
     private final MutableLiveData<String> accountFilter = new MutableLiveData<>();
     private final MutableLiveData<List<TransactionListItem>> displayedTransactions =
             new MutableLiveData<>(new ArrayList<>());
-    private final MutableLiveData<List<LedgerAccount>> displayedAccounts = new MutableLiveData<>();
+    private final MutableLiveData<List<AccountListItem>> displayedAccounts =
+            new MutableLiveData<>();
     private final Locker accountsLocker = new Locker();
     private final MutableLiveData<String> updateError = new MutableLiveData<>();
+    private final Map<String, LedgerAccount> accountMap = new HashMap<>();
     private MobileLedgerProfile profile;
     private List<LedgerAccount> allAccounts = new ArrayList<>();
-    private final Map<String, LedgerAccount> accountMap = new HashMap<>();
     private SimpleDate firstTransactionDate;
     private SimpleDate lastTransactionDate;
     transient private RetrieveTransactionsTask retrieveTransactionsTask;
@@ -127,7 +128,7 @@ public class MainModel extends ViewModel {
         debug("db", "Updating transaction value stamp");
         Date now = new Date();
         profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
-        lastUpdateDate.postValue(now);
+        Data.lastUpdateLiveData.postValue(now);
     }
     public void scheduleTransactionListReload() {
         UpdateTransactionsTask task = new UpdateTransactionsTask();
@@ -209,7 +210,7 @@ public class MainModel extends ViewModel {
             updateAccountsMap(allAccounts);
         }
     }
-    public LiveData<List<LedgerAccount>> getDisplayedAccounts() {
+    public LiveData<List<AccountListItem>> getDisplayedAccounts() {
         return displayedAccounts;
     }
     synchronized public void scheduleAccountListReload() {
@@ -355,18 +356,17 @@ public class MainModel extends ViewModel {
         }
         @Override
         public void run() {
-            List<LedgerAccount> newDisplayed = new ArrayList<>();
+            List<AccountListItem> newDisplayed = new ArrayList<>();
             Logger.debug("dFilter", "waiting for synchronized block");
             Logger.debug("dFilter", String.format(Locale.US,
                     "entered synchronized block (about to examine %d accounts)", list.size()));
+            newDisplayed.add(new AccountListItem());    // header
             for (LedgerAccount a : list) {
-                if (isInterrupted()) {
+                if (isInterrupted())
                     return;
-                }
 
-                if (a.isVisible()) {
-                    newDisplayed.add(a);
-                }
+                if (a.isVisible())
+                    newDisplayed.add(new AccountListItem(a));
             }
             if (!isInterrupted()) {
                 model.displayedAccounts.postValue(newDisplayed);
index 30025c2876d7cded8cd12cd9034718bb086e6eb6..c6c909152de2b6cb575d4130e58bd6688aee63a9 100644 (file)
@@ -20,6 +20,7 @@ package net.ktnx.mobileledger.ui.account_summary;
 import android.content.Context;
 import android.content.res.Resources;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -35,38 +36,54 @@ import androidx.recyclerview.widget.RecyclerView;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.async.DbOpQueue;
+import net.ktnx.mobileledger.model.AccountListItem;
+import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerAccount;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.MainModel;
 import net.ktnx.mobileledger.ui.activity.MainActivity;
 import net.ktnx.mobileledger.utils.Locker;
-import net.ktnx.mobileledger.utils.Logger;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 import java.util.Locale;
+import java.util.Observer;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class AccountSummaryAdapter
         extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
     public static final int AMOUNT_LIMIT = 3;
-    private final AsyncListDiffer<LedgerAccount> listDiffer;
+    private final AsyncListDiffer<AccountListItem> listDiffer;
     private final MainModel model;
     AccountSummaryAdapter(MainModel model) {
         this.model = model;
 
-        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<LedgerAccount>() {
+        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<AccountListItem>() {
             @Override
-            public boolean areItemsTheSame(@NotNull LedgerAccount oldItem,
-                                           @NotNull LedgerAccount newItem) {
-                return TextUtils.equals(oldItem.getName(), newItem.getName());
+            public boolean areItemsTheSame(@NotNull AccountListItem oldItem,
+                                           @NotNull AccountListItem newItem) {
+                final AccountListItem.Type oldType = oldItem.getType();
+                final AccountListItem.Type newType = newItem.getType();
+                if (oldType == AccountListItem.Type.HEADER) {
+                    return newType == AccountListItem.Type.HEADER;
+                }
+                if (oldType != newType)
+                    return false;
+
+                return TextUtils.equals(oldItem.getAccount()
+                                               .getName(), newItem.getAccount()
+                                                                  .getName());
             }
             @Override
-            public boolean areContentsTheSame(@NotNull LedgerAccount oldItem,
-                                              @NotNull LedgerAccount newItem) {
-                return oldItem.equals(newItem);
+            public boolean areContentsTheSame(@NotNull AccountListItem oldItem,
+                                              @NotNull AccountListItem newItem) {
+                if (oldItem.getType()
+                           .equals(AccountListItem.Type.HEADER))
+                    return true;
+                return oldItem.getAccount()
+                              .equals(newItem.getAccount());
             }
         });
     }
@@ -89,26 +106,33 @@ public class AccountSummaryAdapter
         return listDiffer.getCurrentList()
                          .size();
     }
-    public void setAccounts(List<LedgerAccount> newList) {
+    public void setAccounts(List<AccountListItem> newList) {
         listDiffer.submitList(newList);
     }
     class LedgerRowHolder extends RecyclerView.ViewHolder {
-        final TextView tvAccountName, tvAccountAmounts;
-        final ConstraintLayout row;
-        final View expanderContainer;
-        final ImageView expander;
-        final View accountExpanderContainer;
+        private final TextView tvAccountName, tvAccountAmounts;
+        private final ConstraintLayout row;
+        private final View expanderContainer;
+        private final View amountExpanderContainer;
+        private final View lLastUpdate;
+        private final TextView tvLastUpdate;
+        private final View vAccountNameLayout;
         LedgerAccount mAccount;
+        private AccountListItem.Type lastType;
+        private Observer lastUpdateObserver;
         public LedgerRowHolder(@NonNull View itemView) {
             super(itemView);
 
             row = itemView.findViewById(R.id.account_summary_row);
+            vAccountNameLayout = itemView.findViewById(R.id.account_name_layout);
             tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
             tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
             expanderContainer = itemView.findViewById(R.id.account_expander_container);
-            expander = itemView.findViewById(R.id.account_expander);
-            accountExpanderContainer =
+            ImageView expander = itemView.findViewById(R.id.account_expander);
+            amountExpanderContainer =
                     itemView.findViewById(R.id.account_row_amounts_expander_container);
+            lLastUpdate = itemView.findViewById(R.id.last_update_container);
+            tvLastUpdate = itemView.findViewById(R.id.last_update_text);
 
             itemView.setOnLongClickListener(this::onItemLongClick);
             tvAccountName.setOnLongClickListener(this::onItemLongClick);
@@ -121,6 +145,7 @@ public class AccountSummaryAdapter
             expanderContainer.setOnClickListener(v -> toggleAccountExpanded());
             expander.setOnClickListener(v -> toggleAccountExpanded());
             tvAccountAmounts.setOnClickListener(v -> toggleAmountsExpanded());
+
         }
         private void toggleAccountExpanded() {
             if (!mAccount.hasSubAccounts())
@@ -156,11 +181,11 @@ public class AccountSummaryAdapter
             mAccount.toggleAmountsExpanded();
             if (mAccount.amountsExpanded()) {
                 tvAccountAmounts.setText(mAccount.getAmountsString());
-                accountExpanderContainer.setVisibility(View.GONE);
+                amountExpanderContainer.setVisibility(View.GONE);
             }
             else {
                 tvAccountAmounts.setText(mAccount.getAmountsString(AMOUNT_LIMIT));
-                accountExpanderContainer.setVisibility(View.VISIBLE);
+                amountExpanderContainer.setVisibility(View.VISIBLE);
             }
 
             MobileLedgerProfile profile = mAccount.getProfile();
@@ -189,38 +214,103 @@ public class AccountSummaryAdapter
             builder.show();
             return true;
         }
-        public void bindToAccount(LedgerAccount acc) {
-            Logger.debug("accounts", String.format(Locale.US, "Binding to '%s'", acc.getName()));
-            Context ctx = row.getContext();
-            Resources rm = ctx.getResources();
-            mAccount = acc;
+        public void bindToAccount(AccountListItem item) {
+            final AccountListItem.Type newType = item.getType();
+            setType(newType);
 
-            row.setTag(acc);
+            switch (newType) {
+                case ACCOUNT:
+                    LedgerAccount acc = item.getAccount();
 
-            tvAccountName.setText(acc.getShortName());
+                    debug("accounts", String.format(Locale.US, "Binding to '%s'", acc.getName()));
+                    Context ctx = row.getContext();
+                    Resources rm = ctx.getResources();
+                    mAccount = acc;
 
-            ConstraintLayout.LayoutParams lp =
-                    (ConstraintLayout.LayoutParams) tvAccountName.getLayoutParams();
-            lp.setMarginStart(
-                    acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 3);
+                    row.setTag(acc);
 
-            if (acc.hasSubAccounts()) {
-                expanderContainer.setVisibility(View.VISIBLE);
-                expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
-            }
-            else {
-                expanderContainer.setVisibility(View.GONE);
-            }
+                    tvAccountName.setText(acc.getShortName());
 
-            int amounts = acc.getAmountCount();
-            if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
-                tvAccountAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
-                accountExpanderContainer.setVisibility(View.VISIBLE);
+                    ConstraintLayout.LayoutParams lp =
+                            (ConstraintLayout.LayoutParams) tvAccountName.getLayoutParams();
+                    lp.setMarginStart(
+                            acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) /
+                            3);
+
+                    if (acc.hasSubAccounts()) {
+                        expanderContainer.setVisibility(View.VISIBLE);
+                        expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
+                    }
+                    else {
+                        expanderContainer.setVisibility(View.GONE);
+                    }
+
+                    int amounts = acc.getAmountCount();
+                    if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
+                        tvAccountAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
+                        amountExpanderContainer.setVisibility(View.VISIBLE);
+                    }
+                    else {
+                        tvAccountAmounts.setText(acc.getAmountsString());
+                        amountExpanderContainer.setVisibility(View.GONE);
+                    }
+
+                    break;
+                case HEADER:
+                    setLastUpdateText(Data.lastUpdate.get());
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected value: " + newType);
             }
-            else {
-                tvAccountAmounts.setText(acc.getAmountsString());
-                accountExpanderContainer.setVisibility(View.GONE);
+
+        }
+        void setLastUpdateText(long lastUpdate) {
+            final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
+                                    DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NUMERIC_DATE;
+            tvLastUpdate.setText((lastUpdate == 0) ? "----" : DateUtils.formatDateTime(
+                    tvLastUpdate.getContext(), lastUpdate, formatFlags));
+        }
+        private void initLastUpdateObserver() {
+            if (lastUpdateObserver != null)
+                return;
+
+            lastUpdateObserver = (o, arg) -> setLastUpdateText(Data.lastUpdate.get());
+
+            Data.lastUpdate.addObserver(lastUpdateObserver);
+        }
+        private void dropLastUpdateObserver() {
+            if (lastUpdateObserver == null)
+                return;
+
+            Data.lastUpdate.deleteObserver(lastUpdateObserver);
+            lastUpdateObserver = null;
+        }
+        private void setType(AccountListItem.Type newType) {
+            if (newType == lastType)
+                return;
+
+            switch (newType) {
+                case ACCOUNT:
+                    row.setLongClickable(true);
+                    amountExpanderContainer.setVisibility(View.VISIBLE);
+                    vAccountNameLayout.setVisibility(View.VISIBLE);
+                    tvAccountAmounts.setVisibility(View.VISIBLE);
+                    lLastUpdate.setVisibility(View.GONE);
+                    dropLastUpdateObserver();
+                    break;
+                case HEADER:
+                    row.setLongClickable(false);
+                    tvAccountAmounts.setVisibility(View.GONE);
+                    amountExpanderContainer.setVisibility(View.GONE);
+                    vAccountNameLayout.setVisibility(View.GONE);
+                    lLastUpdate.setVisibility(View.VISIBLE);
+                    initLastUpdateObserver();
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected value: " + newType);
             }
+
+            lastType = newType;
         }
     }
 }
index 29003b820f484ea75c06bb24a15a578bc83eddd7..5f4ffe57f2c4213188c526ceb311df6af03c6b54 100644 (file)
@@ -31,8 +31,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.AccountListItem;
 import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerAccount;
 import net.ktnx.mobileledger.ui.MainModel;
 import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
 import net.ktnx.mobileledger.ui.activity.MainActivity;
@@ -102,7 +102,7 @@ public class AccountSummaryFragment extends MobileLedgerListFragment {
         model.getDisplayedAccounts()
              .observe(getViewLifecycleOwner(), this::onAccountsChanged);
     }
-    private void onAccountsChanged(List<LedgerAccount> accounts) {
+    private void onAccountsChanged(List<AccountListItem> accounts) {
         Logger.debug("async-acc",
                 String.format(Locale.US, "fragment: got new account list (%d items)",
                         accounts.size()));
index 1674c029a45dfda2b525cad2402cca1c235903c3..9082695fc4973d0adabd0ac38272ec83c381240d 100644 (file)
@@ -29,7 +29,6 @@ import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
@@ -66,7 +65,6 @@ import net.ktnx.mobileledger.utils.MLDB;
 
 import org.jetbrains.annotations.NotNull;
 
-import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -235,8 +233,6 @@ public class MainActivity extends ProfileThemedActivity {
                      .setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
         }
 
-        mainModel.lastUpdateDate.observe(this, this::updateLastUpdateDisplay);
-
         findViewById(R.id.btn_no_profiles_add).setOnClickListener(
                 v -> startEditProfileActivity(null));
 
@@ -460,28 +456,13 @@ public class MainActivity extends ProfileThemedActivity {
 
         updateLastUpdateTextFromDB();
     }
-    private void updateLastUpdateDisplay(Date newValue) {
-        ViewGroup l = findViewById(R.id.transactions_last_update_layout);
-        TextView v = findViewById(R.id.transactions_last_update);
-        if (newValue == null) {
-            l.setVisibility(View.INVISIBLE);
-            Logger.debug("main", "no last update date :(");
-        }
-        else {
-            final String text = DateFormat.getDateTimeInstance()
-                                          .format(newValue);
-            v.setText(text);
-            l.setVisibility(View.VISIBLE);
-            Logger.debug("main", String.format("Date formatted: %s", text));
-        }
-    }
     private void profileThemeChanged() {
         storeThemeIdInPrefs(profile.getThemeHue());
 
         // un-hook all observed LiveData
         Data.removeProfileObservers(this);
         Data.profiles.removeObservers(this);
-        mainModel.lastUpdateDate.removeObservers(this);
+        Data.lastUpdateLiveData.removeObservers(this);
 
         recreate();
     }
@@ -569,17 +550,28 @@ public class MainActivity extends ProfileThemedActivity {
         if (profile == null)
             return;
 
-        long last_update = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
+        long lastUpdate = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
 
-        Logger.debug("transactions",
-                String.format(Locale.ENGLISH, "Last update = %d", last_update));
-        if (last_update == 0) {
-            mainModel.lastUpdateDate.postValue(null);
+        Logger.debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", lastUpdate));
+        if (lastUpdate == 0) {
+            Data.lastUpdateLiveData.postValue(null);
         }
         else {
-            mainModel.lastUpdateDate.postValue(new Date(last_update));
+            Data.lastUpdateLiveData.postValue(new Date(lastUpdate));
         }
-        scheduleDataRetrievalIfStale(last_update);
+
+        // this is unfortunate, but it appears we need a two-stage rocket to make
+        // a value reach a recycler view item holder. first stage is a regular
+        // LiveData that can be observed by an activity (this).
+        // the second stage forwards the changes, in the UI thread, to the
+        // observable value, observed by the view holders.
+        // view holders can't observe the LiveData because they don't have
+        // access to lifecycle owners. oh, also the value is updated by a thread
+        // so it must be tunnelled by an activity for it to reach the view
+        // holders in the UI thread
+        Data.lastUpdateLiveData.observe(this, date -> runOnUiThread(
+                () -> Data.lastUpdate.set((date == null) ? 0 : date.getTime())));
+        scheduleDataRetrievalIfStale(lastUpdate);
 
     }
     public void onStopTransactionRefreshClick(View view) {
index 492c6151266a31540543c1f7cdf368234bc33afb..94712cbdcbac809caa232ceff382f37ddc9fb766 100644 (file)
@@ -39,6 +39,7 @@ import androidx.recyclerview.widget.RecyclerView;
 
 import net.ktnx.mobileledger.App;
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.TransactionListItem;
@@ -76,6 +77,8 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
                         return oldItem.getTransaction()
                                       .getId() == newItem.getTransaction()
                                                          .getId();
+                    case HEADER:
+                        return true;    // there can be only one header
                     default:
                         throw new IllegalStateException(
                                 String.format(Locale.US, "Unexpected transaction item type %s",
@@ -92,6 +95,10 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
                     case TRANSACTION:
                         return oldItem.getTransaction()
                                       .equals(newItem.getTransaction());
+                    case HEADER:
+                        // headers don't differ in their contents. they observe the last update
+                        // date and react to its changes
+                        return true;
                     default:
                         throw new IllegalStateException(
                                 String.format(Locale.US, "Unexpected transaction item type %s",
@@ -112,10 +119,11 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
         if (item == null)
             return;
 
-        switch (item.getType()) {
+        final TransactionListItem.Type newType = item.getType();
+        holder.setType(newType);
+
+        switch (newType) {
             case TRANSACTION:
-                holder.vTransaction.setVisibility(View.VISIBLE);
-                holder.vDelimiter.setVisibility(View.GONE);
                 LedgerTransaction tr = item.getTransaction();
 
                 //        debug("transactions", String.format("Filling position %d with %d
@@ -137,8 +145,6 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
                 break;
             case DELIMITER:
                 SimpleDate date = item.getDate();
-                holder.vTransaction.setVisibility(View.GONE);
-                holder.vDelimiter.setVisibility(View.VISIBLE);
                 holder.tvDelimiterDate.setText(DateFormat.getDateInstance()
                                                          .format(date.toDate()));
                 if (item.isMonthShown()) {
@@ -159,9 +165,14 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
                     holder.vDelimiterThick.setVisibility(View.GONE);
                 }
                 break;
+            case HEADER:
+                holder.setLastUpdateText(Data.lastUpdate.get());
+
+                break;
+            default:
+                throw new IllegalStateException("Unexpected value: " + newType);
         }
     }
-
     @NonNull
     @Override
     public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
index d665d08198c3b7b839d5ce97914afdb884272fd2..ba48d70ca7994ac9bc4575b573016b1b0b853f49 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.ktnx.mobileledger.ui.transaction_list;
 
+import android.text.format.DateUtils;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -27,6 +28,10 @@ import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.RecyclerView;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.TransactionListItem;
+
+import java.util.Observer;
 
 class TransactionRowHolder extends RecyclerView.ViewHolder {
     final TextView tvDescription;
@@ -37,6 +42,10 @@ class TransactionRowHolder extends RecyclerView.ViewHolder {
     final CardView vTransaction;
     final TextView tvDelimiterMonth, tvDelimiterDate;
     final View vDelimiterThick;
+    final View vHeader;
+    final TextView tvLastUpdate;
+    TransactionListItem.Type lastType;
+    private Observer lastUpdateObserver;
     public TransactionRowHolder(@NonNull View itemView) {
         super(itemView);
         this.row = itemView.findViewById(R.id.transaction_row);
@@ -48,5 +57,58 @@ class TransactionRowHolder extends RecyclerView.ViewHolder {
         this.tvDelimiterDate = itemView.findViewById(R.id.transaction_delimiter_date);
         this.tvDelimiterMonth = itemView.findViewById(R.id.transaction_delimiter_month);
         this.vDelimiterThick = itemView.findViewById(R.id.transaction_delimiter_thick);
+        this.vHeader = itemView.findViewById(R.id.last_update_container);
+        this.tvLastUpdate = itemView.findViewById(R.id.last_update_text);
+    }
+    private void initLastUpdateObserver() {
+        if (lastUpdateObserver != null)
+            return;
+
+        lastUpdateObserver = (o, arg) -> setLastUpdateText(Data.lastUpdate.get());
+
+        Data.lastUpdate.addObserver(lastUpdateObserver);
+    }
+    void setLastUpdateText(long lastUpdate) {
+        final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
+                                DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NUMERIC_DATE;
+        tvLastUpdate.setText((lastUpdate == 0) ? "----"
+                                               : DateUtils.formatDateTime(tvLastUpdate.getContext(),
+                                                       lastUpdate, formatFlags));
+    }
+    private void dropLastUpdateObserver() {
+        if (lastUpdateObserver == null)
+            return;
+
+        Data.lastUpdate.deleteObserver(lastUpdateObserver);
+        lastUpdateObserver = null;
+    }
+    void setType(TransactionListItem.Type newType) {
+        if (newType == lastType)
+            return;
+
+        switch (newType) {
+            case TRANSACTION:
+                vHeader.setVisibility(View.GONE);
+                vTransaction.setVisibility(View.VISIBLE);
+                vDelimiter.setVisibility(View.GONE);
+                dropLastUpdateObserver();
+                break;
+            case DELIMITER:
+                vHeader.setVisibility(View.GONE);
+                vTransaction.setVisibility(View.GONE);
+                vDelimiter.setVisibility(View.VISIBLE);
+                dropLastUpdateObserver();
+                break;
+            case HEADER:
+                vHeader.setVisibility(View.VISIBLE);
+                vTransaction.setVisibility(View.GONE);
+                vDelimiter.setVisibility(View.GONE);
+                initLastUpdateObserver();
+                break;
+            default:
+                throw new IllegalStateException("Unexpected value: " + newType);
+        }
+
+        lastType = newType;
     }
 }
index 8112c2a72e6d27642c53a79f7c3728031b17ccbb..4f345e7dd285141b2a038740d7fa9afd46975136 100644 (file)
                 android:background="@drawable/ic_expand_less_black_24dp"
                 android:backgroundTint="?colorPrimary"
                 android:clickable="true"
+                android:contentDescription="@string/sub_accounts_expand_collapse_trigger_description"
                 android:focusable="true"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="parent"
-                android:contentDescription="@string/sub_accounts_expand_collapse_trigger_description"
                 />
         </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.constraintlayout.widget.ConstraintLayout>
         >
 
     </FrameLayout>
-
+    <include layout="@layout/last_update_layout" />
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/last_update_layout.xml b/app/src/main/res/layout/last_update_layout.xml
new file mode 100644 (file)
index 0000000..e9ddb48
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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 <https://www.gnu.org/licenses/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/last_update_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="4dp"
+    >
+    <TextView
+        android:id="@+id/last_update_label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:paddingStart="8dp"
+        android:paddingEnd="8dp"
+        android:text="@string/transactions_last_update_label"
+        android:textAppearance="@android:style/TextAppearance.Material.Small"
+        app:layout_constraintEnd_toStartOf="@id/last_update_text"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+
+    <TextView
+        android:id="@+id/last_update_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/activity_horizontal_margin"
+        android:layout_weight="1"
+        android:text="\?"
+        android:textAppearance="@android:style/TextAppearance.Material.Small"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="HardcodedText"
+        />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index d9f4b42687224d2cdd19e545ea875ad3141212c3..ffee1994ddfd1d34fdae935a75868469bfb61e67 100644 (file)
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@id/toolbar">
 
-                <androidx.constraintlayout.widget.ConstraintLayout
-                    android:id="@+id/transactions_last_update_layout"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:elevation="24dp"
-                    android:orientation="horizontal"
-                    >
-
-                    <TextView
-                        android:id="@+id/transaction_last_update_label"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:paddingStart="8dp"
-                        android:paddingEnd="8dp"
-                        android:text="@string/transactions_last_update_label"
-                        android:textAppearance="@android:style/TextAppearance.Material.Small"
-                        app:layout_constraintEnd_toStartOf="@id/transactions_last_update"
-                        app:layout_constraintTop_toTopOf="parent"
-                        />
-
-                    <TextView
-                        android:id="@+id/transactions_last_update"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_weight="1"
-                        android:text="\?"
-                        android:textAppearance="@android:style/TextAppearance.Material.Small"
-                        tools:ignore="HardcodedText"
-                        app:layout_constraintEnd_toEndOf="parent"
-                        app:layout_constraintTop_toTopOf="parent"
-                        />
-                </androidx.constraintlayout.widget.ConstraintLayout>
-
                 <LinearLayout
                     android:id="@+id/transaction_progress_layout"
                     android:layout_width="match_parent"
index 13b26ca64ed3e89afd5a6dd22571c5d7302ec153..a8940553ee5b151447072283013a0a1c2007aff9 100644 (file)
@@ -32,9 +32,9 @@
         android:layout_height="wrap_content"
         android:layout_margin="8dp"
         android:visibility="visible"
+        app:cardCornerRadius="0dp"
         app:cardElevation="2dp"
         app:cardUseCompatPadding="false"
-        app:cardCornerRadius="0dp"
         >
 
         <androidx.constraintlayout.widget.ConstraintLayout
                     />
                 <TextView
                     android:id="@+id/transaction_comment"
+                    style="@style/transaction_list_comment"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_marginStart="0dp"
                     android:layout_marginTop="0dp"
                     android:text="Comment text"
                     tools:ignore="HardcodedText"
-                    style="@style/transaction_list_comment"
                     />
             </LinearLayout>
 
             />
 
     </androidx.constraintlayout.widget.ConstraintLayout>
+    <include layout="@layout/last_update_layout" />
 
 </FrameLayout>
\ No newline at end of file