mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Simplify and improve the JumpToDateValidator.
This commit is contained in:
committed by
Alex Hart
parent
d6ade56233
commit
657a7d2a6b
@@ -200,7 +200,7 @@ class ConversationViewModel(
|
||||
|
||||
private val startExpiration = BehaviorSubject.create<MessageTable.ExpirationInfo>()
|
||||
|
||||
private val _jumpToDateValidator: JumpToDateValidator by lazy { JumpToDateValidator(threadId) }
|
||||
private val _jumpToDateValidator: JumpToDateValidator by lazy { JumpToDateValidator.create(threadId) }
|
||||
val jumpToDateValidator: JumpToDateValidator
|
||||
get() = _jumpToDateValidator
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import com.google.android.material.datepicker.CalendarConstraints.DateValidator
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logTime
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.LRUCache
|
||||
import java.time.Instant
|
||||
@@ -18,90 +16,136 @@ import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.concurrent.locks.Condition
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Interface for looking up whether messages exist on given dates.
|
||||
* Allows for easy testing without database dependencies.
|
||||
*/
|
||||
private typealias MessageDateLookup = (Collection<Long>) -> Map<Long, Boolean>
|
||||
|
||||
/**
|
||||
* A calendar validator for jumping to a specific date in a conversation.
|
||||
* This is used to prevent the user from jumping to a date where there are no messages.
|
||||
*
|
||||
* [isValid] is called on the main thread, so we try to race it and fetch the data ahead of time, fetching data in bulk and caching it.
|
||||
* If data is not yet cached, we return true to avoid blocking the main thread.
|
||||
*/
|
||||
@Parcelize
|
||||
class JumpToDateValidator(val threadId: Long) : DateValidator {
|
||||
class JumpToDateValidator private constructor(
|
||||
private val threadId: Long,
|
||||
@IgnoredOnParcel private val messageExistanceLookup: MessageDateLookup = createDefaultLookup(threadId),
|
||||
@IgnoredOnParcel private val executor: Executor = SignalExecutors.BOUNDED,
|
||||
private val zoneId: ZoneId = ZoneId.systemDefault()
|
||||
) : DateValidator {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(JumpToDateValidator::class.java)
|
||||
|
||||
fun createDefaultLookup(threadId: Long): MessageDateLookup {
|
||||
return { dayStarts -> SignalDatabase.messages.messageExistsOnDays(threadId, dayStarts) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun create(threadId: Long): JumpToDateValidator {
|
||||
return JumpToDateValidator(
|
||||
threadId = threadId,
|
||||
messageExistanceLookup = createDefaultLookup(threadId),
|
||||
executor = SignalExecutors.BOUNDED,
|
||||
zoneId = ZoneId.systemDefault()
|
||||
).also {
|
||||
it.performInitialPrefetch()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createForTesting(lookup: MessageDateLookup, executor: Executor, zoneId: ZoneId): JumpToDateValidator {
|
||||
return JumpToDateValidator(
|
||||
threadId = -1,
|
||||
messageExistanceLookup = lookup,
|
||||
executor = executor,
|
||||
zoneId = zoneId
|
||||
).also {
|
||||
it.performInitialPrefetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val lock = ReentrantLock()
|
||||
private val cachedDates: MutableMap<Long, LookupState> = LRUCache(1000)
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val condition: Condition = lock.newCondition()
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val cachedDates: MutableMap<Long, LookupState> = LRUCache(500)
|
||||
|
||||
init {
|
||||
val startOfDay = LocalDate.now(ZoneId.systemDefault())
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
loadAround(startOfDay, allowPrefetch = true)
|
||||
}
|
||||
private val pendingMonths: MutableSet<Long> = mutableSetOf()
|
||||
|
||||
override fun isValid(dateStart: Long): Boolean {
|
||||
return lock.withLock {
|
||||
val localMidnightTimestamp = Instant.ofEpochMilli(dateStart)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
val localMidnightTimestamp = normalizeToLocalMidnight(dateStart)
|
||||
|
||||
var value = cachedDates[localMidnightTimestamp]
|
||||
|
||||
while (value == null || value == LookupState.PENDING) {
|
||||
loadAround(localMidnightTimestamp, allowPrefetch = true)
|
||||
condition.await()
|
||||
value = cachedDates[localMidnightTimestamp]
|
||||
return synchronized(this) {
|
||||
when (cachedDates[localMidnightTimestamp]) {
|
||||
LookupState.FOUND -> true
|
||||
LookupState.NOT_FOUND -> false
|
||||
LookupState.PENDING, null -> {
|
||||
loadAround(localMidnightTimestamp, allowPrefetch = true)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
cachedDates[localMidnightTimestamp] == LookupState.FOUND
|
||||
}
|
||||
}
|
||||
|
||||
private fun performInitialPrefetch() {
|
||||
val today = LocalDate.now(zoneId)
|
||||
val monthsToPrefetch = 6
|
||||
|
||||
for (i in 0 until monthsToPrefetch) {
|
||||
val targetMonth = today.minusMonths(i.toLong()).atStartOfDay(zoneId).toInstant().toEpochMilli()
|
||||
loadAround(targetMonth, allowPrefetch = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeToLocalMidnight(timestamp: Long): Long {
|
||||
return Instant.ofEpochMilli(timestamp)
|
||||
.atZone(zoneId)
|
||||
.toLocalDate()
|
||||
.atStartOfDay(zoneId)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a date, this will load all of the dates for entire month the date is in.
|
||||
*/
|
||||
private fun loadAround(dateStart: Long, allowPrefetch: Boolean) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val startOfDay = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateStart), ZoneId.systemDefault())
|
||||
val startOfDay = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateStart), zoneId)
|
||||
|
||||
val startOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.firstDayOfMonth())
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
val startOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.firstDayOfMonth())
|
||||
.atZone(zoneId)
|
||||
.toLocalDate()
|
||||
.atStartOfDay(zoneId)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
val endOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.lastDayOfMonth())
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
val endOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.lastDayOfMonth())
|
||||
.atZone(zoneId)
|
||||
.toLocalDate()
|
||||
.atStartOfDay(zoneId)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
synchronized(this) {
|
||||
if (pendingMonths.contains(startOfMonth)) {
|
||||
return
|
||||
}
|
||||
pendingMonths.add(startOfMonth)
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
val daysOfMonth = (startOfMonth..endOfMonth step 1.days.inWholeMilliseconds).toSet() + dateStart
|
||||
|
||||
val lookupsNeeded = lock.withLock {
|
||||
val lookupsNeeded = synchronized(this) {
|
||||
daysOfMonth
|
||||
.filter { !cachedDates.containsKey(it) }
|
||||
.filter { cachedDates[it] == null }
|
||||
.onEach { cachedDates[it] = LookupState.PENDING }
|
||||
}
|
||||
|
||||
@@ -109,26 +153,18 @@ class JumpToDateValidator(val threadId: Long) : DateValidator {
|
||||
return@execute
|
||||
}
|
||||
|
||||
val existence = logTime(TAG, "query(${lookupsNeeded.size})", decimalPlaces = 2) {
|
||||
SignalDatabase.messages.messageExistsOnDays(threadId, lookupsNeeded)
|
||||
}
|
||||
val existence = messageExistanceLookup.invoke(lookupsNeeded)
|
||||
|
||||
lock.withLock {
|
||||
synchronized(this) {
|
||||
cachedDates.putAll(existence.mapValues { if (it.value) LookupState.FOUND else LookupState.NOT_FOUND })
|
||||
|
||||
if (allowPrefetch) {
|
||||
val dayInPreviousMonth = startOfMonth - 1.days.inWholeMilliseconds
|
||||
if (!cachedDates.containsKey(dayInPreviousMonth)) {
|
||||
loadAround(dayInPreviousMonth, allowPrefetch = false)
|
||||
}
|
||||
loadAround(dayInPreviousMonth, allowPrefetch = false)
|
||||
|
||||
val dayInNextMonth = endOfMonth + 1.days.inWholeMilliseconds
|
||||
if (!cachedDates.containsKey(dayInNextMonth)) {
|
||||
loadAround(dayInNextMonth, allowPrefetch = false)
|
||||
}
|
||||
loadAround(dayInNextMonth, allowPrefetch = false)
|
||||
}
|
||||
|
||||
condition.signalAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user