-
Notifications
You must be signed in to change notification settings - Fork 173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SES-1966] Attachment batch download and tidy-up #1507
[SES-1966] Attachment batch download and tidy-up #1507
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work - some optional comments for you to address as you see fit. Approved!
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
@bemusementpark Wanna have a look again? I've extracted the said Flow operator to |
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
app/src/test/java/org/thoughtcrime/securesms/util/FlowUtilsKtTest.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
…ownload-task # Conflicts: # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few ideas, mostly the naming, maybe it's more of a JobScheduler
, or a Debouncer
, than a Helper
, which suggests more benign impedance matching/courtesy functions, rather than actual business logic.
val maxItems = 5 | ||
|
||
// When | ||
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf()) | |
val result = flow.timedBuffer(timeoutMillis, maxItems).toList() |
Can we use built-ins? result doesn't need to be a MutableList
. Don't unnecessarily expose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! ChatGPT really needs to up its game!
app/src/test/java/org/thoughtcrime/securesms/util/FlowUtilsKtTest.kt
Outdated
Show resolved
Hide resolved
val pendingAttachmentIDs = storage | ||
.getAllPendingJobs(AttachmentDownloadJob.KEY, AttachmentUploadJob.KEY) | ||
.values | ||
.mapNotNullTo(hashSetOf()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be more readable to just mapNotNull
, and toSet
, if you want performance, then convert to sequence above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Let's go for readability
// Before handling out attachment to the download task, we need to | ||
// check the requisite for that attachment. This check is very likely to be | ||
// performed again in the download task, but adding stuff into job system | ||
// is expensive so we need to avoid spawning new task whenever we can. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a detailed low-level comment, but there's no comment on this class or entire init block, so the intention of this big chain is kind of unclear. Consider adding higher level comments and/or breaking this out into a well-named function .
init {
initAttachmentProcessing() // <- insert better name here
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tided up
downloadRequests | ||
.receiveAsFlow() | ||
.timedBuffer(BUFFER_TIMEOUT_MILLS, BUFFER_MAX_ITEMS) | ||
.map { attachments -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe some fun deduplicate(attachment)
could replace this block with .map(::deduplicate)
and make this chain a lot more manageable/readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
val threadRecipient = storage.getRecipientForThread(message.threadId) | ||
if (threadRecipient == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have to retrieve a thread
AND a Contact
before we can return a boolean ||
?
Is it not valid if we have a thread, but no contact?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not, have updated to reflect this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic was inserted here trying to replicate the same in AttachmentDownloadJob.execute
. I guess it's better to extract the logic from that function and use it here.
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHelper.kt
Outdated
Show resolved
Hide resolved
|
||
if (buffer.size < maxItems) { | ||
// If the buffer is not full, wait until the time limit is reached | ||
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L)) | |
delay(System.currentTimeMillis() + timeoutMillis - bufferBeganAt)) |
Don't need it to be non-negative, <= 0
returns immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A negative number doesn't make sense for delay
. If I am the implementator of delay
, I would throw an exception. Were you looking at the implementation of the delay
itself? I'd prefer not to rely on the leniency of such implementation details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did look at the source, haha, yeah, but it's also documented:
delay
suspend fun delay(timeMillis: Long)(source)
Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time. If the given timeMillis is non-positive, this function returns immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bemusementpark I have addressed some of your concerns and also reverted some changes to make my intent clearer.
if (message.isOutgoing) return true | ||
|
||
val sender = message.individualRecipient.address.serialize() | ||
val contact = storage.getContactWithSessionID(sender) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is just introducing unnecessary mental load. It takes no time for the original code to read but it'd probably take me more than 1 sec to read with this style. Also I feel really awkward to have inter-inline function returns in Kotlin, it seems like after-thought after introducing these inline functions that could change the main syntax flow.
|
||
if (buffer.size < maxItems) { | ||
// If the buffer is not full, wait until the time limit is reached | ||
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A negative number doesn't make sense for delay
. If I am the implementator of delay
, I would throw an exception. Were you looking at the implementation of the delay
itself? I'd prefer not to rely on the leniency of such implementation details.
val maxItems = 5 | ||
|
||
// When | ||
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! ChatGPT really needs to up its game!
} | ||
|
||
val threadRecipient = storage.getRecipientForThread(message.threadId) | ||
if (threadRecipient == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not, have updated to reflect this
// Before handling out attachment to the download task, we need to | ||
// check the requisite for that attachment. This check is very likely to be | ||
// performed again in the download task, but adding stuff into job system | ||
// is expensive so we need to avoid spawning new task whenever we can. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tided up
} | ||
|
||
val threadRecipient = storage.getRecipientForThread(message.threadId) | ||
if (threadRecipient == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic was inserted here trying to replicate the same in AttachmentDownloadJob.execute
. I guess it's better to extract the logic from that function and use it here.
downloadRequests | ||
.receiveAsFlow() | ||
.timedBuffer(BUFFER_TIMEOUT_MILLS, BUFFER_MAX_ITEMS) | ||
.map { attachments -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
val pendingAttachmentIDs = storage | ||
.getAllPendingJobs(AttachmentDownloadJob.KEY, AttachmentUploadJob.KEY) | ||
.values | ||
.mapNotNullTo(hashSetOf()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Let's go for readability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm liking the KDoc and structure here! maybe a tiny bit more Boolean simplification could help, otherwise LGTM!
* requests will go through different level of checking before they are queued for download. | ||
* | ||
* To use this handler, call [onAttachmentDownloadRequest] with the attachment that needs to be | ||
* downloaded. The call to [onAttachmentDownloadRequest] is cheap and can be called multiple times. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love it
jobQueue.add( | ||
AttachmentDownloadJob( | ||
attachmentID = attachment.attachmentId.rowId, | ||
databaseMessageID = attachment.mmsId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is cool, now this is the first interesting part you see, which is clearly adding jobs, so kinda self-documenting IMHO.
* time [timeoutMillis] limit. | ||
*/ | ||
fun <T> Flow<T>.timedBuffer(timeoutMillis: Long, maxItems: Int): Flow<List<T>> { | ||
return channelFlow { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=
could save some brackets and indentation.
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) { | ||
return false | ||
} | ||
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it simpler, like this?
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) { | |
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && !selfSend) { |
or even:
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) { | |
return threadRecipient.isGroupRecipient || contact?.isTrusted == true || selfSend |
actually, that is a big mental unlock for me understanding this easily without squinting.
if ( its a group or its a trusted contact, or its from us )
then its good 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh I have no idea what that check actually check. This whole check is existing code and I figure I'd reuse the same
} | ||
|
||
val contact = sender?.let { storage.getContactWithSessionID(it) } | ||
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
If they're in a group are they definitely a contact? this part requires that we have a contact for them, even if they're in a group. Is that necessary? (I don't know the full semantic of a
Contact
) -
I think the last of this is partially built into the next if statement.
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) { | |
if (threadRecipient == null || sender == null)) { |
but then you can probably inline the threadRecipient and sender :
val threadRecipient = storage.getRecipientForThread(threadID) ?: return false
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's one of those painful "what do you mean by contact" problem that we have on Android.
From doing some quick tests we were able to see a photo in a group from someone who isn't an actual contact. But this db call probably means something different by "contact", especially with the isTrusted
property in there.
if (message.isOutgoing) return true | ||
|
||
val sender = message.individualRecipient.address.serialize() | ||
val contact = storage.getContactWithSessionID(sender) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of the ?: run { ... }
syntax myself, but once the logs are gone and it's a 1-liner it's nicer ?: return false
I thought these would maintain the same functionality just reduce the repetitive references to each val.
* fix: Authenticate all Open Group API calls * Use unblinded authentication when we have `capabilities` data for the open group server we are sending the request to but don't have the `blind` capability * Use blinded authentication when we haven't gotten any `capabilities` for an open group server, or if we have `capabilities` and the server has the `blind` capability * Hide send button when message contains only whitespace * Fix bug displaying user ID when quoting own message * Fix notification update for incoming unsend request * Improve check if author is own user when quoting messages * Fixed video call auto rotate, when auto rotate is disabled * refactor: simplify comparison * Stop playing message if deleted * Accidental change * Accidental change * Comments * Feedback * Comments * Import * Fix delete message for everyone doesn't stop the audio playing * Correct the usage of flowOn * Import * Optimise XML * Remove unused file * Remove view pools * Remove the use of executor in ThreadUtils * Using trim and empty to capture semantic concept of nothing being in there * Remove config checks (PR 1294) Refactor: remove checks for whether new config is enabled throughout config factory generation. First commit from PR 1294. * [SES-2162] - Remove wrapping of config message (#1517) * Remove wrapping of config message * Addresses feedback * Merged in ThreadUtils fix * JDK installation * Revert JDK change --------- Co-authored-by: fanchao <[email protected]> * Update libsession * [SES-337] Add rounded corners to thumbnail in QuoteView (#1285) * Add rounded corners to thumbnail in QuoteView * Simplify ThumbnailView * Cleanup ThumbnailView * Removed include custom attributes The custom attributes are not passed to the view. I added the radius programatically instead. * Clipping whole thumbnail view instead of just the image requests --------- Co-authored-by: AL-Session <[email protected]> Co-authored-by: ThomasSession <[email protected]> * Highlight @you mentions (#985) * Highlight @you mentions * fix: resolve merge conflicts * Setting the proper design rules for mentions * New RoundedBackgroundSpan, applied to "you" mentions The rounded background highlighter can take padding, so there is no need to add those extra spaces at the start and end. * Better mention highlight logic Some mention highlight should only format the text and not apply any styling. Also making sure we cater for all cases properly * Updated the text color logic based on design rules * Fine tuning the color rules * Removing usage of Resources.getSystem() Only making the db call if there actually is a mention * Moving color definition outside the loop to avoid repetitions --------- Co-authored-by: charles <[email protected]> Co-authored-by: 0x330a <[email protected]> Co-authored-by: ThomasSession <[email protected]> * [SES-2018] Refactor mention (#1510) * Refactor mention * Fixes robolectric test problem * Fixes tests * Naming and comments * Naming * Dispatcher --------- Co-authored-by: fanchao <[email protected]> * [SES-1966] Attachment batch download and tidy-up (#1507) * Attachment batch download * Addressed feedback and test issues * Feedback fixes * timedWindow for flow * Feedback * Dispatchers * Remove `flowOn` * New implementation of timedBuffer * Organise import * Feedback * Fix test * Tidied up logic around `eligibleForDownload` * Updated comment --------- Co-authored-by: fanchao <[email protected]> * Fix issue with span being the full length (#1528) * Proper display of unresolved names in mentions (#1530) * Fix issue with span being the full length * Making sure a mention with a username without a resolved name still displayed with the appropriate style with the truncated is * Testnet build (#1532) Co-authored-by: fanchao <[email protected]> * Allow "public.loki.foundation" to be accessed by http (#1534) Co-authored-by: fanchao <[email protected]> * Bumping the version code and name * Reverting temporary change --------- Co-authored-by: charles <[email protected]> Co-authored-by: andrew <[email protected]> Co-authored-by: aaronkerckhoff <[email protected]> Co-authored-by: Rugved Darwhekar <[email protected]> Co-authored-by: 0x330a <[email protected]> Co-authored-by: fanchao <[email protected]> Co-authored-by: Fanchao Liu <[email protected]> Co-authored-by: AL-Session <[email protected]> Co-authored-by: ceokot <[email protected]>
Description
This PR reworks some aspect of the attachment download:
AttachmentDownloadHelper
is created to batch the download request.AttachmentDownloadJob
There's one more fix on the
ThreadUtils
where theExecutorService
seems to misbehave. Changed over to use the Kotlin Coroutine's IO dispatcher.