Skip to content

Refactor mms compression to use a binary search for images, and a one pass algorithm for GIFs - #826

Open
octoshrimpy wants to merge 4 commits into
masterfrom
feature/mms-compression-refactor
Open

Refactor mms compression to use a binary search for images, and a one pass algorithm for GIFs#826
octoshrimpy wants to merge 4 commits into
masterfrom
feature/mms-compression-refactor

Conversation

@octoshrimpy

@octoshrimpy octoshrimpy commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator
  • fix(mms): Replace broken compression loop with binary search
  • refactor(mms): Add an algorithm for compressing GIFs in one pass

Closes: #825
Closes: #116
Closes: #657
Closes: #759

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the MMS attachment compression path in MessageRepositoryImpl, introducing a binary-search-based image downscaling approach plus a quality-reduction fallback, and adds an ImageUtils helper to support the fallback compression flow.

Changes:

  • Add ImageUtils.getScaledImageWithQuality(...) to iteratively reduce JPEG quality toward a target byte size.
  • Refactor MMS attachment building to route attachment byte generation through a new compressedAttachmentBytes(...) helper.
  • Replace the prior image compression loop with a binary search over image dimensions, with logging around compression attempts.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt Adds a helper to retry JPEG compression at decreasing quality levels until a target byte size is met (best-effort).
data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt Refactors MMS attachment processing and introduces compressedAttachmentBytes(...) implementing binary-search dimension compression with fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt Outdated
@Inhishonor
Inhishonor force-pushed the feature/mms-compression-refactor branch from 1074e1f to a4c6c54 Compare June 16, 2026 14:06

@Inhishonor Inhishonor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to improve the architecture here, right now it is too performance heavy, but the compression also runs before the message is even marked as sending, so nothing appears in the UI. We should run the compression after marking it as sending. But we also need to fix some of the performance bottlenecks.

Comment thread data/src/main/java/com/moez/QKSMS/util/ImageUtils.kt Outdated
Comment thread data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt Outdated
Scale factor algorithm became <= 0 after 4-5 attempts regardless of
image size, silently bailing and leaving images potentially over the
carrier size limit. Replace with binary search on dimensions (100px to
carrier max) plus a quality-reduction fallback (90→40) for images that
still don't fit. Adds compressAttachment dispatch for extensibility
(audio/* logged, image/* compressed, else raw). Fixes InputStream leak
in BitmapFactory dimension probe. Queries MMS_CONFIG_ALLOW_ATTACH_AUDIO
for future audio support groundwork.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Inhishonor
Inhishonor force-pushed the feature/mms-compression-refactor branch from a4c6c54 to 1486596 Compare June 30, 2026 17:02
@Inhishonor Inhishonor changed the title feature/mms compression refactor Refactor mms compression to use a binary search for images, and a one pass algorithm for GIFs Jun 30, 2026
octoshrimpy and others added 2 commits August 1, 2026 17:15
Addresses the review feedback on #826.

Compression searches for a size that fits under the carrier limit, so it encodes
the same image many times: a binary search over dimensions, then a quality ladder
from 90 down to 40 if nothing fit. Every one of those attempts went through
ImageUtils.getScaledImage, which re-opened the source URI and re-decoded it from
scratch. Decoding is the expensive part and it was the only part being repeated
unnecessarily; a large photo could be decoded a dozen or more times to send once.

ScaledImageEncoder decodes the source a single time, sampled down to no smaller
than the largest width the search will ask for, and re-encodes that bitmap from
memory for each attempt. The search itself is unchanged.

The decode is guarded: with no carrier cap the target size is the original image,
which on a modern camera sensor is large enough to matter, so an OutOfMemoryError
returns null and the caller falls back to Glide, which streams instead of holding
the whole bitmap.

Also fixes three edge cases raised in review:

- A bounds decode that fails leaves outWidth/outHeight at zero, making the aspect
  ratio 0/0 and every derived dimension garbage. Detect it and fall back rather
  than divide by it.

- searchHi was floored at 100, which raised it above the original width, and above
  the carrier maximum when that was under 100. Both upscale the image and inflate
  the payload. The floor is now 1 and the search starts at 1 rather than 100.

- The last-resort fallback hard-coded a width of 100, upscaling anything narrower.
  It is now bounded by searchHi, so it can only shrink.
@octoshrimpy

Copy link
Copy Markdown
Collaborator Author

Pushed 2bd62f2. You and the bot were pointing at the same thing: every attempt went through getScaledImage, which reopened and re-decoded the source. Binary search does that once per step, quality ladder another six, so a big photo got decoded a dozen plus times to send once.

ScaledImageEncoder now decodes once per attachment and re-encodes from memory. Search logic unchanged. Decode is guarded, since with no carrier cap the target is the original image, so OOM falls back to Glide.

Copilot's three are fixed too: bounds decode failing left aspect ratio at 0/0, searchHi floored at 100 upscaled anything narrower, same for the hard-coded 100 in the fallback.

Haven't touched the ordering yet, want your read first. Fixing it properly means creating a provider record before its parts exist, which risks half formed messages in telephony. If decode-once makes the stall imperceptible I'd rather not take that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants