{"record":{"id":"681ea328cdf0a60a","repo":"DrKLO/Telegram","slug":"op-should-be-remove-or-update","errorCode":null,"errorMessage":"op should be remove or update.{}","messagePattern":"op should be remove or update\\.(.+?)","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"critical","filePath":"TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java","lineNumber":294,"sourceCode":"\n        // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial\n        // TODO Since move ops are pushed to end, we should not need this anymore\n        int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);\n        if (DEBUG) {\n            Log.d(TAG, \"pos:\" + op.positionStart + \",updatedPos:\" + tmpStart);\n        }\n        int tmpCnt = 1;\n        int offsetPositionForPartial = op.positionStart;\n        final int positionMultiplier;\n        switch (op.cmd) {\n            case UpdateOp.UPDATE:\n                positionMultiplier = 1;\n                break;\n            case UpdateOp.REMOVE:\n                positionMultiplier = 0;\n                break;\n            default:\n                throw new IllegalArgumentException(\"op should be remove or update.\" + op);\n        }\n        for (int p = 1; p < op.itemCount; p++) {\n            final int pos = op.positionStart + (positionMultiplier * p);\n            int updatedPos = updatePositionWithPostponed(pos, op.cmd);\n            if (DEBUG) {\n                Log.d(TAG, \"pos:\" + pos + \",updatedPos:\" + updatedPos);\n            }\n            boolean continuous = false;\n            switch (op.cmd) {\n                case UpdateOp.UPDATE:\n                    continuous = updatedPos == tmpStart + 1;\n                    break;\n                case UpdateOp.REMOVE:\n                    continuous = updatedPos == tmpStart;\n                    break;\n            }\n            if (continuous) {\n                tmpCnt++;","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/DrKLO/Telegram/blob/45ab8f4308496e1f01026a97fcdb0d58a5274474/TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java#L276-L312","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Audit any custom AdapterHelper.Callback or UpdateOp pool reuse to ensure op.cmd is never mutated post-submission.","Verify ProGuard/R8 keep rules preserve androidx.recyclerview.widget.UpdateOp field values (do not optimize/inline them).","Reproduce with RecyclerView updates serialized on the main thread; if it still occurs, the UpdateOp pool is being corrupted by a custom component.","Align the fork's AdapterHelper source with the matching AndroidX recyclerview version so constants and dispatch logic are consistent."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Validate op.cmd before dispatch (internal fork guard)\nstatic void assertKnownCmd(int cmd) {\n    if (cmd != UpdateOp.ADD && cmd != UpdateOp.MOVE\n            && cmd != UpdateOp.REMOVE && cmd != UpdateOp.UPDATE) {\n        throw new IllegalStateException(\"unknown cmd \" + cmd);\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep UpdateOp and AdapterHelper from the same AndroidX release in any fork.","Add ProGuard/R8 keep rules to prevent constant inlining in the recyclerview widget package.","Never mutate an UpdateOp after it is submitted to the helper."],"tags":["recyclerview","internal-invariant","updateop","obfuscation","threading"],"backgroundTag":null,"analyzedSha":"45ab8f4308496e1f01026a97fcdb0d58a5274474","analyzedAt":"2026-08-14T05:19:30.815Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}