Skip to content

Sticker delivery, incremental import, a webp to GIF pipeline, and keyboard UX - #91

Open
patildhruv wants to merge 1 commit into
FredHappyface:mainfrom
patildhruv:feat/fork-work
Open

Sticker delivery, incremental import, a webp to GIF pipeline, and keyboard UX#91
patildhruv wants to merge 1 commit into
FredHappyface:mainfrom
patildhruv:feat/fork-work

Conversation

@patildhruv

@patildhruv patildhruv commented Jul 31, 2026

Copy link
Copy Markdown

This is a large PR and I don't expect it merged as-is. I'm opening it because the work is verified on a physical device (Galaxy S25, One UI) and may be useful to you whole or in pieces. Happy to split it into separate reviewable PRs, or to drop anything you don't want.

Sending

WhatsApp advertises image/webp.wasticker and never plain image/webp, so a webp sticker failed the supported-mimetype check and fell through to the png fallback, arriving as a flattened photo with any animation lost. Vendor mimetypes are now matched on the image/webp. prefix rather than hardcoding wasticker, so another app's equivalent works too.

Also: a wildcard now counts as accepting the original, so an app advertising both image/* and image/gif is no longer handed a downscaled GIF. The png fallback moved off the thread that draws the keyboard, and a decode failure no longer returns a phantom path that commitContent reports as success.

webp to GIF pipeline (new)

For apps that advertise image/gif and no webp mimetype at all, where a GIF is the only way to keep an animated sticker moving. Hand-written demuxer verified byte-exact against the reference decoder, GIF89a encoder with median cut and Floyd-Steinberg dithering, and a Lanczos-3 resampler (createScaledBitmap is bilinear only and loses against the reference).

Output is within 0.13 dB and 6% of tools/gif-variants.py, the desktop reference. A counting sort replaced an insertion sort in median cut: byte-identical output about 10x faster, taking a 53-sticker regeneration from 247s to 32s on device. The encoder, resampler and reader carry no Android imports so they are unit-testable on the JVM; tests are included.

Import

Incremental and non-destructive, driven by a manifest of size and mtime. A no-change reload went from 32s to about 0.3s, and adding one pack copies that pack rather than rebuilding the library and every generated GIF. The SAF tree is listed with one cursor per directory instead of thousands of Binder round trips, which also yields the metadata the manifest needs. Pruning refuses to act on a listing it cannot corroborate, and never infers staleness from timestamps, because copying rewrites the destination's mtime.

Keyboard

Settings and library changes apply to a running keyboard, so both "reload the keyboard" strings are gone in every locale. All packs in one continuous list with a heading per pack and a pack bar that follows the scroll, favourites, sticker deletion, Material You on Android 12+, and a drag-resizable keyboard height stored per orientation (replacing a hard-coded 800 pixels).

Notes

Docs for each area are under docs/. Every behavioural claim came from adb logcat, a screenshot, or a measurement rather than from reasoning.

Testing: the branch is patildhruv:feat/fork-work if you want to try it on a device -- ./gradlew assembleDebug and sideload the result. I'd rather point you at the source than hand over an APK signed with a debug key. Happy to answer anything about the changes here.

The commit carries a Co-Authored-By: Claude trailer. The work was done with AI assistance and I would rather that be visible than not.

…board UX

Work from a personal fork, offered upstream. Every behavioural claim below came
from adb logcat, a screenshot, or a measurement on a physical device rather than
from reasoning. See docs/ for the details and the numbers.

Sending
- WhatsApp advertises image/webp.wasticker and never plain image/webp, so webp
  stickers failed the supported-mimetype check and fell through to the png
  fallback, arriving as flattened photos with any animation lost. Vendor
  mimetypes are now matched by image/webp. prefix rather than hardcoding
  wasticker, so another app's equivalent works too.
- A wildcard counts as accepting the original, so an app advertising both
  image/* and image/gif is no longer handed a downscaled GIF.
- The png fallback moved off the thread that draws the keyboard, and a decode
  failure no longer returns a phantom path that commitContent reports as
  success. A zero-length file no longer satisfies the "already converted" check.

webp to GIF pipeline (new)
- For apps that advertise image/gif and no webp mimetype at all. WebpAnimation
  demuxes an animated webp and composites frames; verified byte-exact against
  the reference decoder. GifEncoder writes GIF89a with median cut and
  Floyd-Steinberg dithering. ImageResampler does Lanczos-3, because
  createScaledBitmap is bilinear only and loses against the reference.
- Output is within 0.13 dB and 6% of tools/gif-variants.py, the desktop
  reference. A counting sort replaced an insertion sort in median cut for
  byte-identical output about 10x faster: regenerating 53 stickers went from
  247s to 32s on device.
- GifEncoder, ImageResampler and the reader are free of Android imports, so
  they are unit-testable on the JVM. Tests included.

Import
- Incremental and non-destructive, driven by a manifest of size and mtime. A
  no-change reload went from 32s to about 0.3s; adding one pack copies that
  pack rather than rebuilding the library and every generated GIF.
- The SAF tree is listed with one cursor per directory instead of thousands of
  Binder round trips, which also yields the metadata the manifest needs.
- Pruning refuses to act on a listing it cannot corroborate, and never infers
  staleness from timestamps, because copying rewrites the destination's mtime.

Keyboard
- Settings and library changes apply to a running keyboard; both "reload the
  keyboard" strings are gone, in every locale.
- All packs in one continuous list with a heading per pack, and a pack bar that
  follows the scroll. Favourites, and deleting a sticker. Drag-resizable
  keyboard height, stored per orientation, replacing a hard-coded 800 pixels.
- Material You on Android 12+, with selection drawn as a container rather than
  a colour filter over the sticker artwork.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant