DrKLO/Telegram · critical · IllegalArgumentException

op should be remove or update.{}

Error message

op should be remove or update.{}

What it means

Thrown in the inner loop of dispatchAndUpdateViewHolders where each position of a multi-position op is walked. The switch only handles UPDATE (positionMultiplier 1) and REMOVE (positionMultiplier 0); any other command value is rejected. This is a defense-in-depth guard: by the time code reaches here the op.cmd has already been filtered at the method entry (error 0), so reaching the default means the UpdateOp object was mutated after submission or a code path built an op with an unexpected cmd constant. It signals corrupted internal state rather than a direct caller mistake.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java:294

        // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
        // TODO Since move ops are pushed to end, we should not need this anymore
        int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
        if (DEBUG) {
            Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
        }
        int tmpCnt = 1;
        int offsetPositionForPartial = op.positionStart;
        final int positionMultiplier;
        switch (op.cmd) {
            case UpdateOp.UPDATE:
                positionMultiplier = 1;
                break;
            case UpdateOp.REMOVE:
                positionMultiplier = 0;
                break;
            default:
                throw new IllegalArgumentException("op should be remove or update." + op);
        }
        for (int p = 1; p < op.itemCount; p++) {
            final int pos = op.positionStart + (positionMultiplier * p);
            int updatedPos = updatePositionWithPostponed(pos, op.cmd);
            if (DEBUG) {
                Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
            }
            boolean continuous = false;
            switch (op.cmd) {
                case UpdateOp.UPDATE:
                    continuous = updatedPos == tmpStart + 1;
                    break;
                case UpdateOp.REMOVE:
                    continuous = updatedPos == tmpStart;
                    break;
            }
            if (continuous) {
                tmpCnt++;

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Audit any custom AdapterHelper.Callback or UpdateOp pool reuse to ensure op.cmd is never mutated post-submission.
  2. Verify ProGuard/R8 keep rules preserve androidx.recyclerview.widget.UpdateOp field values (do not optimize/inline them).
  3. Reproduce with RecyclerView updates serialized on the main thread; if it still occurs, the UpdateOp pool is being corrupted by a custom component.
  4. Align the fork's AdapterHelper source with the matching AndroidX recyclerview version so constants and dispatch logic are consistent.
Defensive patterns

Strategy: validation

Validate before calling

// Validate op.cmd before dispatch (internal fork guard)
static void assertKnownCmd(int cmd) {
    if (cmd != UpdateOp.ADD && cmd != UpdateOp.MOVE
            && cmd != UpdateOp.REMOVE && cmd != UpdateOp.UPDATE) {
        throw new IllegalStateException("unknown cmd " + cmd);
    }
}

Prevention

When it happens

Trigger: A UpdateOp is recycled/reused and its cmd field is overwritten between obtainUpdateOp and consumption; a custom Callback or pre-processor that alters op.cmd; concurrent modification of the pending queue on multiple threads; an obfuscated/modified build (this is a Telegram fork) that changed UpdateOp constants.

Common situations: ProGuard/R8 stripping or reordering of UpdateOp static final constants such that ADD/MOVE/UPDATE/REMOVE no longer match the values the helper expects; a fork that patched AdapterHelper but not UpdateOp; thread-safety bugs in a custom adapter that re-enters the recycler during prefetch.

Related errors


AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14). Data as JSON: /api/errors/681ea328cdf0a60a. Report an issue: GitHub.