Skip to content

[Bug] Cover-art comparison can call Bitmap.sameAs() on the UI thread #222

@venkyqz

Description

@venkyqz

Hi Clementine Team,

I’m a PhD student researching Android thread-related issues. My research group recently ran a static-analysis scan for thread-related bugs in real-world F-Droid apps, and our prototype flagged a potential issue in the Clementine app.

Checked target

  • Source-level caller: de.qspool.clementineremote.ui.fragments.playerpages.PlayerPageFragment.updateTrackMetadata()
  • Detected API / pattern: Bitmap.sameAs(...) used during cover-art refresh
  • Observed context: main/UI thread, through fragment lifecycle and UI metadata update paths
  • Expected context: avoid pixel-by-pixel bitmap comparison on the UI thread; run it on a worker thread or compare lightweight artwork identifiers instead

What I found

PlayerPageFragment.updateTrackMetadata() is a UI refresh method. It updates track title/artist/album labels, seek-bar state, and cover-art ImageView state. In the same method, it compares the previous and current cover-art bitmaps using Bitmap.sameAs(...):

Bitmap newArt = currentSong.getArt();
Bitmap oldArt = mCurrentSong.getArt();

if (newArt == null) {
    mImgArt.setImageResource(R.drawable.ic_notif_large);
} else if (oldArt == null || !oldArt.sameAs(newArt)) {
    if (mFirstCall) {
        mImgArt.setImageBitmap(newArt);
    } else {
        mImgArt.startAnimation(mAlphaDown);
    }
}

The method is reachable from UI-facing paths, including fragment lifecycle updates and metadata updates delivered to the player page.

Verified bug trace

Clementine backend connection receives CURRENT_METAINFO
  -> sendUiMessage(clementineMessage)
  -> main/UI Handler dispatches the message
  -> PlayerFragment.MessageFromClementine(...)
  -> PlayerPageFragment.MessageFromClementine(...)
  -> PlayerPageFragment.updateTrackMetadata()
  -> oldArt.sameAs(newArt)

This means a cover-art equality check can be performed synchronously on the main/UI thread.

Why this matters

Android documents Bitmap.sameAs(Bitmap) as a potentially expensive operation because it compares bitmap pixel data. The documentation says it may take several seconds and should only be called from a worker thread.

In this case, the compared bitmaps are cover-art images. If the artwork is large, if metadata updates are frequent, or if this occurs during UI transitions, the app can suffer from jank, delayed input handling, or short UI freezes.

This is a performance/threading issue rather than a guaranteed crash. It may be hard to reproduce with small artwork, but the pattern is fragile because the main thread performs a pixel-by-pixel bitmap comparison.

Possible fix

Avoid using Bitmap.sameAs(...) directly on the UI thread.

A cleaner design is to compare a lightweight artwork identity instead of pixel data, for example:

  • album-art URL/path/id from the Clementine metadata,
  • a stable artwork version or cache key,
  • a hash computed when the artwork is decoded or received on a worker thread.

If pixel equality is truly required, run the comparison on a worker thread and post only the UI update back to the main thread.

Example sketch:

private final ExecutorService artworkExecutor = Executors.newSingleThreadExecutor();

private void updateCoverArtAsync(final Bitmap oldArt, final Bitmap newArt) {
    artworkExecutor.execute(() -> {
        final boolean changed = oldArt == null || !oldArt.sameAs(newArt);

        mImgArt.post(() -> {
            if (!isAdded()) {
                return;
            }

            if (newArt == null) {
                mImgArt.setImageResource(R.drawable.ic_notif_large);
            } else if (changed) {
                if (mFirstCall) {
                    mImgArt.setImageBitmap(newArt);
                } else {
                    mImgArt.startAnimation(mAlphaDown);
                }
            }
        });
    });
}

An even safer patch would avoid pixel comparison entirely and use metadata/cache identity to decide whether the cover art changed.

Duplicate check

I searched the current GitHub issues for related terms such as sameAs, Bitmap.sameAs, and cover art, and did not find an existing matching report.

References

  • Android Bitmap.sameAs(Bitmap) documentation: the method compares pixel data and should only be called from a worker thread.
  • Source-level caller: PlayerPageFragment.updateTrackMetadata().
  • Message-delivery path: Clementine backend connection sends received protocol messages to the UI handler before fragment update.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions