import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.LockHolder;
import net.ktnx.mobileledger.utils.MLDB;
import java.util.ArrayList;
SQLiteDatabase db = MLDB.getDatabase();
db.beginTransaction();
try {
+ try (LockHolder lh = params[0].accountList.lockForWriting()) {
for (int i = 0; i < params[0].accountList.size(); i++ ){
LedgerAccount acc = params[0].accountList.get(i);
Log.d("CAT", String.format("Setting %s to %s", acc.getName(),
acc.setHiddenByStar(acc.isHiddenByStarToBe());
if (!params[0].showOnlyStarred || !acc.isHiddenByStar()) newList.add(acc);
+ }
db.setTransactionSuccessful();
}
}
package net.ktnx.mobileledger.model;
+import net.ktnx.mobileledger.utils.LockHolder;
import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.ObservableAtomicInteger;
import net.ktnx.mobileledger.utils.ObservableList;
profile.set(newProfile);
}
public static int getProfileIndex(MobileLedgerProfile profile) {
+ try(LockHolder lh = profiles.lockForReading()) {
for (int i = 0; i < profiles.size(); i++) {
MobileLedgerProfile p = profiles.get(i);
if (p.equals(profile)) return i;
+ }
+ return -1;
}
-
- return -1;
}
}
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
+import net.ktnx.mobileledger.async.DbOpQueue;
import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.LockHolder;
import net.ktnx.mobileledger.utils.MLDB;
import java.util.ArrayList;
db.beginTransaction();
try {
int orderNo = 0;
+ try (LockHolder lh = Data.profiles.lockForReading()) {
for (int i = 0; i < Data.profiles.size(); i++) {
MobileLedgerProfile p = Data.profiles.get(i);
db.execSQL("update profiles set order_no=? where uuid=?",
new Object[]{orderNo, p.getUuid()});
p.orderNo = orderNo;
orderNo++;
+ }
}
db.setTransactionSuccessful();
}
public String getName() {
return name;
}
- public void setName(String name) {
- this.name = 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(String url) {
- this.url = url;
- }
public void setUrl(CharSequence text) {
setUrl(String.valueOf(text));
}
+ public void setUrl(String url) {
+ this.url = url;
+ }
public boolean isAuthEnabled() {
return authEnabled;
}
public String getAuthUserName() {
return authUserName;
}
- public void setAuthUserName(String authUserName) {
- this.authUserName = 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(String authPassword) {
- this.authPassword = authPassword;
- }
public void setAuthPassword(CharSequence text) {
setAuthPassword(String.valueOf(text));
}
+ public void setAuthPassword(String authPassword) {
+ this.authPassword = authPassword;
+ }
public void storeInDB() {
SQLiteDatabase db = MLDB.getDatabase();
db.beginTransaction();
}
public void setOption(String name, String value) {
Log.d("profile", String.format("setting option %s=%s", name, value));
- SQLiteDatabase db = MLDB.getDatabase();
- db.execSQL("insert or replace into options(profile, name, value) values(?, ?, ?);",
+ DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
new String[]{uuid, name, value});
}
public void setLongOption(String name, long value) {
// Log.d("profile", String.format("Profile.getThemeId() returning %d", themeId));
return this.themeId;
}
+ public void setThemeId(Object o) {
+ setThemeId(Integer.valueOf(String.valueOf(o)).intValue());
+ }
public void setThemeId(int themeId) {
// Log.d("profile", String.format("Profile.setThemeId(%d) called", themeId));
this.themeId = themeId;
}
- public void setThemeId(Object o) {
- setThemeId(Integer.valueOf(String.valueOf(o)).intValue());
- }
public void markTransactionsAsNotPresent(SQLiteDatabase db) {
db.execSQL("UPDATE transactions set keep=0 where profile=?", new String[]{uuid});
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.LockHolder;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
}
public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
- if (position < Data.accounts.size()) {
- LedgerAccount acc = Data.accounts.get(position);
- Context ctx = holder.row.getContext();
- Resources rm = ctx.getResources();
-
- holder.row.setTag(acc);
- holder.row.setVisibility(View.VISIBLE);
- holder.vTrailer.setVisibility(View.GONE);
- holder.tvAccountName.setText(acc.getShortName());
- ConstraintLayout.LayoutParams lp =
- (ConstraintLayout.LayoutParams) holder.tvAccountName.getLayoutParams();
- lp.setMarginStart(
- acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 2);
- holder.expanderContainer
- .setVisibility(acc.hasSubAccounts() ? View.VISIBLE : View.INVISIBLE);
- holder.expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
- holder.tvAccountAmounts.setText(acc.getAmountsString());
-
- if (acc.isHiddenByStar()) {
- holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
- holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
+ try (LockHolder lh = Data.accounts.lockForReading()) {
+ if (position < Data.accounts.size()) {
+ LedgerAccount acc = Data.accounts.get(position);
+ Context ctx = holder.row.getContext();
+ Resources rm = ctx.getResources();
+
+ holder.row.setTag(acc);
+ holder.row.setVisibility(View.VISIBLE);
+ holder.vTrailer.setVisibility(View.GONE);
+ holder.tvAccountName.setText(acc.getShortName());
+ ConstraintLayout.LayoutParams lp =
+ (ConstraintLayout.LayoutParams) holder.tvAccountName.getLayoutParams();
+ lp.setMarginStart(
+ acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 2);
+ holder.expanderContainer
+ .setVisibility(acc.hasSubAccounts() ? View.VISIBLE : View.INVISIBLE);
+ holder.expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
+ holder.tvAccountAmounts.setText(acc.getAmountsString());
+
+ if (acc.isHiddenByStar()) {
+ holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
+ holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
+ }
+ else {
+ holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
+ holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
+ }
+
+ holder.selectionCb.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
+ holder.selectionCb.setChecked(!acc.isHiddenByStarToBe());
+
+ holder.row.setTag(R.id.POS, position);
}
else {
- holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
- holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
+ holder.vTrailer.setVisibility(View.VISIBLE);
+ holder.row.setVisibility(View.GONE);
}
-
- holder.selectionCb.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
- holder.selectionCb.setChecked(!acc.isHiddenByStarToBe());
-
- holder.row.setTag(R.id.POS, position);
- }
- else {
- holder.vTrailer.setVisibility(View.VISIBLE);
- holder.row.setVisibility(View.GONE);
}
}
return Data.accounts.size() + 1;
}
public void startSelection() {
- for (int i = 0; i < Data.accounts.size(); i++ ) {
- LedgerAccount acc = Data.accounts.get(i);
- acc.setHiddenByStarToBe(acc.isHiddenByStar());
+ try (LockHolder lh = Data.accounts.lockForWriting()) {
+ for (int i = 0; i < Data.accounts.size(); i++) {
+ LedgerAccount acc = Data.accounts.get(i);
+ acc.setHiddenByStarToBe(acc.isHiddenByStar());
+ }
+ this.selectionActive = true;
+ lh.downgrade();
+ notifyDataSetChanged();
}
- this.selectionActive = true;
- notifyDataSetChanged();
}
public void stopSelection() {
}
public void selectItem(int position) {
- LedgerAccount acc = Data.accounts.get(position);
- acc.toggleHiddenToBe();
- toggleChildrenOf(acc, acc.isHiddenByStarToBe(), position);
- notifyItemChanged(position);
+ try (LockHolder lh = Data.accounts.lockForWriting()) {
+ LedgerAccount acc = Data.accounts.get(position);
+ acc.toggleHiddenToBe();
+ toggleChildrenOf(acc, acc.isHiddenByStarToBe(), position);
+ notifyItemChanged(position);
+ }
}
void toggleChildrenOf(LedgerAccount parent, boolean hiddenToBe, int parentPosition) {
int i = parentPosition + 1;
- for (int j = 0; j < Data.accounts.size(); j++) {
- LedgerAccount acc = Data.accounts.get(j);
- if (acc.getName().startsWith(parent.getName() + ":")) {
- acc.setHiddenByStarToBe(hiddenToBe);
- notifyItemChanged(i);
- toggleChildrenOf(acc, hiddenToBe, i);
- i++;
+ try (LockHolder lh = Data.accounts.lockForWriting()) {
+ for (int j = 0; j < Data.accounts.size(); j++) {
+ LedgerAccount acc = Data.accounts.get(j);
+ if (acc.getName().startsWith(parent.getName() + ":")) {
+ acc.setHiddenByStarToBe(hiddenToBe);
+ notifyItemChanged(i);
+ toggleChildrenOf(acc, hiddenToBe, i);
+ i++;
+ }
}
}
}
import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.LockHolder;
import net.ktnx.mobileledger.utils.MLDB;
import java.lang.ref.WeakReference;
// removing all child accounts from the view
int start = -1, count = 0;
+ try (LockHolder lh = Data.accounts.lockForWriting()) {
for (int i = 0; i < Data.accounts.size(); i++) {
if (acc.isParentOf(Data.accounts.get(i))) {
// Log.d("accounts", String.format("Found a child '%s' at position %d",
mAccountSummaryFragment.modelAdapter
.notifyItemRangeRemoved(start, count);
+ }
}
}
else {
animator.rotationBy(-180);
List<LedgerAccount> children =
Data.profile.get().loadVisibleChildAccountsOf(acc);
+ try (LockHolder lh = Data.accounts.lockForWriting()) {
int parentPos = Data.accounts.indexOf(acc);
if (parentPos != -1) {
// may have disappeared in a concurrent refresh operation
mAccountSummaryFragment.modelAdapter
.notifyItemRangeInserted(parentPos + 1, children.size());
}
+ }
}
break;
case R.id.account_row_acc_amounts:
import net.ktnx.mobileledger.ui.DatePickerFragment;
import net.ktnx.mobileledger.ui.OnSwipeTouchListener;
import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.LockHolder;
import net.ktnx.mobileledger.utils.MLDB;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
-import java.util.List;
import java.util.Locale;
import java.util.Objects;
String profileUUID = c.getString(0);
int transactionId = c.getInt(1);
LedgerTransaction tr;
+ try(LockHolder lh = Data.profiles.lockForReading()) {
MobileLedgerProfile profile = null;
for (int i = 0; i < Data.profiles.size(); i++) {
MobileLedgerProfile p = Data.profiles.get(i);
if (p.getUuid().equals(profileUUID)) {
profile = p;
break;
+ }
}
- }
if (profile == null) throw new RuntimeException(String.format(
"Unable to find profile %s, which is supposed to contain " +
"transaction %d with description %s", profileUUID, transactionId,
description));
tr = profile.loadTransaction(transactionId);
+ }
int i = 0;
table = findViewById(R.id.new_transaction_accounts_table);
ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
--- /dev/null
+/*
+ * Copyright © 2019 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.utils;
+
+import java.io.Closeable;
+import java.util.concurrent.locks.Lock;
+
+public class LockHolder implements Closeable {
+ private Lock rLock, wLock;
+ LockHolder(Lock rLock) {
+ this.rLock = rLock;
+ this.wLock = null;
+ }
+ public LockHolder(Lock rLock, Lock wLock) {
+ this.rLock = rLock;
+ this.wLock = wLock;
+ }
+ @Override
+ public void close() {
+ if (wLock != null) wLock.unlock();
+ if (rLock != null) rLock.unlock();
+ }
+ public void downgrade() {
+ if (rLock == null) throw new IllegalStateException("no locks are held");
+
+ if (wLock == null) return;
+
+ wLock.unlock();
+ wLock = null;
+ }
+}
import java.util.ListIterator;
import java.util.Observable;
import java.util.Spliterator;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class ObservableList<T> extends Observable implements List<T> {
private List<T> list;
+ private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public ObservableList(List<T> list) {
this.list = list;
}
notifyObservers(index);
}
public int size() {
- return list.size();
+ try (LockHolder lh = lockForReading()) {
+ return list.size();
+ }
}
public boolean isEmpty() {
- return list.isEmpty();
+ try (LockHolder lh = lockForReading()) {
+ return list.isEmpty();
+ }
}
public boolean contains(@Nullable Object o) {
- return list.contains(o);
+ try (LockHolder lh = lockForReading()) {
+ return list.contains(o);
+ }
}
@NonNull
public Iterator<T> iterator() {
- return list.iterator();
+ throw new RuntimeException("Iterators break encapsulation and ignore locking");
+// return list.iterator();
}
public Object[] toArray() {
- return list.toArray();
+ try (LockHolder lh = lockForReading()) {
+ return list.toArray();
+ }
}
public <T1> T1[] toArray(@Nullable T1[] a) {
- return list.toArray(a);
+ try (LockHolder lh = lockForReading()) {
+ return list.toArray(a);
+ }
}
public boolean add(T t) {
- boolean result = list.add(t);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.add(t);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
public boolean remove(@Nullable Object o) {
- boolean result = list.remove(o);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.remove(o);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
public T removeQuietly(int index) {
return list.remove(index);
}
public boolean containsAll(@NonNull Collection<?> c) {
- return list.containsAll(c);
+ try (LockHolder lh = lockForReading()) {
+ return list.containsAll(c);
+ }
}
public boolean addAll(@NonNull Collection<? extends T> c) {
- boolean result = list.addAll(c);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.addAll(c);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
public boolean addAll(int index, @NonNull Collection<? extends T> c) {
- boolean result = list.addAll(index, c);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.addAll(index, c);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
public boolean addAllQuietly(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
public boolean removeAll(@NonNull Collection<?> c) {
- boolean result = list.removeAll(c);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.removeAll(c);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
public boolean retainAll(@NonNull Collection<?> c) {
- boolean result = list.retainAll(c);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.retainAll(c);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
@RequiresApi(api = Build.VERSION_CODES.N)
public void replaceAll(@NonNull UnaryOperator<T> operator) {
- list.replaceAll(operator);
- forceNotify();
+ try (LockHolder lh = lockForWriting()) {
+ list.replaceAll(operator);
+ lh.downgrade();
+ forceNotify();
+ }
}
@RequiresApi(api = Build.VERSION_CODES.N)
public void sort(@Nullable Comparator<? super T> c) {
- list.sort(c);
- forceNotify();
+ try (LockHolder lh = lockForWriting()) {
+ lock.writeLock().lock();
+ list.sort(c);
+ lh.downgrade();
+ forceNotify();
+ }
}
public void clear() {
- boolean wasEmpty = list.isEmpty();
- list.clear();
- if (!wasEmpty) forceNotify();
+ try (LockHolder lh = lockForWriting()) {
+ boolean wasEmpty = list.isEmpty();
+ list.clear();
+ lh.downgrade();
+ if (!wasEmpty) forceNotify();
+ }
}
public T get(int index) {
- return list.get(index);
+ try (LockHolder lh = lockForReading()) {
+ return list.get(index);
+ }
}
public T set(int index, T element) {
- T result = list.set(index, element);
- forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ T result = list.set(index, element);
+ lh.downgrade();
+ forceNotify();
+ return result;
+ }
}
public void add(int index, T element) {
- list.add(index, element);
- forceNotify();
+ try (LockHolder lh = lockForWriting()) {
+ list.add(index, element);
+ lh.downgrade();
+ forceNotify();
+ }
}
public T remove(int index) {
- T result = list.remove(index);
- forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ T result = list.remove(index);
+ lh.downgrade();
+ forceNotify();
+ return result;
+ }
}
public int indexOf(@Nullable Object o) {
- return list.indexOf(o);
+ try (LockHolder lh = lockForReading()) {
+ return list.indexOf(o);
+ }
}
public int lastIndexOf(@Nullable Object o) {
- return list.lastIndexOf(o);
+ try (LockHolder lh = lockForReading()) {
+ return list.lastIndexOf(o);
+ }
}
public ListIterator<T> listIterator() {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Iterators break encapsulation and ignore locking. Write-lock first");
return list.listIterator();
}
public ListIterator<T> listIterator(int index) {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Iterators break encapsulation and ignore locking. Write-lock first");
return list.listIterator(index);
}
public List<T> subList(int fromIndex, int toIndex) {
- return list.subList(fromIndex, toIndex);
+ try (LockHolder lh = lockForReading()) {
+ return list.subList(fromIndex, toIndex);
+ }
}
@RequiresApi(api = Build.VERSION_CODES.N)
public Spliterator<T> spliterator() {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Iterators break encapsulation and ignore locking. Write-lock first");
return list.spliterator();
}
@RequiresApi(api = Build.VERSION_CODES.N)
public boolean removeIf(Predicate<? super T> filter) {
- boolean result = list.removeIf(filter);
- if (result) forceNotify();
- return result;
+ try (LockHolder lh = lockForWriting()) {
+ boolean result = list.removeIf(filter);
+ lh.downgrade();
+ if (result) forceNotify();
+ return result;
+ }
}
@RequiresApi(api = Build.VERSION_CODES.N)
public Stream<T> stream() {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Iterators break encapsulation and ignore locking. Write-lock first");
return list.stream();
}
@RequiresApi(api = Build.VERSION_CODES.N)
public Stream<T> parallelStream() {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Iterators break encapsulation and ignore locking. Write-lock first");
return list.parallelStream();
}
@RequiresApi(api = Build.VERSION_CODES.N)
public void forEach(Consumer<? super T> action) {
- list.forEach(action);
+ try (LockHolder lh = lockForReading()) {
+ list.forEach(action);
+ }
}
public List<T> getList() {
+ if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
+ "Direct list access breaks encapsulation and ignore locking. Write-lock first");
return list;
}
public void setList(List<T> aList) {
- list = aList;
- forceNotify();
+ try (LockHolder lh = lockForWriting()) {
+ list = aList;
+ lh.downgrade();
+ forceNotify();
+ }
}
public void triggerItemChangedNotification(T item) {
- int index = list.indexOf(item);
- if (index == -1) {
- Log.d("ObList", "??? not sending notifications for item not found in the list");
- return;
+ try (LockHolder lh = lockForReading()) {
+ int index = list.indexOf(item);
+ if (index == -1) {
+ Log.d("ObList", "??? not sending notifications for item not found in the list");
+ return;
+ }
+ Log.d("ObList", "Notifying item change observers");
+ triggerItemChangedNotification(index);
}
- Log.d("ObList", "Notifying item change observers");
- triggerItemChangedNotification(index);
}
public void triggerItemChangedNotification(int index) {
forceNotify(index);
}
+ public LockHolder lockForWriting() {
+ ReentrantReadWriteLock.WriteLock wLock = lock.writeLock();
+ wLock.lock();
+
+ ReentrantReadWriteLock.ReadLock rLock = lock.readLock();
+ rLock.lock();
+
+ return new LockHolder(rLock, wLock);
+ }
+ public LockHolder lockForReading() {
+ ReentrantReadWriteLock.ReadLock rLock = lock.readLock();
+ rLock.lock();
+ return new LockHolder(rLock);
+ }
}
\ No newline at end of file