diff --git a/app/src/main/java/androidx/room/InvalidationTracker.kt b/app/src/main/java/androidx/room/InvalidationTracker.kt index 38067b702f..76cf95815c 100644 --- a/app/src/main/java/androidx/room/InvalidationTracker.kt +++ b/app/src/main/java/androidx/room/InvalidationTracker.kt @@ -405,10 +405,14 @@ open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX private fun checkUpdatedTable(): Set { val invalidatedTableIds = buildSet { - database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor -> - while (cursor.moveToNext()) { - add(cursor.getInt(0)) + try { + database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor -> + while (cursor.moveToNext()) { + add(cursor.getInt(0)) + } } + } catch (ex: Throwable) { + eu.faircode.email.Log.w(ex) } } if (invalidatedTableIds.isNotEmpty()) { diff --git a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt index 171b57d16e..fc53a72171 100644 --- a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt +++ b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt @@ -54,62 +54,79 @@ internal class RoomTrackingLiveData ( val invalid = AtomicBoolean(true) val computing = AtomicBoolean(false) val registeredObserver = AtomicBoolean(false) + val queued = eu.faircode.email.ObjectHolder(0) + val lock = Object() val refreshRunnable = Runnable { - if (registeredObserver.compareAndSet(false, true)) { - database.invalidationTracker.addWeakObserver(observer) - } - var computed: Boolean - do { - computed = false - // compute can happen only in 1 thread but no reason to lock others. - if (computing.compareAndSet(false, true)) { - // as long as it is invalid, keep computing. - try { - var value: T? = null - while (invalid.compareAndSet(true, false)) { - computed = true - try { - value = computeFunction.call() - } catch (e: Exception) { - throw RuntimeException( - "Exception while computing database live data.", - e - ) - } - } - if (computed) { - postValue(value) - } - } finally { - // release compute lock - computing.set(false) - } - } - // check invalid after releasing compute lock to avoid the following scenario. - // Thread A runs compute() - // Thread A checks invalid, it is false - // Main thread sets invalid to true - // Thread B runs, fails to acquire compute lock and skips - // Thread A releases compute lock - // We've left invalid in set state. The check below recovers. - } while (computed && invalid.get()) - } + eu.faircode.email.Log.i("MMM Refresh") + synchronized(lock) { + queued.value-- + if (queued.value < 0) { + eu.faircode.email.Log.e("$computeFunction queued=$queued.value") + queued.value = 0 + } + } + eu.faircode.email.Log.i("MMM Refreshing") + + if (registeredObserver.compareAndSet(false, true)) { + database.invalidationTracker.addWeakObserver(observer) + } + + var value: T? = null + var computed = false + synchronized(computeFunction) { + var retry = 0 + while (!computed) { + try { + value = computeFunction.call() + computed = true + } catch (e: Throwable) { + if (++retry > 5) { + eu.faircode.email.Log.e(e) + break + } + eu.faircode.email.Log.w(e) + try { + Thread.sleep(2000L) + } catch (ignored: InterruptedException) { + } + } + } + } + if (computed) { + postValue(value) + } + eu.faircode.email.Log.i("MMM Refreshed") + } val invalidationRunnable = Runnable { - val isActive = hasActiveObservers() - if (invalid.compareAndSet(false, true)) { - if (isActive) { - queryExecutor.execute(refreshRunnable) - } - } - } + val isActive = hasActiveObservers() + if (invalid.compareAndSet(false, true)) { + if (isActive) { + eu.faircode.email.Log.i("MMM Invalidate") + synchronized(lock) { + if (queued.value > 0) { + eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug, "$computeFunction queued=$queued.value") + } else { + queued.value++ + queryExecutor.execute(refreshRunnable) + } + eu.faircode.email.Log.i("MMM Invalidated") + } + } + } + } @Suppress("UNCHECKED_CAST") override fun onActive() { - super.onActive() - container.onActive(this as LiveData) - queryExecutor.execute(refreshRunnable) - } + super.onActive() + container.onActive(this as LiveData) + eu.faircode.email.Log.i("MMM Active") + synchronized(lock) { + queued.value++ + queryExecutor.execute(refreshRunnable) + } + eu.faircode.email.Log.i("MMM Actived") + } @Suppress("UNCHECKED_CAST") override fun onInactive() { diff --git a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java index 2b5c391dbc..077ae233e8 100644 --- a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java +++ b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java @@ -153,7 +153,7 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona @NonNull LoadInitialCallback callback) { registerObserverIfNecessary(); List list = Collections.emptyList(); - int totalCount; + int totalCount = 0; int firstLoadPosition = 0; RoomSQLiteQuery sqLiteQuery = null; Cursor cursor = null; @@ -171,6 +171,8 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona mDb.setTransactionSuccessful(); list = rows; } + } catch (Throwable ex) { + eu.faircode.email.Log.w(ex); } finally { if (cursor != null) { cursor.close(); diff --git a/patches/roomkt.patch b/patches/roomkt.patch new file mode 100644 index 0000000000..adb762da47 --- /dev/null +++ b/patches/roomkt.patch @@ -0,0 +1,171 @@ +diff --git a/app/src/main/java/androidx/room/InvalidationTracker.kt b/app/src/main/java/androidx/room/InvalidationTracker.kt +index 38067b702f..76cf95815c 100644 +--- a/app/src/main/java/androidx/room/InvalidationTracker.kt ++++ b/app/src/main/java/androidx/room/InvalidationTracker.kt +@@ -405,10 +405,14 @@ open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX + + private fun checkUpdatedTable(): Set { + val invalidatedTableIds = buildSet { +- database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor -> +- while (cursor.moveToNext()) { +- add(cursor.getInt(0)) ++ try { ++ database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor -> ++ while (cursor.moveToNext()) { ++ add(cursor.getInt(0)) ++ } + } ++ } catch (ex: Throwable) { ++ eu.faircode.email.Log.w(ex) + } + } + if (invalidatedTableIds.isNotEmpty()) { +diff --git a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt +index 171b57d16e..e253e98eee 100644 +--- a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt ++++ b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt +@@ -54,62 +54,72 @@ internal class RoomTrackingLiveData ( + val invalid = AtomicBoolean(true) + val computing = AtomicBoolean(false) + val registeredObserver = AtomicBoolean(false) ++ val queued = eu.faircode.email.ObjectHolder(0) ++ val lock = Object() + val refreshRunnable = Runnable { +- if (registeredObserver.compareAndSet(false, true)) { +- database.invalidationTracker.addWeakObserver(observer) +- } +- var computed: Boolean +- do { +- computed = false +- // compute can happen only in 1 thread but no reason to lock others. +- if (computing.compareAndSet(false, true)) { +- // as long as it is invalid, keep computing. +- try { +- var value: T? = null +- while (invalid.compareAndSet(true, false)) { +- computed = true +- try { +- value = computeFunction.call() +- } catch (e: Exception) { +- throw RuntimeException( +- "Exception while computing database live data.", +- e +- ) +- } +- } +- if (computed) { +- postValue(value) +- } +- } finally { +- // release compute lock +- computing.set(false) +- } +- } +- // check invalid after releasing compute lock to avoid the following scenario. +- // Thread A runs compute() +- // Thread A checks invalid, it is false +- // Main thread sets invalid to true +- // Thread B runs, fails to acquire compute lock and skips +- // Thread A releases compute lock +- // We've left invalid in set state. The check below recovers. +- } while (computed && invalid.get()) +- } ++ synchronized(lock) { ++ queued.value-- ++ if (queued.value < 0) { ++ eu.faircode.email.Log.e("$computeFunction queued=$queued.value") ++ queued.value = 0 ++ } ++ } ++ ++ if (registeredObserver.compareAndSet(false, true)) { ++ database.invalidationTracker.addWeakObserver(observer) ++ } ++ ++ var value: T? = null ++ var computed = false ++ synchronized(computeFunction) { ++ var retry = 0 ++ while (!computed) { ++ try { ++ value = computeFunction.call() ++ computed = true ++ } catch (e: Throwable) { ++ if (++retry > 5) { ++ eu.faircode.email.Log.e(e) ++ break ++ } ++ eu.faircode.email.Log.w(e) ++ try { ++ Thread.sleep(2000L) ++ } catch (ignored: InterruptedException) { ++ } ++ } ++ } ++ } ++ if (computed) { ++ postValue(value) ++ } ++ } + + val invalidationRunnable = Runnable { +- val isActive = hasActiveObservers() +- if (invalid.compareAndSet(false, true)) { +- if (isActive) { +- queryExecutor.execute(refreshRunnable) +- } +- } +- } ++ val isActive = hasActiveObservers() ++ if (invalid.compareAndSet(false, true)) { ++ if (isActive) { ++ synchronized(lock) { ++ if (queued.value > 0) { ++ eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug, "$computeFunction queued=$queued.value") ++ } else { ++ queued.value++ ++ queryExecutor.execute(refreshRunnable) ++ } ++ } ++ } ++ } ++ } + + @Suppress("UNCHECKED_CAST") + override fun onActive() { +- super.onActive() +- container.onActive(this as LiveData) +- queryExecutor.execute(refreshRunnable) +- } ++ super.onActive() ++ container.onActive(this as LiveData) ++ synchronized(lock) { ++ queued.value++ ++ queryExecutor.execute(refreshRunnable) ++ } ++ } + + @Suppress("UNCHECKED_CAST") + override fun onInactive() { +diff --git a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java +index 2b5c391dbc..077ae233e8 100644 +--- a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java ++++ b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java +@@ -153,7 +153,7 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona + @NonNull LoadInitialCallback callback) { + registerObserverIfNecessary(); + List list = Collections.emptyList(); +- int totalCount; ++ int totalCount = 0; + int firstLoadPosition = 0; + RoomSQLiteQuery sqLiteQuery = null; + Cursor cursor = null; +@@ -171,6 +171,8 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona + mDb.setTransactionSuccessful(); + list = rows; + } ++ } catch (Throwable ex) { ++ eu.faircode.email.Log.w(ex); + } finally { + if (cursor != null) { + cursor.close();