From: Damyan Ivanov Date: Sun, 24 Mar 2019 17:09:41 +0000 (+0200) Subject: locks around observable list's access X-Git-Tag: v0.8.1~12 X-Git-Url: https://git.ktnx.net/?a=commitdiff_plain;h=7165b89c8ff2b9d8f69e02354197127ec27a4a47;p=mobile-ledger.git locks around observable list's access to help guarantee that the list is not modified by another thread while traversing --- diff --git a/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java index 8f8c1dac..7a6f7138 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java @@ -23,6 +23,7 @@ import android.util.Log; 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; @@ -38,6 +39,7 @@ public class CommitAccountsTask 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(), @@ -48,6 +50,7 @@ public class CommitAccountsTask acc.setHiddenByStar(acc.isHiddenByStarToBe()); if (!params[0].showOnlyStarred || !acc.isHiddenByStar()) newList.add(acc); + } db.setTransactionSuccessful(); } } 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 5965d27d..d92ddef1 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/Data.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/Data.java @@ -17,6 +17,7 @@ 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; @@ -41,12 +42,13 @@ public final class Data { 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; } } diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java index 1971ca26..9731e3e6 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java @@ -21,7 +21,9 @@ import android.database.Cursor; 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; @@ -101,12 +103,14 @@ public final class MobileLedgerProfile { 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(); } @@ -126,21 +130,21 @@ public final class MobileLedgerProfile { 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; } @@ -150,21 +154,21 @@ public final class MobileLedgerProfile { 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(); @@ -274,8 +278,7 @@ public final class MobileLedgerProfile { } 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) { @@ -352,13 +355,13 @@ public final class MobileLedgerProfile { // 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}); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java index 37785525..c2ec0e96 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java @@ -31,6 +31,7 @@ import android.widget.TextView; 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; @@ -45,41 +46,43 @@ public class AccountSummaryAdapter } 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); } } @@ -96,12 +99,15 @@ public class AccountSummaryAdapter 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() { @@ -114,20 +120,24 @@ public class AccountSummaryAdapter } 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++; + } } } } 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 0f668cee..cd58740e 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 @@ -50,6 +50,7 @@ import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter; 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; @@ -588,6 +589,7 @@ public class MainActivity extends ProfileThemedActivity { // 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", @@ -616,6 +618,7 @@ public class MainActivity extends ProfileThemedActivity { mAccountSummaryFragment.modelAdapter .notifyItemRangeRemoved(start, count); + } } } else { @@ -624,6 +627,7 @@ public class MainActivity extends ProfileThemedActivity { animator.rotationBy(-180); List 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 @@ -631,6 +635,7 @@ public class MainActivity extends ProfileThemedActivity { mAccountSummaryFragment.modelAdapter .notifyItemRangeInserted(parentPos + 1, children.size()); } + } } break; case R.id.account_row_acc_amounts: diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java index b7770dcc..5a5a352c 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java @@ -56,12 +56,12 @@ import net.ktnx.mobileledger.model.MobileLedgerProfile; 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; @@ -492,20 +492,22 @@ public class NewTransactionActivity extends ProfileThemedActivity 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 accounts = tr.getAccounts(); diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/LockHolder.java b/app/src/main/java/net/ktnx/mobileledger/utils/LockHolder.java new file mode 100644 index 00000000..f0c976dd --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/utils/LockHolder.java @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +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; + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/ObservableList.java b/app/src/main/java/net/ktnx/mobileledger/utils/ObservableList.java index 9e44faef..eac02376 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/ObservableList.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/ObservableList.java @@ -27,6 +27,7 @@ import java.util.List; 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; @@ -38,6 +39,7 @@ import androidx.annotation.RequiresApi; public class ObservableList extends Observable implements List { private List list; + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public ObservableList(List list) { this.list = list; } @@ -50,149 +52,243 @@ public class ObservableList extends Observable implements 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 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[] 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 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 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 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 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 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 listIterator() { + if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException( + "Iterators break encapsulation and ignore locking. Write-lock first"); return list.listIterator(); } public ListIterator listIterator(int index) { + if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException( + "Iterators break encapsulation and ignore locking. Write-lock first"); return list.listIterator(index); } public List 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 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 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 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 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 action) { - list.forEach(action); + try (LockHolder lh = lockForReading()) { + list.forEach(action); + } } public List getList() { + if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException( + "Direct list access breaks encapsulation and ignore locking. Write-lock first"); return list; } public void setList(List 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