on main help sign in
Add PATCH.md: how and what to patch after a Telegram update

A guide for keeping MultiGram on top of new Forkgram (and so Telegram) releases: what to do for each sync outcome, how to fix a conflicting replay or a compile failure, how to re-apply the six commits by hand, every hook with the exact text to find and the change to make, plain Telegram instead of Forkgram, how to verify, and how to keep this guide current. Written for multigram 4780eb095b on Forkgram 12.10.6 (Telegram 12.10.5). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mVy1fu8zBn38wHgJqmLNz

commit49adb9a182d8f4ccb05e582990534a58361c1958 authorClaude <noreply@anthropic.com> dateSun 27 Sep 2026, 10:47 (2 weeks ago)
1 file changed+3719
PATCH.md+3719
PATCH.mdnew file+3719
@@ -0,0 +1,3719 @@
1+# MultiGram patch guide
2+
3+This guide says how to keep MultiGram working after Telegram, and then Forkgram, release an update.
4+
5+MultiGram is Forkgram plus six commits of its own. Usually the automatic sync moves those commits onto each new Forkgram release by itself. When it cannot, this guide tells you what to do. It also lists every place where MultiGram edits a Forkgram file, with the exact text, so that you can put each change back by hand.
6+
7+## 1. About this document
8+
9+### 1.1 Stamp
10+
11+| | |
12+|---|---|
13+| Written for | `multigram` at 4780eb095b |
14+| Built on | Forkgram 12.10.6 (Forkgram commit b994e1446e), which is Telegram 12.10.5 (DrKLO commit dc780e81e, "update to 12.10.5 (7105)") |
15+| Forkgram snapshot | `forkgram` at 4543767655 |
16+| Sync scripts | `main` at da0cc7c548 |
17+| Date | 2026-09-27 |
18+
19+Line numbers, counts and outputs in this guide are from that state. When you use it after an update, expect line numbers to have moved. Every hook is therefore described by its content, not only by its line. Section 11 says how to bring this guide up to date.
20+
21+Output marked "example output" comes from a test with a made-up Forkgram release. Its commit hashes do not exist in the real repository.
22+
23+### 1.2 Words used here
24+
25+| Word | Meaning in this guide |
26+|---|---|
27+| Upstream | The code MultiGram builds on: Forkgram, which is Telegram's Android app (DrKLO/Telegram) plus Forkgram's own changes. |
28+| Snapshot | A commit on the `forkgram` branch that holds one Forkgram release, without its `.github` folder. |
29+| Stack | The six MultiGram commits on top of the snapshot. Together with the snapshot they form the `multigram` branch. |
30+| Hook | A small edit in an upstream file that calls MultiGram code. Most hooks are one line that ends with `// MultiGram: ...`. |
31+| New file | A file the stack adds. It never conflicts, but it can stop compiling when an upstream name it uses changes. |
32+| Replay | Moving the stack onto a new snapshot with `git rebase`. |
33+| Conflict | A place where git cannot combine an upstream change with a MultiGram change by itself. |
34+| rerere | Git's "reuse recorded resolution": it remembers how you fixed a conflict and repeats the fix, but only in the same clone. |
35+| Worktree | An extra working folder of the same clone, made with `git worktree add`, with its own branch checked out. |
36+| CI | "Continuous integration": here, the GitHub Actions workflows that compile and check each candidate. |
37+| Static initializer | Java code in a `static { ... }` block. It runs once, when the class is first used. `Theme`'s static initializer loads the themes at app start. |
38+| ARGB | A colour as four bytes: alpha (opacity), red, green, blue, written `0xAARRGGBB` or `#AARRGGBB`. `.attheme` files store it as a signed decimal number. |
39+| Accent | The main colour of a theme. A **runtime accent** is one made on the device (MultiGram's generated style, or a colour picked with the colour wheel). Its id is above 100, and it either has no server theme or has one the user created (`accent.info.creator`, for example a custom accent the user shared). |
40+| Contrast, WCAG | How readable a colour is on its background, as a ratio from 1 : 1 to 21 : 1. The usual minimum (the WCAG guideline) is 4.5 : 1 for text and 3 : 1 for icons. |
41+| Overlay A | The palette fix's list of colour changes, in `multigram/palette-fix/overlay_A.json`. |
42+| Simulator | Python code in `multigram/tools/sim/` that recomputes Telegram's theme colours without Android. It makes overlay A and checks the style table. |
43+| Seed | A random number stored once per install. The random style and the shapes are derived from it. |
44+| Knob | One shape or layout choice derived from the seed, such as the bubble corner radius. |
45+| use_next | An input of the sync workflow. It means "take my fixed `multigram-next` as it is, compile it, and promote it". |
46+
47+### 1.3 Branches
48+
49+| Branch or tag | What it holds |
50+|---|---|
51+| `forkgram` | One snapshot commit per Forkgram release. |
52+| `multigram` | The latest snapshot plus the stack. This is the app. |
53+| `main` | README, the workflows and the scripts in `scripts/`. |
54+| `multigram-next` | The next `multigram`, while it is being checked or fixed. |
55+| `multigram-before-<version>` | A tag on each `multigram` the sync replaced. |
56+| `docs` | This guide. |
57+
58+### 1.4 What you need
59+
60+- A clone of `iko-soy/Multigram` with the right to push branches and run workflows.
61+- git, bash, and Python 3.9 or later. The MultiGram tools use only Python's standard library.
62+- For a local compile only: JDK 17 and the Android SDK. The CI has both, so you can skip this.
63+- Optional: the GitHub CLI `gh`. The `gh` commands in this guide were not run while writing it.
64+
65+### 1.5 How a hook is described
66+
67+Section 8 describes each of the 62 hooks in the same way:
68+
69+- **File** and **Where**: the file, the class and method, and (for upstream files) the line where the text starts in Forkgram 12.10.6.
70+- **Find this**: text to search for. It is copied from the file, so a plain text search finds it. For K1 and for K18 to K24 it is the text as an earlier stack commit left the file, so it has no Forkgram line number.
71+- **Change it to**: the change as a diff, copied from the stack commit. Lines starting with `-` go, lines starting with `+` are added, and the other lines are context that stays.
72+- **Why**: what the hook does.
73+- **If the code moved**: how to find the right place when the text above is gone.
74+
75+## 2. After a Telegram update: what to do
76+
77+1. Open the repository's **Issues** and look for an open issue labelled `forkgram-sync`.
78+2. Open **Actions > Sync with Forkgram** and look at the latest run.
79+3. Pick the row that matches:
80+
81+| What you see | What it means | Go to |
82+|---|---|---|
83+| No open issue. The last run is green, and `multigram` sits on the new snapshot (see the command below). | The stack moved by itself. | Build an APK and test it (10.4, 10.5). Also re-derive the palette values once (8.2.5, a few seconds): a colour change upstream can make them too weak without any conflict. |
84+| An issue "Forkgram sync needs a hand" that says the stack "does not apply" | A conflict: git could not replay one of the commits. | Section 5 |
85+| An issue that says "the compile check failed", and the failed step is **Compile** | The Java code no longer compiles. | Section 6 |
86+| The same, but the failed step is **Run MultiGram tool checks** | `check.sh` found a problem. | 10.2 |
87+| The rebase conflicts in many places, or Forkgram moved or rewrote the code around the hooks | Replaying is not practical. | Section 7, with section 8 for each hook |
88+| Forkgram released, but there is no run and no issue | The schedule has not run yet, or Forkgram's `dev` branch has not moved. | Section 4.3 (run the sync by hand) |
89+| The issue or the run says `next-edited` | A fix of yours on `multigram-next` is waiting. The sync did not replace it. | 4.2 |
90+| The issue or the run says `behind` | `multigram` is still on an older snapshot, because an earlier sync could not move it. | 4.2: run the sync with **force**. If it conflicts, section 5. |
91+| Telegram has released, but Forkgram has not moved for months | Forkgram has slowed down or stopped. | 4.5 |
92+| You want to build on plain Telegram instead of Forkgram | | Section 9 |
93+
94+To see which Forkgram release `multigram` is built on:
95+
96+```sh
97+git fetch origin
98+git log -1 --format=%B origin/forkgram
99+git merge-base --is-ancestor origin/forkgram origin/multigram && echo "multigram is on the latest snapshot"
100+```
101+
102+## 3. The stack at a glance
103+
104+### 3.1 The six commits
105+
106+| Commit | What it does | Upstream files touched | New files | How it is checked |
107+|---|---|---|---|---|
108+| d457ac74ae Add the rebrand toolkit | A build-time tool that gives a build its own name, app id, icons, signing key and account type. A stock build is unchanged. | 4: `AppIconsSelectorCell.java`, `DialogsActivity.java`, `ForkSettingsActivity.java`, `TMessagesProj_App/build.gradle` (+9 −3 lines; hooks R1–R5) | 7 | `multigram/rebrand/selftest.py` (run by `check.sh` since 4780eb095b). No script checks the four Java hooks: use the `git grep` in 10.3. |
109+| 5b53859682 Fix palette contrast for generated accents | Makes grey texts readable (contrast 4.5 : 1), and makes marks on accent fills readable for runtime accents. | 13: `Theme.java`, `ThemeColors.java`, `EmojiThemes.java`, `ChannelColorActivity.java`, `ChatActivityEnterView.java`, `MessageEntityView.java`, `PeerColorActivity.java`, `DarkThemeResourceProvider.java` (+30 −18 lines), and 5 `.attheme` files (36 values); hooks P1–P18 | 4 | `multigram/tools/palette_fix_check.py`; the simulator (8.2.5) |
110+| cddd62df98 Add the style table and its generator | A precomputed table of 20,480 readable colour styles, and the Python tools that make and check it. Also creates `check.sh`. | none | 17 | `check_style_table.py`, `make_style_table.py --check` |
111+| 27d32d57b4 Give each install its own random style | Each fresh install picks a day and a night style from the table. Adds "Shuffle my style" to Chat Settings. Generated styles are never uploaded. | 7: `ApplicationLoader.java`, `MessagesController.java`, `Theme.java`, `AlertsCreator.java`, `DefaultThemesPreviewCell.java`, `ThemeActivity.java`, `ThemePreviewActivity.java` (+16 lines; hooks S1–S15) | 6. It also changes `check.sh` and `multigram/tools/README.md`. | `multigram/tools/random_style_check.py` |
112+| 3b941c01de Randomise shapes and chat list layout per install | Derives corner radii, the chat list layout and the wallpaper phase from the install's seed. | 10: `ApplicationLoader.java`, `Theme.java`, `ThemeActivity.java`, `ChatActionCell.java`, `FragmentFloatingButton.java`, `GradientButtonWithCounterView.java`, `ReactionsLayoutInBubble.java`, `RecyclerListView.java`, `UniversalRecyclerView.java`, `ButtonWithCounterView.java` (+23 −16 lines; hooks K1–K17) | 3. It also changes `RandomStyle.java` and `RandomStyleUi.java` (hooks K18–K22), `check.sh` (K23), and two READMEs. | `multigram/tools/style_knobs_check.py` |
113+| 4780eb095b Run the rebrand self-test in the CI checks | Adds the rebrand self-test to `check.sh`. | none | none. It changes `check.sh` (K24). | `check.sh` itself |
114+
115+In total the stack adds 37 new files and touches 30 upstream files:
116+
117+- 25 Java or Gradle files: 78 added lines (3 of them blank) and 37 removed lines. Most removed lines are replaced by a changed copy.
118+- 5 `.attheme` files: 36 changed values.
119+
120+72 of the added lines carry the word `MultiGram`. The other six are the 3 blank lines, and 3 lines whose `// MultiGram:` comment sits on the line above them (hooks R2, R3 and R5).
121+
122+### 3.2 Why the order matters
123+
124+Keep the six commits in this order. Later commits build on earlier ones:
125+
126+- 27d32d57b4 needs 5b53859682: `RandomStyle` uses `PaletteFix.LAST_STOCK_ACCENT_ID`, and generated styles are only readable with the palette fix.
127+- 27d32d57b4 needs cddd62df98: it reads the style table asset, and it adds a step to `check.sh`, which cddd62df98 creates.
128+- 3b941c01de needs 27d32d57b4: `StyleKnobs` uses `RandomStyle`, hook K1 sits on the line after hook S1, and the commit changes `RandomStyle.java` and `RandomStyleUi.java`.
129+- 4780eb095b needs d457ac74ae (the self-test) and cddd62df98 (`check.sh`).
130+- The file `overlay_A.json` exists twice: `multigram/palette-fix/overlay_A.json` (from 5b53859682) and `multigram/tools/sim/palette-fix/overlay_A.json` (from cddd62df98). The two copies must stay byte-identical. No script checks this; the `cmp` line in 5.4 does.
131+
132+## 4. The automatic sync, briefly
133+
134+The full description is in `README.md` on `main` ("How the sync works" and "When the sync issue opens"), and in `bash scripts/sync-forkgram.sh --help`. This section is a summary.
135+
136+### 4.1 What it does
137+
138+The workflow **Sync with Forkgram** (`.github/workflows/sync-forkgram.yml` on `main`) runs every 6 hours, at 23 minutes past the hour (`cron: '23 */6 * * *'`, UTC). You can also start it by hand. It has four jobs:
139+
140+1. **sync** runs `scripts/sync-forkgram.sh`. It reads the tip of Forkgram's `dev` branch. If the tip is new, it adds a snapshot commit to `forkgram`. Then it replays the stack onto the new snapshot as `multigram-next`, with `git rebase --onto`. It pushes `forkgram` and `multigram-next` in one push.
141+2. **compile** runs the workflow **Build MultiGram** on `multigram-next`: the Java compile and `check.sh` (see 10.1).
142+3. **promote** runs only if the compile passed. It tags the old `multigram` as `multigram-before-<version>`, moves `multigram` to `multigram-next`, and deletes `multigram-next`.
143+4. **report** (`scripts/sync-report.py`) opens, updates or closes the issue labelled `forkgram-sync`. The issue title is "Forkgram sync needs a hand (Forkgram <version>)". It holds the exact commands for the case at hand.
144+
145+A new snapshot's commit message names its source in the lines `Source:`, `Forkgram-Commit:`, `Forkgram-Version:` and `Telegram-Base:`. The last one is left out when the sync cannot find Telegram's "update to ..." commit in Forkgram's history.
146+
147+### 4.2 What each outcome means
148+
149+The sync script ends by printing `status=<value>`.
150+
151+| Status | What it means | What you do |
152+|---|---|---|
153+| `up-to-date` | Nothing new. | Nothing. |
154+| `candidate` | The stack was replayed onto the new snapshot. The compile job runs next. | Wait for the run to finish. |
155+| `promoted` (promote job) | `multigram` moved to the new release. The old tip is tagged `multigram-before-<version>`. | Build an APK and test it (10.4, 10.5). |
156+| `unchanged` (promote job) | `multigram` already was the candidate. `multigram-next` was deleted. | Nothing. |
157+| `conflict` (exit status 2) | A stack commit does not apply. `forkgram` has the new snapshot, and `multigram` is unchanged. | Section 5. |
158+| A failed compile job | The stack applied, but the compile or `check.sh` failed. `multigram-next` holds the candidate. | Section 6. |
159+| `next-edited` | `multigram-next` has commits the sync did not make (your fix). The sync changed nothing. | Run the workflow with **use_next**, or delete the branch (`git push origin --delete multigram-next`). |
160+| `behind` | No new release, but `multigram` is still on an older snapshot, because an earlier problem was never fixed. | Finish the fix and run **use_next**, or run with **force** to retry. |
161+| `error` | Something else: Forkgram unreachable, a rejected push, a `.github` folder in the stack, or a merge commit with changes of its own. | Read the message on the issue, fix the cause, run again. |
162+
163+Keep `multigram` linear: rebase, never merge. The replay flattens merges, and a merge commit with changes of its own stops the sync.
164+
165+### 4.3 Running the sync by hand
166+
167+In the browser: **Actions > Sync with Forkgram > Run workflow**. The inputs:
168+
169+| Input | Use it when |
170+|---|---|
171+| (none) | You want an extra run now, for example right after a Forkgram release. |
172+| **force** | You want to rebuild `multigram-next` although Forkgram has no new release: to retry after a failed compile, or to test the pipeline. |
173+| **use_next** | You fixed `multigram-next` yourself. The run compiles it and, if that passes, promotes it. It first checks that `multigram-next` carries every commit on `multigram`, matched by author and subject, or by patch content. |
174+| **allow_drop** | Only with **use_next**: your `multigram-next` leaves out some commits on purpose. |
175+
176+The same from a terminal (not run while writing this guide):
177+
178+```sh
179+gh workflow run sync-forkgram.yml -R iko-soy/Multigram --ref main -f use_next=true
180+gh workflow run sync-forkgram.yml -R iko-soy/Multigram --ref main -f force=true
181+gh run list -R iko-soy/Multigram --workflow sync-forkgram.yml -L 5
182+gh run rerun <run-id> -R iko-soy/Multigram --failed
183+```
184+
185+The last command is the same as the **Re-run failed jobs** button. Use it when a run failed because of a runner or network problem.
186+
187+### 4.4 Running the sync locally
188+
189+This uses your clone's rerere memory (5.6). Run it from a checkout of `main`. It refuses to run while `forkgram` or `multigram-next` is checked out in any worktree.
190+
191+```sh
192+git fetch origin
193+git switch main
194+bash scripts/sync-forkgram.sh --remote origin
195+```
196+
197+That leaves the results in your local `forkgram` and `multigram-next` branches. Add `--push` to push them too. Sections 5 to 7 start from `origin/forkgram`, so use `--push` when you go on to them. After a local `--push`, start the workflow with **use_next**, not **force**: a forced run starts from a fresh clone without your rerere memory, and would hit the same conflict again.
198+
199+To test the sync scripts themselves against fake repositories:
200+
201+```sh
202+bash scripts/selftest-sync.sh
203+```
204+
205+On `main` da0cc7c548 it ended with `183 passed, 0 failed` after about 10 seconds.
206+
207+### 4.5 When Forkgram stops
208+
209+Forkgram follows Telegram with a delay. If Telegram has released and Forkgram's `dev` branch has not moved for months, compare the `Telegram-Base` line of `git log -1 --format=%B origin/forkgram` with the newest "update to ..." commit at https://github.com/DrKLO/Telegram. Then choose one of these:
210+
211+- **Stay on the last snapshot.** Nothing to do. The scheduled sync reports `up-to-date` until Forkgram moves again.
212+- **Follow another fork.** The sync script reads its source from the environment variables `FORKGRAM_URL` and `FORKGRAM_BRANCH` (defaults `https://github.com/forkgram/TelegramAndroid` and `dev`), or from the options `--url` and `--branch`. It takes the version from `APP_VERSION_NAME` in the source's `gradle.properties`. On Actions, add both variables to the `env:` of the **Sync** step in `.github/workflows/sync-forkgram.yml` on `main`, then run the workflow. The workflow does not set them today.
213+- **Move to plain Telegram.** Read section 9 first. The sync can take Telegram itself as the source, in the same way.
214+
215+To see what the sync makes of another source before you change the workflow, run it locally from a checkout of `main` (4.4), without `--push`:
216+
217+```sh
218+bash scripts/sync-forkgram.sh --remote origin --url https://github.com/DrKLO/Telegram --branch master
219+```
220+
221+On 2026-09-27, with DrKLO's `master` at dc780e81e, it made a snapshot titled `Forkgram 12.10.5 snapshot` (the script says "Forkgram" whatever the source) with a correct `Telegram-Base` line. Then it ended with `status=conflict` at commit 1 of 6: the three conflicts of 9.1. With any new source, expect to settle conflicts with section 5 or 7. Afterwards update the stamp (11.2).
222+
223+This trial run leaves its snapshot in your local `forkgram` branch, and a trial `multigram-next` if the replay went through. A later local sync that finds nothing new does not reset them. Never push them: 5.1 pushes your local `forkgram`. Put `forkgram` back with `git branch -f forkgram origin/forkgram`. If there is a trial `multigram-next`, delete it with `git branch -D multigram-next`.
224+
225+## 5. Fixing a conflicting replay
226+
227+Use this section when the sync issue says the stack "does not apply". The issue names the commit that stopped and the files involved.
228+
229+### 5.1 Replay the stack by hand
230+
231+The new snapshot must be on `origin/forkgram`. After a sync on Actions it is. If you ran the sync locally (4.4) without `--push`, push the snapshot first with `git push origin forkgram`.
232+
233+```sh
234+git fetch origin
235+git switch -C multigram-next origin/multigram
236+git config rerere.enabled true
237+git rebase origin/forkgram
238+```
239+
240+The third line switches on rerere for this clone (5.6). Git replays the six commits one by one. It stops at the first one that conflicts and names the files. If git answers `Current branch multigram-next is up to date.`, `origin/forkgram` is still the old snapshot: `git log -1 --format=%B origin/forkgram` shows which release it holds.
241+
242+`git switch -C` resets a local `multigram-next`. If yours holds work you have not pushed and want to keep, rename it first: `git branch -m multigram-next my-fix`.
243+
244+The issue shows a longer form, `git rebase --onto <new snapshot> <old snapshot>`. It does the same thing, because each new snapshot is a child of the previous one.
245+
246+### 5.2 Read and fix a conflict
247+
248+Example output, from a test with a made-up Forkgram 12.10.7 that changed the line under hook S2:
249+
250+```text
251+Rebasing (1/6)Rebasing (2/6)Rebasing (3/6)Rebasing (4/6)Auto-merging TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java
252+CONFLICT (content): Merge conflict in TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java
253+error: could not apply 27d32d57b4... Give each install its own random style
254+```
255+
256+The file then contains this (example output; the first line inside the markers is the made-up Forkgram change):
257+
258+```text
259+<<<<<<< HEAD
260+ if (themeInfo == null || accent == null) {
261+=======
262+ if (org.telegram.messenger.multigram.RandomStyle.isUploadBlocked(themeInfo, accent)) return; // MultiGram: a generated style never leaves the device
263+ if (themeInfo == null) {
264+>>>>>>> 27d32d57b4 (Give each install its own random style)
265+ return;
266+ }
267+```
268+
269+How to read it:
270+
271+- Between `<<<<<<< HEAD` and `=======` is the **new Forkgram** code.
272+- Between `=======` and `>>>>>>>` is the **old Forkgram** code with the MultiGram hook in it.
273+- Never take the MultiGram side as a whole. It would bring back Forkgram's old code.
274+- Git also reports a conflict when Forkgram only changed a line next to a hook, as here.
275+
276+How to fix it depends on the hook. Its "Change it to" block in section 8 shows which kind it is:
277+
278+- **The hook only adds lines** (the block has no `-` line), as S2 here. Keep Forkgram's new code and put the MultiGram line back where the hook's "Where" says. If Forkgram added a line at the same spot, the hook's "Where" and "If the code moved" say which comes first. For example, P11 goes after every calculated-colour call, and S10 goes directly after `themeListRow2 = rowCount++;`.
279+- **The hook replaces an upstream line** (the block has a `-` line), as R2 to R4, P1 to P6, P8, P12, P14, P15, P18, K3 to K7, K9 to K14 and K16. Take Forkgram's new line and make the MultiGram change to it again. Keep only that one line, not both. For example, if Forkgram changed the default title from `"Fork Client"` to `"Forkgram"`, R2 becomes `getString("forkCustomTitle", org.telegram.messenger.multigram.Rebrand.defaultTitle("Forkgram"))`. For the colour lines of P1 to P6, 5.3 says what to do.
280+- Delete the three marker lines.
281+
282+Four hooked files end without a final newline: `assets/arctic.attheme`, `assets/day.attheme`, `ui/Components/RecyclerListView.java` and `ui/PeerColorActivity.java`. Keep it that way. Many editors add one when they save (in vim, `:set nofixendofline` stops that). Check with `git diff`: it must not show the last line as changed. An added newline makes `palette_fix_check.py --base` report `unexpected arctic.attheme change: <last line>`, and `style_knobs_check.py --base` report `added line without a "MultiGram:" marker: }`.
283+
284+The fixed text in this example:
285+
286+```text
287+ if (org.telegram.messenger.multigram.RandomStyle.isUploadBlocked(themeInfo, accent)) return; // MultiGram: a generated style never leaves the device
288+ if (themeInfo == null || accent == null) {
289+```
290+
291+To see which hooks the stopped commit puts in a file, run `git show REBASE_HEAD -- <file>`. Section 8 explains each hook, and 5.3 says what a conflict in each file usually means.
292+
293+When a file is fixed:
294+
295+```sh
296+git add <file>
297+git rebase --continue
298+```
299+
300+Repeat until git prints `Successfully rebased and updated refs/heads/multigram-next.` To give up and go back, run `git rebase --abort`.
301+
302+### 5.3 What a conflict in each file usually means
303+
304+| File | Hooks | A conflict here usually means |
305+|---|---|---|
306+| `ui/Cells/AppIconsSelectorCell.java` | R1 | Forkgram changed how the app icon list is built. Put the filter call after the list is filled. |
307+| `ui/DialogsActivity.java` | R2 | Forkgram changed the chat list title code. Wrap every literal default of `forkCustomTitle` again. |
308+| `ui/ForkSettingsActivity.java` | R3, R4 | Forkgram changed its own settings screen. Wrap the custom title defaults again. If the file was renamed, git reports it as deleted: find the new file with `git grep -n '"forkCustomTitle"'`. |
309+| `TMessagesProj_App/build.gradle` | R5 | Forkgram added lines at the end of the file. Keep them, and keep the `apply from:` line last. |
310+| `ui/ActionBar/ThemeColors.java` | P1 | Forkgram changed a stock colour next to, or on, one of the 11 grey lines. Take MultiGram's line for the 11 keys that carry `// MultiGram: contrast fix`, and Forkgram's line for every other key. The simulator is not in the tree at this stop (it comes with cddd62df98), so re-derive the values after the rebase (5.4). |
311+| `assets/*.attheme` (5 files) | P2–P6 | Forkgram changed a colour of a bundled theme. As for ThemeColors: MultiGram's value for the keys of P2 to P6, Forkgram's for all others, and re-derive after the rebase. |
312+| `ui/ActionBar/EmojiThemes.java` | P7 | Forkgram changed how per-chat themes build their colours. Put the call back after `fillAccentColors`, in `createColors` only. |
313+| `ui/ActionBar/Theme.java` | P8–P12, S3, S4, K2 | Forkgram changed the theme engine. Keep the new code and put each hook back at the same logical place. Then diff the engine methods, because the simulator copies them (8.3.6), and re-derive the palette values (8.2.5). |
314+| `ui/ChannelColorActivity.java`, `ui/PeerColorActivity.java`, `ui/Components/Paint/Views/MessageEntityView.java` | P13, P17, P16 | The screen's preview colour code changed. Put the call back after `fillAccentColors`. |
315+| `ui/Components/ChatActivityEnterView.java` | P14, P15 | The send button's drawing code changed. Every mark drawn on the button's fill must use `multigramGlyphColor` when it is not 0. |
316+| `ui/Stories/DarkThemeResourceProvider.java` | P18 | The dark provider's colour lookup changed. Its fallback must go through `getOverrideFallbackColor`. |
317+| `messenger/ApplicationLoader.java` | S1, K1 | Forkgram changed the app start. S1 and K1 must still run after `applicationContext` is set and before anything loads `Theme`. |
318+| `messenger/MessagesController.java` | S2 | Forkgram changed the theme upload. The guard must stay the first statement of `saveThemeToServer`. |
319+| `ui/Components/AlertsCreator.java` | S5 | The "create theme" dialog changed. The call goes right after the fragment check. |
320+| `ui/DefaultThemesPreviewCell.java` | S6 | The theme strip changed. The call stays the first statement of `updateDayNightMode()`. |
321+| `ui/ThemePreviewActivity.java` | S7 | The accent editor changed. The call stays right after the editor gets its accent. |
322+| `ui/ThemeActivity.java` | S8–S15, K15–K17 | Chat Settings changed: rows, menus or the list adapter. Put each hook back by its landmark (the rows, the reset menu, the click listener, the view types). `random_style_check.py` and `style_knobs_check.py` check the placement. |
323+| `ui/Cells/ChatActionCell.java` | K3, K4 | The service pill drawing changed. Wrap the corner constants again, and keep the offset in step. |
324+| `ui/Components/FragmentFloatingButton.java` | K5–K7 | The floating button changed. Route its background, outline and small-button radius through `StyleKnobs` again. |
325+| `ui/Components/Premium/boosts/GradientButtonWithCounterView.java` | K8 | The gradient button changed. Keep `setRoundRadius(8)` at the end of its constructor. |
326+| `ui/Components/Reactions/ReactionsLayoutInBubble.java` | K9, K10 | Reaction chip drawing changed. Both radius lines must use the same function. |
327+| `ui/Components/RecyclerListView.java`, `ui/Components/UniversalRecyclerView.java` | K11, K12 | The settings card code changed. Wrap the default radius in every default `setSections` overload. |
328+| `ui/Stories/recorder/ButtonWithCounterView.java` | K13, K14 | The shared button changed. The field and the constructor must both use `radiusDp`. |
329+
330+All Java paths above start with `TMessagesProj/src/main/java/org/telegram/`, and the `.attheme` files are in `TMessagesProj/src/main/assets/`.
331+
332+The stack's new files never conflict: they do not exist in Forkgram.
333+
334+### 5.4 Finish and hand it back
335+
336+When the rebase is done, check the result. These are the checks the CI runs, plus stricter versions:
337+
338+```sh
339+bash multigram/tools/check.sh
340+python3 multigram/tools/palette_fix_check.py --base origin/forkgram
341+python3 multigram/tools/random_style_check.py --base origin/forkgram
342+python3 multigram/tools/style_knobs_check.py --base origin/forkgram
343+git grep -n -e 'multigram\.Rebrand\.' -e 'multigram/rebrand/rebrand.gradle' -- TMessagesProj TMessagesProj_App
344+git grep -n '"forkCustomTitle"' -- TMessagesProj/src/main/java
345+cmp multigram/palette-fix/overlay_A.json multigram/tools/sim/palette-fix/overlay_A.json && echo "overlay copies identical"
346+```
347+
348+- `check.sh` must end with `== all MultiGram checks passed` (about 1 to 3 minutes; section 10.2 explains each part).
349+- Each of the three Python checks must print one line ending in `OK`. With `--base`, `random_style_check.py` and `style_knobs_check.py` also require the `MultiGram:` marker on every non-blank line the stack adds to their hooked files, and `palette_fix_check.py` checks that `ThemeColors.java` and the `.attheme` files change only the keys of overlay A.
350+- The first `git grep` must print 5 lines (hooks R1 to R5). Only R5 is also checked by a script (the rebrand self-test in `check.sh`).
351+- The second `git grep` lists every use of the title setting. On 4780eb095b it prints 4 lines. Each line that reads the setting must pass its default through `Rebrand.defaultTitle(` or use `defaultValue` (R2 to R4). A doubled line here means a conflict was fixed by keeping both sides.
352+- The `cmp` must print `overlay copies identical`. If it does not, copy `multigram/palette-fix/overlay_A.json` over the other copy and fold that into "Add the style table and its generator" (6.3).
353+
354+If the conflict was in `ThemeColors.java`, an `.attheme` file or `Theme.java`, also re-derive the palette values (8.2.5).
355+
356+Then push, and let the sync compile and promote your branch:
357+
358+```sh
359+git push --force origin multigram-next
360+```
361+
362+Open **Actions > Sync with Forkgram > Run workflow**, tick **use_next**, and run it. If the compile passes, `multigram` moves to your branch and the issue closes.
363+
364+### 5.5 Leaving a commit out
365+
366+If Forkgram now does what a MultiGram commit did, run `git rebase --skip` when git stops at that commit. The commit is then left out. Tick **allow_drop** as well as **use_next** when you run the workflow, or it refuses a branch that lacks a commit.
367+
368+The automatic replay drops a commit by itself when it becomes empty because Forkgram took the same change. The closing note on the issue lists such commits.
369+
370+### 5.6 rerere: git's memory of your fixes
371+
372+With `rerere.enabled`, git records how you fixed each conflict. If you redo the same rebase in the same clone, git fills in the same fix. It still stops, and you still run `git add` and `git rebase --continue`. To have git stage its replayed fixes by itself, also run `git config rerere.autoUpdate true`.
373+
374+rerere only knows the fixes recorded in the clone where you made them. The scheduled sync starts from a fresh clone every time. So it reports the same conflict again until `multigram` carries your fix. That is why you push `multigram-next` and run **use_next**.
375+
376+### 5.7 Using patches instead of a rebase
377+
378+You can also export the stack as patch files and apply them to any Forkgram checkout. From a checkout of `main`:
379+
380+```sh
381+git fetch origin
382+bash scripts/export-patches.sh --base origin/forkgram --head origin/multigram
383+```
384+
385+It writes six numbered `.patch` files and `APPLY.txt` to the folder `multigram-patches/`. `APPLY.txt` says how to clone the matching Forkgram release and apply them with `git am -3`. The `-3` lets git merge like a rebase when the text around a hook changed.
386+
387+When a patch conflicts, git stops. The markers read `<<<<<<< HEAD` (Forkgram) and `>>>>>>> <commit subject>`. Fix the file as in 5.2, then:
388+
389+```sh
390+git add <file>
391+git am --continue
392+```
393+
394+Other commands: `git am --skip` leaves a patch out, `git am --abort` goes back, and `git am --show-current-patch=diff` shows the patch that failed. To export again into the same folder, add `--force` to `export-patches.sh`.
395+
396+A shallow clone (`--depth 1`) of a newer Forkgram release lacks the old files that `-3` needs. Git then fails with `Repository lacks necessary blobs to fall back on 3-way merge.` Fetch the release the patches were made on into the same clone, and start again:
397+
398+```sh
399+git am --abort
400+git fetch --depth 1 origin tag 12.10.6.0
401+git am -3 /path/to/multigram-patches/*.patch
402+```
403+
404+The tag is the Forkgram version the patches were exported from, plus `.0`.
405+
406+## 6. Fixing a compile failure
407+
408+Use this section when the sync issue says "the compile check failed". The stack applied, and the candidate is on `multigram-next`.
409+
410+### 6.1 Find what failed
411+
412+Open the run page linked from the issue. In the job **Compile Java and Kotlin**, see which step failed:
413+
414+- **Compile**: a Java or Kotlin error. Read on.
415+- **Run MultiGram tool checks**: `check.sh` failed. See 10.2.
416+- A step failed with a network or runner error: press **Re-run failed jobs**. Nothing else is needed.
417+
418+From a terminal (not run while writing this guide):
419+
420+```sh
421+gh run view <run-id> -R iko-soy/Multigram --log-failed
422+```
423+
424+The Java compiler prints each error as `<file>:<line>: error: <message>`. Search the log for `error:`.
425+
426+### 6.2 Common errors
427+
428+| The error mentions | Likely cause | Fix |
429+|---|---|---|
430+| `cannot find symbol` and `isMonet()` in `PaletteFix.java` or `RandomStyle.java` | Forkgram renamed or removed `Theme.ThemeInfo.isMonet()`. | Use the new name, or drop the Monet condition. The calls are at `PaletteFix.java` lines 165 and 225, and `RandomStyle.java` lines 449 and 624. |
431+| `cannot find symbol` and a `key_...` name | Telegram renamed or removed a colour key in `Theme.java`. | Rename it everywhere MultiGram names it, with and without the `key_` prefix (see below the table). The `git grep` below the table finds every place. Most keys of overlay A are in `PaletteFix.java`, both `overlay_A.json` copies, `overlay_B.json` and the simulator (`sim/readability.py`, `sim/canvas.py`, two in `sim/engine.py`). A few keys are also in `style_table.py`, `style_knobs_check.py`, `RandomStyleUi.java` or a README. Fix the simulator first: until then `canvas.py --build` stops with a Python `KeyError` naming the old key. Then re-derive the palette (8.2.5). |
432+| `reference to getColor is ambiguous` in `PaletteFix.java` | Upstream added another `Theme.getColor` with three arguments. | Change `Theme.getColor(key, null, true)` at `PaletteFix.java` line 316 to `Theme.getColor(key, (boolean[]) null, true)`. |
433+| `getAccent`, `saveThemeAccents`, `deleteThemeAccent` or a `ThemeAccent` field in `RandomStyle.java` | Telegram changed the accent API. | Read the new methods and adjust `RandomStyle` (8.3.6). |
434+| `SharedConfig.bubbleRadius`, `useThreeLinesLayout` or `setUseThreeLinesLayout` in `StyleKnobs.java` | Telegram renamed a chat setting. | Use the new names (8.4.6). |
435+| `duplicate case label` in `ThemeActivity.java` | Upstream now binds one of the three knob row types itself. | Call `bindStyleKnobRow` from upstream's `case` and remove hook K17's line. |
436+| `cannot find symbol` and `DEFAULT` in `Rebrand.java` | Forkgram renamed `LauncherIconController.LauncherIcon.DEFAULT`. | Point `Rebrand.filterLauncherIcons` at the new default icon entry (R1). |
437+| `cannot find symbol` for a variable used in a hook line, such as `applyingTheme`, `accent` or `sparseIntArray` | Upstream renamed a local variable or field. | Use the new name in the hook line. Keep the `// MultiGram:` comment. If a check script matches that line exactly, update its pattern too. |
438+| `cannot find symbol` for any other method, class or field, in a file under `org/telegram/messenger/multigram/` | Upstream renamed or removed a name that a new file uses. The "Upstream names it relies on" columns of 8.1.2, 8.2.2, 8.3.2 and 8.4.2 list these names. | Find the new name, and fix every use (see below the table). `check.sh` does not find this; only the compile does. |
439+| A Gradle message that starts with `[rebrand]` | `rebrand.gradle` found a problem. | See the list of messages in 8.1.5. |
440+| A Gradle error that the task `buildNativeDeps` does not exist | Forkgram renamed or removed its native build task. The compile command skips it with `-x :TMessagesProj:buildNativeDeps`. | Update `.github/workflows/build.yml` on `main`. |
441+
442+**When upstream renamed a name.** To see what became of it, search the upstream changes since the snapshot `multigram` is built on. The first command below prints each changed line that holds the old name, with three lines around it, so the new line next to a removed one shows too. The second lists every place MultiGram uses the old name, in the new files and the tools:
443+
444+```sh
445+git diff $(git merge-base origin/multigram origin/forkgram) origin/forkgram -- TMessagesProj/src/main/java | grep -C 3 '<old name>'
446+git grep -n -w '<old name>' -- TMessagesProj/src/main/java/org/telegram/messenger/multigram multigram
447+```
448+
449+For a colour key, search for both spellings, as in `git grep -n -w -e key_chat_inViews -e chat_inViews -- TMessagesProj/src/main/java/org/telegram/messenger/multigram multigram`: the Java code uses the `key_` prefix, the theme files and some tools do not.
450+
451+A rename can hit files of several stack commits. For example, a rename of `Theme.getActiveTheme()` hits `PaletteFix.java` (5b53859682) and `RandomStyle.java` and `RandomStyleUi.java` (27d32d57b4). Fold each file into its own commit (6.3).
452+
453+### 6.3 Fold the fix into the commit it belongs to
454+
455+The fix must go into the stack commit it belongs to, not on top as a seventh commit. A "fixup" commit and an automatic rebase do that.
456+
457+**1. Get the branch.** Coming from 6.1 (a failed compile on Actions), start from the candidate the sync pushed:
458+
459+```sh
460+git fetch origin
461+git switch -C multigram-next origin/multigram-next
462+```
463+
464+Skip these two commands if you are already on your own `multigram-next` (from section 5, or from 7.5). `git switch -C` would replace your local branch, and everything on it you have not pushed, with the remote one.
465+
466+**2. Find the commit each change belongs to.** This lists the six commits with their short hashes:
467+
468+```sh
469+git log --oneline origin/forkgram..multigram-next
470+```
471+
472+Each file, and each line, belongs to the commit that added it. For a file the stack adds: `git log --format='%h %s' --diff-filter=A origin/forkgram..multigram-next -- <file>`. For line `n` of any file: `git blame -s -L n,n HEAD -- <file>` (with `HEAD`, it reads the committed file, so it also works after you have edited the line). The tables in 3.1 and 7.2 help too.
473+
474+A fix can belong to several commits. For example, a rename in upstream code can hit `PaletteFix.java` (5b53859682) and `RandomStyle.java` (27d32d57b4).
475+
476+**3. Make one fixup commit per stack commit.** Name the files. Do not use `git commit -a`: it puts every edited file into one fixup, so changes land in the wrong commit, and the rebase can stop with the `CONFLICT (modify/delete)` described at the end of this step. The sync issue and `README.md` on `main` give the same per-commit command.
477+
478+```sh
479+git commit --fixup=<hash of commit A> -- <files of commit A>
480+git commit --fixup=<hash of commit B> -- <files of commit B>
481+GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash origin/forkgram
482+```
483+
484+If one file needs changes for two commits (`Theme.java` holds hooks of three), edit the lines of one commit, run its `git commit --fixup`, then edit the lines of the next.
485+
486+`--fixup` makes a commit titled `fixup! <subject>`. `--autosquash` melts it into the commit with that subject. `GIT_SEQUENCE_EDITOR=:` accepts the plan without opening an editor. Afterwards the branch has six commits again, with the same subjects and authors: `git log --oneline origin/forkgram..multigram-next` shows them.
487+
488+If the rebase stops with `CONFLICT (modify/delete): <file> deleted in HEAD and modified in ... (fixup! ...)`, a fixup holds a file that its commit does not have yet. Run `git rebase --abort`, then `git reset HEAD~1` for one fixup commit (`HEAD~2` for two, and so on). That removes the fixup commits and keeps your edits in the files. Then make the fixups again, one per commit.
489+
490+**4. Hand it back.** Coming from 7.5, go on with 7.6 instead. Otherwise push:
491+
492+```sh
493+git push --force origin multigram-next
494+```
495+
496+Then open **Actions > Sync with Forkgram > Run workflow** and tick **use_next**.
497+
498+### 6.4 Compiling locally
499+
500+This is what the CI runs. It was not run while writing this guide, because it needs JDK 17 and the Android SDK:
501+
502+```sh
503+git submodule update --init --depth 1 TMessagesProj/lib/jlatexmath TMessagesProj_Modules/media
504+./gradlew --no-daemon --stacktrace -x :TMessagesProj:buildNativeDeps :TMessagesProj:compileReleaseJavaWithJavac :TMessagesProj_App:compileAfatReleaseJavaWithJavac
505+bash multigram/tools/check.sh
506+```
507+
508+Gradle finds the SDK through `ANDROID_HOME` or a `local.properties` file with `sdk.dir=<path>`. The CI also appends `org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g` to `gradle.properties`. Do that if Gradle runs out of memory, but do not commit it.
509+
510+## 7. Re-applying the stack by hand
511+
512+Use this section when a rebase is not practical: many conflicts, or files that Forkgram moved or rewrote. You start from the new snapshot and add MultiGram's changes again.
513+
514+The result must again be six commits with the same subjects and authors. Then the sync's **use_next** accepts it.
515+
516+If a rebase from section 5 is still running, stop it first with `git rebase --abort`: git refuses to switch branches during a rebase.
517+
518+### 7.1 Get the base
519+
520+The new Forkgram release must be on `forkgram` first. The sync pushes the new snapshot even when the replay conflicts, so after a sync run it is there. If no sync has run, run it locally from a checkout of `main` (4.4) with `--push`.
521+
522+Then:
523+
524+```sh
525+git fetch origin
526+git switch -C multigram-next origin/forkgram
527+OLD=origin/multigram
528+OLDSNAP=$(git merge-base $OLD origin/forkgram)
529+git log -1 --format=%B origin/forkgram
530+```
531+
532+- `OLD` is the stack you re-apply. A tag `multigram-before-<version>` works too.
533+- `OLDSNAP` is the old snapshot that `OLD` sits on.
534+- The last command shows which Forkgram release you are on.
535+- `git switch -C` resets a local `multigram-next`, as in 5.1.
536+
537+The next steps use these two shell variables. If you open a new terminal, set them again.
538+
539+### 7.2 Copy all new files
540+
541+One command copies all 37 files the stack adds, as they are on `OLD`:
542+
543+```sh
544+git checkout $OLD -- multigram TMessagesProj/src/main/java/org/telegram/messenger/multigram \
545+ TMessagesProj/src/main/res/values/multigram_rebrand.xml TMessagesProj/src/main/res/values/multigram_strings.xml \
546+ TMessagesProj/src/main/assets/multigram_styles.bin
547+git reset -q
548+```
549+
550+`git reset -q` un-stages the files but leaves them in place, so the checks of 7.4 can run from the start. Do not commit these copies: they are the versions of `OLD`, and 7.3 copies each commit's own files again at that commit's version. `git status --short` now shows 5 untracked entries: the 2 folders and the 3 single files.
551+
552+None of these paths exists in Forkgram 12.10.6. If a future Forkgram adds a file with one of these names, `git checkout` would overwrite it. This command must print nothing:
553+
554+```sh
555+git ls-tree -r --name-only origin/forkgram -- multigram TMessagesProj/src/main/java/org/telegram/messenger/multigram TMessagesProj/src/main/res/values/multigram_rebrand.xml TMessagesProj/src/main/res/values/multigram_strings.xml TMessagesProj/src/main/assets/multigram_styles.bin
556+```
557+
558+The 37 files:
559+
560+| # | Path | Added by | Section |
561+|---|---|---|---|
562+| 1 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/Rebrand.java` | d457ac74ae | 8.1 |
563+| 2 | `TMessagesProj/src/main/res/values/multigram_rebrand.xml` | d457ac74ae | 8.1 |
564+| 3 | `multigram/rebrand/.gitignore` | d457ac74ae | 8.1 |
565+| 4 | `multigram/rebrand/README.md` | d457ac74ae | 8.1 |
566+| 5 | `multigram/rebrand/generate_rebrand.py` | d457ac74ae | 8.1 |
567+| 6 | `multigram/rebrand/rebrand.gradle` | d457ac74ae | 8.1 |
568+| 7 | `multigram/rebrand/selftest.py` | d457ac74ae | 8.1 |
569+| 8 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/PaletteFix.java` | 5b53859682 | 8.2 |
570+| 9 | `multigram/palette-fix/README.md` | 5b53859682 | 8.2 |
571+| 10 | `multigram/palette-fix/overlay_A.json` | 5b53859682 | 8.2 |
572+| 11 | `multigram/tools/palette_fix_check.py` | 5b53859682 | 8.2 |
573+| 12 | `TMessagesProj/src/main/assets/multigram_styles.bin` | cddd62df98 | 8.3 |
574+| 13 | `multigram/tools/.gitignore` | cddd62df98 | 8.3 |
575+| 14 | `multigram/tools/README.md` | cddd62df98 | 8.3 |
576+| 15 | `multigram/tools/check.sh` | cddd62df98 | 8.3 |
577+| 16 | `multigram/tools/check_style_table.py` | cddd62df98 | 8.3 |
578+| 17 | `multigram/tools/make_style_table.py` | cddd62df98 | 8.3 |
579+| 18 | `multigram/tools/sim/README.md` | cddd62df98 | 8.3 |
580+| 19 | `multigram/tools/sim/canvas.py` | cddd62df98 | 8.3 |
581+| 20 | `multigram/tools/sim/detmath.py` | cddd62df98 | 8.3 |
582+| 21 | `multigram/tools/sim/detmath_tables.py` | cddd62df98 | 8.3 |
583+| 22 | `multigram/tools/sim/engine.py` | cddd62df98 | 8.3 |
584+| 23 | `multigram/tools/sim/palette-fix/overlay_A.json` | cddd62df98 | 8.3 |
585+| 24 | `multigram/tools/sim/palette-fix/overlay_B.json` | cddd62df98 | 8.3 |
586+| 25 | `multigram/tools/sim/readability.py` | cddd62df98 | 8.3 |
587+| 26 | `multigram/tools/sim/style_sim.py` | cddd62df98 | 8.3 |
588+| 27 | `multigram/tools/sim/tgsrc.py` | cddd62df98 | 8.3 |
589+| 28 | `multigram/tools/style_table.py` | cddd62df98 | 8.3 |
590+| 29 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyle.java` | 27d32d57b4 | 8.3 |
591+| 30 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyleUi.java` | 27d32d57b4 | 8.3 |
592+| 31 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/StyleTable.java` | 27d32d57b4 | 8.3 |
593+| 32 | `TMessagesProj/src/main/res/values/multigram_strings.xml` | 27d32d57b4 | 8.3 |
594+| 33 | `multigram/random-style/README.md` | 27d32d57b4 | 8.3 |
595+| 34 | `multigram/tools/random_style_check.py` | 27d32d57b4 | 8.3 |
596+| 35 | `TMessagesProj/src/main/java/org/telegram/messenger/multigram/StyleKnobs.java` | 3b941c01de | 8.4 |
597+| 36 | `multigram/style-knobs/README.md` | 3b941c01de | 8.4 |
598+| 37 | `multigram/tools/style_knobs_check.py` | 3b941c01de | 8.4 |
599+
600+### 7.3 Put the hooks back, commit by commit
601+
602+Work through the six commits in order. For each one: copy the commit's own files at that commit's version, put its hooks into the upstream files, and commit with the same message and author.
603+
604+**The quick way: a loop.** This loop does that for every commit. For each upstream file it tries the commit's change with `git apply -3`, which merges the way a rebase does. It stops at the first commit with a file it cannot patch cleanly, and names the files.
605+
606+```sh
607+START=$OLDSNAP
608+for C in $(git rev-list --reverse $START..$OLD); do
609+ echo "== $(git log -1 --format='%h %s' $C)"
610+ new=$(git diff-tree --no-commit-id -r --name-only --diff-filter=A $C)
611+ own=$(git diff-tree --no-commit-id -r --name-only --diff-filter=M $C -- multigram TMessagesProj/src/main/java/org/telegram/messenger/multigram)
612+ up=$(git diff-tree --no-commit-id -r --name-only --diff-filter=M $C -- . ':!multigram' ':!TMessagesProj/src/main/java/org/telegram/messenger/multigram')
613+ if [ -n "$new$own" ]; then git checkout $C -- $new $own; fi
614+ todo=
615+ for f in $up; do
616+ git diff $C^ $C -- $f | git apply -3 --index || todo="$todo $f"
617+ done
618+ if [ -n "$todo" ]; then
619+ echo "STOPPED at $C. Put the hooks of these files in by hand:$todo"
620+ break
621+ fi
622+ git commit --quiet -C $C
623+done
624+```
625+
626+`git commit -C $C` reuses the commit's message, author and date. That is what **use_next** matches on.
627+
628+This was tested on 2026-09-27 against the made-up Forkgram release of 5.2. The loop stopped at 27d32d57b4 on `MessagesController.java`. After the fix below, all six new commits had exactly the same content as the rebase in section 5 gave.
629+
630+**When the loop stops:**
631+
632+1. Look at each file it names.
633+ - If it contains conflict markers, fix it as in 5.2. Here the markers read `<<<<<<< ours` (new Forkgram) and `>>>>>>> theirs` (old Forkgram with the hook).
634+ - If git printed `Resolved '<file>' using previous resolution`, rerere (5.6) has already put in a fix you made earlier in this clone, for example during a section 5 attempt. The file has no markers but still counts as unmerged. Read the fix with `git diff HEAD -- <file>` before you `git add` it. rerere repeats values, not the `overlay_A.json` that went with them, so if the file is `ThemeColors.java` or an `.attheme` file, run `python3 multigram/tools/palette_fix_check.py --base origin/forkgram` before you commit. If it reports `expected <value>`, write the value it expects: that is the value of this commit's `overlay_A.json`. 7.5 re-derives the palette afterwards and adopts new values then.
635+ - If it is unchanged, git could not place the change at all. Put the hooks in by hand, with section 8. `git show $C -- <file>` shows what the commit changed.
636+2. Commit the stopped commit:
637+
638+ ```sh
639+ git add <the files>
640+ git commit -C $C
641+ ```
642+
643+3. Continue after it. Set `START` to the commit that stopped, and paste the loop again from its second line:
644+
645+ ```sh
646+ START=$C
647+ ```
648+
649+**By hand, without the loop.** First list the six commits you re-apply, with their hashes:
650+
651+```sh
652+git log --reverse --format='%h %s' $OLDSNAP..$OLD
653+```
654+
655+Use these hashes as `<commit>` below, not the hashes printed in this guide. Those are the commits of `multigram` 4780eb095b, and every sync rebases the stack, so after the next sync they are old versions (still reachable through the `multigram-before-<version>` tags, so git would not complain). Then, for each commit in that order (the table in 7.7 shows what each one holds):
656+
657+1. List what it touches: `git diff-tree --no-commit-id -r --name-status <commit>`.
658+2. Copy the MultiGram files it adds or changes, at its version:
659+
660+ ```sh
661+ git checkout <commit> -- $(git diff-tree --no-commit-id -r --name-only <commit> -- multigram TMessagesProj/src/main/java/org/telegram/messenger/multigram TMessagesProj/src/main/res/values/multigram_rebrand.xml TMessagesProj/src/main/res/values/multigram_strings.xml TMessagesProj/src/main/assets/multigram_styles.bin)
662+ ```
663+
664+ This copies only the paths the commit has. Naming a path by hand that the commit does not have yet (say `multigram_styles.bin` for d457ac74ae) makes `git checkout` copy nothing at all.
665+3. Put its hooks into the upstream files. Section 8 lists them in order. Mind the four files without a final newline (5.2).
666+4. `git add` the upstream files and run `git commit -C <commit>`.
667+
668+### 7.4 Use the checks as a to-do list
669+
670+The check scripts name every missing or misplaced hook. Run them at any point:
671+
672+```sh
673+python3 multigram/tools/palette_fix_check.py --base origin/forkgram
674+python3 multigram/tools/random_style_check.py --base origin/forkgram
675+python3 multigram/tools/style_knobs_check.py --base origin/forkgram
676+git grep -n -e 'multigram\.Rebrand\.' -e 'multigram/rebrand/rebrand.gradle' -- TMessagesProj TMessagesProj_App
677+cmp multigram/palette-fix/overlay_A.json multigram/tools/sim/palette-fix/overlay_A.json && echo "overlay copies identical"
678+```
679+
680+With all 37 files copied but no hooks in place, a test tree gave 59, 27 and 35 problems from the three checks, and the rebrand self-test failed one check (`TMessagesProj_App/build.gradle applies rebrand.gradle`). When everything is back:
681+
682+- each check prints a single line ending in `OK`;
683+- the `git grep` prints 5 lines, one for each of R1 to R5;
684+- the `cmp` prints `overlay copies identical` (see 5.4 if not).
685+
686+Each problem line says what is wrong, for example `the upload guard is not the first statement of saveThemeToServer`.
687+
688+### 7.5 Regenerate and check derived data
689+
690+Two things are derived from upstream sources and may need a refresh. Do this after 7.3, on your local `multigram-next`:
691+
692+1. **The palette values** (overlay A). Re-derive them from the new plain snapshot with the simulator (8.2.5, step 2). If they change, adopt them as "To adopt a new overlay" in 8.2.5 says. Its last step names the commit each file goes into.
693+2. **The style table** (`multigram_styles.bin`). Run `bash multigram/tools/check.sh`. If it asks for a new table, run `python3 multigram/tools/make_style_table.py` (8.3.5). The new file goes into "Add the style table and its generator".
694+
695+To fold a change into its commit, follow 6.3 from its step 2. Skip its step 1: your six commits exist only on your local branch until 7.6, and `git switch -C multigram-next origin/multigram-next` would throw them away.
696+
697+Nothing else is derived. The rebrand output is generated at build time and never committed. The knob values are computed on the device.
698+
699+### 7.6 Compile
700+
701+Push the branch and run the compile check on it (10.1), or compile locally (6.4):
702+
703+```sh
704+git push --force origin multigram-next
705+```
706+
707+### 7.7 Check the six commits
708+
709+The new commits must match the old ones in author, date and subject:
710+
711+```sh
712+diff <(git log --reverse --format='%an %ad %s' $OLDSNAP..$OLD) <(git log --reverse --format='%an %ad %s' origin/forkgram..multigram-next) && echo "same six commits"
713+```
714+
715+| # | Subject (keep it exactly) | Hooks | Section |
716+|---|---|---|---|
717+| 1 | Add the rebrand toolkit | R1–R5 | 8.1 |
718+| 2 | Fix palette contrast for generated accents | P1–P18 | 8.2 |
719+| 3 | Add the style table and its generator | none (new files only) | 8.3 |
720+| 4 | Give each install its own random style | S1–S15 | 8.3 |
721+| 5 | Randomise shapes and chat list layout per install | K1–K17 (K18–K23 come with the copied files) | 8.4 |
722+| 6 | Run the rebrand self-test in the CI checks | none (K24 comes with the copied `check.sh`) | 8.4 |
723+
724+### 7.8 Hand it to the sync
725+
726+Push (7.6), then run **Actions > Sync with Forkgram** with **use_next**.
727+
728+If **use_next** refuses with "multigram-next does not carry N of multigram's commit(s)", then someone pushed to `multigram` after you started, or a subject or author changed. Add the missing commit with `git cherry-pick <commit>`, or tick **allow_drop** if you left it out on purpose.
729+
730+## 8. The changes, one by one
731+
732+Each part below has the same six subsections: what and why, new files, hooks, data, how to regenerate and verify, and gotchas.
733+
734+### 8.1 Rebrand toolkit (d457ac74ae)
735+
736+#### 8.1.1 What and why
737+
738+The rebrand toolkit gives a build its own identity: application id, app name, launcher, splash and notification icons, signing key, and Android account type. It is meant for builds you give to other people. A stock build (no rebrand) behaves exactly like Forkgram.
739+
740+It works at build time:
741+
742+1. You run `python3 multigram/rebrand/generate_rebrand.py --seed <any text>`. The seed makes the identity repeatable.
743+2. The generator writes a resource overlay to `multigram/rebrand/generated/res` and settings to `multigram/rebrand/rebrand.properties`. Both are git-ignored.
744+3. It also edits three upstream files in place, because the account type is a Java literal that no overlay can replace: `ContactsController.java`, `res/xml/auth.xml` and `res/xml/sync_contacts.xml`. It keeps backups and adds a marker comment. These edits are never committed.
745+4. `rebrand.gradle` reads the properties and sets the application id, signing, version and resource folders.
746+5. `python3 multigram/rebrand/generate_rebrand.py --clean` puts everything back byte for byte.
747+
748+The committed upstream changes are small: four Java hooks of one code line each (R2 and R3 also put a comment line above theirs) and one Gradle line. Three Java hooks replace Forkgram's default chat list title "Fork Client" with the app's own name in a rebranded build. One hides the other launcher icons.
749+
750+#### 8.1.2 New files
751+
752+| Path | Purpose | Upstream names it relies on |
753+|---|---|---|
754+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/Rebrand.java` | The only runtime code. `isActive()` reads the flag `multigram_rebrand_active`. `defaultTitle(stock)` returns the app's own `AppName` in a rebranded build and `stock` otherwise. `filterLauncherIcons(list)` keeps only the default icon in a rebranded build. | `ApplicationLoader.applicationContext`, `FileLog.e(Throwable)`, `R.string.AppName`, `LauncherIconController.LauncherIcon.DEFAULT` |
755+| `TMessagesProj/src/main/res/values/multigram_rebrand.xml` | Declares `bool/multigram_rebrand_active` as false. The generated overlay sets it to true. | None. It must stay in the `TMessagesProj` library, because `Rebrand.java` compiles against that module's `R`. |
756+| `multigram/rebrand/.gitignore` | Keeps `generated/`, `rebrand.properties` (it holds key passwords) and `__pycache__/` out of git. | None |
757+| `multigram/rebrand/README.md` | User documentation: quick start, what changes, icons, account type, signing and updates. | Documentation only. It names Forkgram's icons, strings and ids; correct it if they change. |
758+| `multigram/rebrand/generate_rebrand.py` | The generator (Python standard library, plus `keytool` for the key). Makes the identity from the seed, writes the overlay and properties, patches the account type, and `--clean` undoes it all. | The resource folders `TMessagesProj/src/*/res` and `TMessagesProj_App/src/*/res`; the icon, splash and string names in 8.1.4; the account type in the three files above |
759+| `multigram/rebrand/rebrand.gradle` | Gradle glue, applied by hook R5. In stock mode it only fails the build if a rebrand marker was left behind. In rebrand mode it sets the id, signing, version and resources, and feeds the new app name to Telegram's string tasks. | The `android {}` block of `TMessagesProj_App/build.gradle`; the stock ids `org.forkclient.messenger` and `org.forkgram.messenger`; the `generate*TelegramStrings` tasks of class `org.telegram.tasks.TelegramStringsTask` with the properties `stringsXml` and `localizationFiles` |
760+| `multigram/rebrand/selftest.py` | End-to-end test without an Android SDK. It copies the tracked sources into a throwaway git repository, runs the generator with two seeds, checks every output, and checks that `--clean` restores the tree. | `git`; the exact R5 line; the three account-type files; exactly 5 account-type literals in `ContactsController.java` |
761+
762+#### 8.1.3 Hooks
763+
764+##### R1. Hide the other launcher icons in a rebranded build
765+
766+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Cells/AppIconsSelectorCell.java`
767+
768+**Where:** Class `AppIconsSelectorCell`, method `updateIconsVisibility()`. The new line goes right after the icon list is filled. In Forkgram 12.10.6 the text below starts at line 153.
769+
770+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
771+
772+```java
773+ availableIcons.addAll(Arrays.asList(LauncherIconController.LauncherIcon.values()));
774+```
775+
776+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
777+
778+```diff
779+ private void updateIconsVisibility() {
780+ availableIcons.clear();
781+ availableIcons.addAll(Arrays.asList(LauncherIconController.LauncherIcon.values()));
782++ org.telegram.messenger.multigram.Rebrand.filterLauncherIcons(availableIcons); // MultiGram: a rebranded build offers only its own icon
783+ if (MessagesController.getInstance(currentAccount).premiumFeaturesBlocked()) {
784+ for (int i = 0; i < availableIcons.size(); i++) {
785+ if (availableIcons.get(i).premium) {
786+```
787+
788+**Why:** Chat Settings has an app icon row that lists every launcher icon. The other icons are Forkgram's and Telegram's logos. In a rebranded build, `Rebrand.filterLauncherIcons` keeps only `DEFAULT`, the icon whose files the rebrand replaces. In a stock build it does nothing.
789+
790+**If the code moved:** Put the call right after the list that feeds the picker is filled from `LauncherIconController.LauncherIcon.values()`, and before anything reads the list (the premium filter, `notifyDataSetChanged`, the scroll loop). Find candidates with `git grep -n "LauncherIcon.values()" -- TMessagesProj/src/main/java`. Leave two other users alone: `LauncherIconController` (it switches every icon alias on and off, so it must see all of them) and `PremiumAppIconsPreviewView` (the Premium screen, left as it is on purpose). If Forkgram renames `DEFAULT`, or points another entry at the `icon_01_*` files, update `Rebrand.filterLauncherIcons`.
791+
792+
793+##### R2. Chat list title: default to the rebranded app name
794+
795+**File:** `TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java`
796+
797+**Where:** Class `DialogsActivity`, method `createView(Context)`, in the last `else` branch that sets up the main chat list title (no folder, no community). In Forkgram 12.10.6 the text below starts at line 3512.
798+
799+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
800+
801+```java
802+ actionBar.setTitle(MessagesController.getGlobalMainSettings().getString("forkCustomTitle", "Fork Client"));
803+```
804+
805+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
806+
807+```diff
808+ SpannableStringBuilder ssb = new SpannableStringBuilder(getString(R.string.AppName));
809+ ssb.setSpan(new ImageSpan(logoDrawable), 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
810+ actionBar.setTitle(ssb, statusDrawable);
811+- actionBar.setTitle(MessagesController.getGlobalMainSettings().getString("forkCustomTitle", "Fork Client"));
812++ // MultiGram: a rebranded build's default title is its own app name.
813++ actionBar.setTitle(MessagesController.getGlobalMainSettings().getString("forkCustomTitle", org.telegram.messenger.multigram.Rebrand.defaultTitle("Fork Client")));
814+ actionBar.setTitleLongClickListener(v -> {
815+ boolean mainTabsHidden = !UserConfig.getInstance(currentAccount).getMainTabsHiddenFork();
816+ UserConfig.getInstance(currentAccount).setMainTabsHiddenFork(mainTabsHidden);
817+```
818+
819+**Why:** Forkgram shows the setting `forkCustomTitle` as the chat list title, with the fixed default "Fork Client". With the hook, a rebranded build shows its own app name when the user has not set a title. A stock build still shows "Fork Client". The comment sits on its own line above the hook.
820+
821+**If the code moved:** Find every place that reads the title setting: `git grep -n '"forkCustomTitle"' -- TMessagesProj/src/main/java`. Wrap each literal default in `org.telegram.messenger.multigram.Rebrand.defaultTitle(<the stock text>)`. If Forkgram changes the default text, pass the new text, so stock builds stay identical to Forkgram. If Forkgram drops the custom title and shows `R.string.AppName` as text, no hook is needed: the rebrand already renames `AppName`.
822+
823+**On plain Telegram (DrKLO):** Forkgram only. Plain Telegram has no `forkCustomTitle`: it draws the `telegram_logo_2` picture over `R.string.AppName` (the line just above the hook), so renaming `AppName` does not change what is shown. See section 9.
824+
825+
826+##### R3. Fork Client Settings: show the same default title
827+
828+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java`
829+
830+**Where:** Class `ForkSettingsActivity`, method `fillSettings(ArrayList<UItem>)`: the "Custom title" row (`ID_CUSTOM_TITLE`). In Forkgram 12.10.6 the text below starts at line 547.
831+
832+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
833+
834+```java
835+ items.add(UItem.asSettingsCell(ID_CUSTOM_TITLE, LocaleController.getString(R.string.EditAdminRank), prefs().getString("forkCustomTitle", "Fork Client")));
836+```
837+
838+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
839+
840+```diff
841+ items.add(UItem.asButtonCheck(ID_HIDE_BOTTOM_BUTTON, LocaleController.getString(R.string.HideBottomButton), LocaleController.getString(R.string.HideBottomButtonInfo))
842+ .setChecked(pref("hideBottomButton", false)).setMultiline(true));
843+ }
844+- items.add(UItem.asSettingsCell(ID_CUSTOM_TITLE, LocaleController.getString(R.string.EditAdminRank), prefs().getString("forkCustomTitle", "Fork Client")));
845++ // MultiGram: a rebranded build's default title is its own app name.
846++ items.add(UItem.asSettingsCell(ID_CUSTOM_TITLE, LocaleController.getString(R.string.EditAdminRank), prefs().getString("forkCustomTitle", org.telegram.messenger.multigram.Rebrand.defaultTitle("Fork Client"))));
847+ items.add(UItem.asShadow(null));
848+
849+ items.add(UItem.asHeader(LocaleController.getString(R.string.AvatarShape)));
850+```
851+
852+**Why:** The row shows the current title as its value. Without a saved title, a rebranded build now shows its own app name here too, the same as the chat list. A stock build still shows "Fork Client".
853+
854+**If the code moved:** This is the settings row that shows the `forkCustomTitle` value. Wrap its literal default in `Rebrand.defaultTitle(<stock text>)`. It must agree with R2 and R4.
855+
856+**On plain Telegram (DrKLO):** Forkgram only: `ForkSettingsActivity.java` does not exist in plain Telegram.
857+
858+
859+##### R4. Custom title dialog: same default
860+
861+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java`
862+
863+**Where:** Class `ForkSettingsActivity`, method `showCustomTitleDialog(View)`, first line. In Forkgram 12.10.6 the text below starts at line 1154.
864+
865+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
866+
867+```java
868+ final String defaultValue = "Fork Client";
869+```
870+
871+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
872+
873+```diff
874+ }
875+
876+ private void showCustomTitleDialog(View view) {
877+- final String defaultValue = "Fork Client";
878++ final String defaultValue = org.telegram.messenger.multigram.Rebrand.defaultTitle("Fork Client"); // MultiGram: rebranded default
879+ org.telegram.messenger.forkgram.ForkDialogs.createFieldAlert(
880+ getContext(),
881+ LocaleController.getString(R.string.EditAdminRank),
882+```
883+
884+**Why:** The dialog fills its text field with the saved title or `defaultValue`, and an empty answer saves `defaultValue`. In a rebranded build both now use the app's own name, so clearing the field never brings "Fork Client" back.
885+
886+**If the code moved:** Find the dialog that saves the title: `git grep -n 'putString("forkCustomTitle"' -- TMessagesProj/src/main/java`. Whatever default it shows or saves must come from `Rebrand.defaultTitle(<stock text>)`.
887+
888+**On plain Telegram (DrKLO):** Forkgram only.
889+
890+
891+##### R5. Apply the rebrand Gradle script
892+
893+**File:** `TMessagesProj_App/build.gradle`
894+
895+**Where:** The very end of the file, after the closing brace of the `android { }` block. In Forkgram 12.10.6 the text below starts at line 330.
896+
897+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
898+
899+```groovy
900+ checkReleaseBuilds false
901+ }
902+}
903+```
904+
905+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
906+
907+```diff
908+ checkReleaseBuilds false
909+ }
910+ }
911++
912++// MultiGram: build-time rebrand, a no-op unless multigram/rebrand/rebrand.properties exists.
913++apply from: "${rootDir}/multigram/rebrand/rebrand.gradle"
914+```
915+
916+**Why:** This connects the toolkit to the app module. It must come after the module's own `android { }` block, so that the rebrand's application id, signing, version and resource folder replace the module's values. Without `multigram/rebrand/rebrand.properties` (a stock build) the script only checks that no rebrand patch was left behind in three source files.
917+
918+**If the code moved:** Keep one `apply from:` line at the very end of the app module's build script, after `android { }`. If upstream adds lines after `android { }`, put this line after them. If the app module is renamed, or `settings.gradle` gets another app module, hook that module; the paths inside `rebrand.gradle` start at the repository root. `selftest.py` looks for this exact text.
919+
920+**On plain Telegram (DrKLO):** Conflicts: plain Telegram's file ends with `apply plugin: 'com.google.gms.google-services'`. Keep both lines.
921+
922+
923+#### 8.1.4 Data
924+
925+Nothing derived is committed. The generator looks for these upstream names. If Forkgram renames one, the generator or the self-test says so.
926+
927+| What | Names in Forkgram 12.10.6 | Where the name is set |
928+|---|---|---|
929+| App name strings | `AppName`, `AppNameBeta`, `AppNameFdroid` in `values/strings.xml` (lines 177 to 179) and 10 translations; also `AppNameBeta` ("Fork Client Test") in `TMessagesProj_App/src/forkTest/res/values/strings.xml`, which the generator scans too | `APP_NAME_STRINGS` in `generate_rebrand.py` |
930+| Icons | 12 entries: mipmap `ic_launcher`, `ic_launcher_round`, `icon_01_launcher`, `icon_01_launcher_round`, `icon_01_launcher_adaptive`, `icon_01_launcher_sa`, `icon_01_foreground_sa`; drawable `icon_01_background_sa`, `ic_launcher_dr`, `splash_fork_320`, `tg_splash_320`, `notification`. That is 54 files today. | `ICON_TARGETS` in `generate_rebrand.py` |
931+| Account type in Java | 5 literals `"org.telegram.messenger"` in `TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java` | `JAVA_EXPECTED_LITERALS` in `generate_rebrand.py`, and `== 5` at `selftest.py` line 270 |
932+| Account type in XML | `android:accountType="${applicationId}"`, once each in `res/xml/auth.xml` and `res/xml/sync_contacts.xml` | `STOCK_XML_ACCOUNT_TYPE` in `generate_rebrand.py` |
933+| Stock application ids | `org.forkclient.messenger`, and `org.forkgram.messenger` for F-Droid builds | `stockIds` in `rebrand.gradle` |
934+| Default chat list title | `"Fork Client"` | The argument of `Rebrand.defaultTitle(...)` in hooks R2, R3 and R4 |
935+
936+#### 8.1.5 Regenerate and verify
937+
938+**The self-test.** Run it from the root of a checkout that is not rebranded. It takes about 30 seconds, and needs `git` (and `keytool` for the signing checks):
939+
940+```sh
941+python3 multigram/rebrand/selftest.py
942+```
943+
944+It passes when no line starts with `FAIL` and the last line is `PASSED: 0 failure(s)` (exit status 0). Besides the `ok` lines it prints the throwaway folder, an indented "Rebrand identity" block per seed and blank lines. An excerpt of a passing run on 4780eb095b (the temporary path differs each time):
945+
946+```text
947+throwaway tree: /tmp/rebrand-selftest-n2jxoni1/tree
948+ok TMessagesProj_App/build.gradle applies rebrand.gradle
949+ok seed A: generator succeeds
950+ok seed A: no warnings
951+ok seed A: overlay covers all 54 upstream icon files in the same folders
952+ok seed A: account type dev.quietshoal.app in Java (5x) and both XML files
953+ok --clean: git status clean (incl. ignored files), files byte-identical
954+ok --clean twice is harmless
955+
956+PASSED: 0 failure(s)
957+```
958+
959+- A `FAIL` line ends the run with `FAILED: N failure(s)` and exit status 1. The line names the check.
960+- Exit status 2 with `This tree is rebranded; run generate_rebrand.py --clean first.` means the tree you ran it from has `generated/` or `rebrand.properties`.
961+- Without `keytool` it prints `note: keytool not found, signing checks skipped` just before the final `PASSED` or `FAILED` line.
962+- The counts (54 icon files and so on) come from the tree. They may change when Forkgram adds or drops icon sizes.
963+
964+**The hooks.** No script checks the four Java hooks. This must print 5 lines:
965+
966+```sh
967+git grep -n -e 'multigram\.Rebrand\.' -e 'multigram/rebrand/rebrand.gradle' -- TMessagesProj TMessagesProj_App
968+```
969+
970+On 4780eb095b they are `AppIconsSelectorCell.java:154`, `DialogsActivity.java:3513`, `ForkSettingsActivity.java:548` and `:1155`, and `TMessagesProj_App/build.gradle:335`.
971+
972+Every use of the title setting must pass through `Rebrand.defaultTitle`. This lists them:
973+
974+```sh
975+git grep -n '"forkCustomTitle"' -- TMessagesProj/src/main/java
976+```
977+
978+**A real identity, in a scratch worktree.** Never do this in the checkout you commit from (see 8.1.6):
979+
980+```sh
981+git fetch origin
982+git worktree add --detach /tmp/mg-rebrand origin/multigram
983+cd /tmp/mg-rebrand
984+python3 multigram/rebrand/generate_rebrand.py --seed patch-doc-demo
985+```
986+
987+On 2026-09-27 it printed, among other lines:
988+
989+```text
990+ applicationId dev.cleargrove.im (Gradle adds the build type suffix, e.g. .beta)
991+ app name Willowbirch
992+ TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java: 5 literal(s)
993+```
994+
995+Success means: exit status 0, no `WARNING:` line, and exactly `5 literal(s)`. A line like `N literal(s) (expected 5: check the file)` is a problem, even without the word WARNING. `git status --porcelain` then shows the three patched files as modified, and `generated/` holds 74 files.
996+
997+To undo it and remove the worktree:
998+
999+```sh
1000+python3 multigram/rebrand/generate_rebrand.py --clean
1001+cd -
1002+git worktree remove /tmp/mg-rebrand
1003+```
1004+
1005+`--clean` prints `Rebrand removed: stock identity restored (3 source file(s) put back).` and `The generated signing key was deleted with it.` `cd -` goes back to the checkout you started from.
1006+
1007+**Gradle messages.** These come from reading `rebrand.gradle`, not from a build. A rebranded build prints lines that start with `[rebrand] :TMessagesProj_App:`. A stock build prints no `[rebrand]` line. The messages that point to a problem:
1008+
1009+| Message starts with | Kind | Meaning |
1010+|---|---|---|
1011+| `[rebrand] :TMessagesProj_App: no generate*TelegramStrings task found` | warning | Telegram's build code (`buildSrc`) changed, and the in-app strings keep the stock name. Update `rebrand.gradle`. |
1012+| `[rebrand] account type ... is not in place in ...` | error | A half-patched tree. Run the generator again, or `--clean`. |
1013+| `[rebrand] no active rebrand (multigram/rebrand/rebrand.properties), but these files still carry a rebrand's account type` | error | `rebrand.properties` was deleted without `--clean`. See 8.1.6. |
1014+| `[rebrand] signing keystore ... not found` | error | The key file named in `rebrand.properties` is missing. Put it back. |
1015+
1016+**On the APK and a device** (needs the Android build tools):
1017+
1018+- `aapt2 dump badging <file>.apk | grep -E '^package:|application-label'` shows the new package and label.
1019+- `apksigner verify --print-certs <file>.apk` shows the key fingerprint the generator printed.
1020+- On the device: the launcher, splash and notification icons are the generated ones; the chat list title is the app name; the app icon picker offers one icon; after login with contact sync, **Settings > Accounts** lists an account with the app's name.
1021+
1022+#### 8.1.6 Gotchas
1023+
1024+- **Never commit, rebase, pull or switch to a new Forkgram while a rebrand is active.** The three patched files show as modified. Run `--clean` first and generate again afterwards.
1025+- **`--clean` deletes `rebrand.properties` and the generated key.** Note the seed and options first. The identity comes back from the same seed, but the key does not. Keep a key for real users outside the repository (`--keystore PATH`), or phones cannot install your updates.
1026+- **Deleting `generated/` by hand** leaves the three files patched, and `--clean` refuses. Fix it with `git checkout -- TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java TMessagesProj/src/main/res/xml/auth.xml TMessagesProj/src/main/res/xml/sync_contacts.xml`, then run `--clean`. **Deleting only `rebrand.properties`** makes Gradle refuse (`[rebrand] no active rebrand ...`, see 8.1.5), because the three files still carry the rebrand's account type. `--clean` still puts them back.
1027+- **If the generator stops with an error** such as `unexpected <dir>/<name>.xml: update ICON_TARGETS`, a partial `generated/` stays behind. Run `--clean` before building. The self-test refuses to start while `generated/` exists.
1028+- **The account type count.** The generator replaces every `"org.telegram.messenger"` literal in `ContactsController.java`. If upstream adds that literal for another purpose, it would be replaced too. When the count is no longer 5, read each use, then change `JAVA_EXPECTED_LITERALS` and `selftest.py` line 270 together.
1029+- **Keep each Java hook one line, with the full name** `org.telegram.messenger.multigram.Rebrand`. No import is added, so upstream changes to the import list never conflict.
1030+- **`Rebrand.defaultTitle` reads `AppName` through Android, not through `LocaleController`**, because cloud language packs carry Telegram's name. This works because Telegram's string task keeps `AppName` as a real resource (`GENERATED_EXCLUSIONS` in `buildSrc/src/main/kotlin/org/telegram/tasks/TelegramStringsTask.kt`). If that list changes, the title may come out wrong.
1031+- **The stock title "Fork Client" appears in three hooks.** If Forkgram changes it, change all three.
1032+- **The `[rebrand] ... app name added to N TelegramStrings task(s)` message** counts tasks by name only. If `buildSrc` renames the task class or its properties, the message still appears but the in-app strings keep "Fork Client". Check `TelegramBuildAppPlugin.kt` and `TelegramStringsTask.kt` after an update.
1033+- **Forkgram's `auth.xml` and `sync_contacts.xml` say `${applicationId}`**; plain Telegram says `org.telegram.messenger`. If Forkgram switches, the generator warns and the self-test fails `seed A: no warnings`. Update `STOCK_XML_ACCOUNT_TYPE`.
1034+- **`filterLauncherIcons` keeps only `DEFAULT`.** Do not filter `LauncherIconController`'s own loops (they switch every icon on and off) or `PremiumAppIconsPreviewView` (the Premium screen).
1035+- **The self-test copies only files git tracks.** `git add` a new upstream file before you trust its result.
1036+- **Two build variants of one identity** (for example `.beta` and `.web`) share the account type, so only one of them can hold the account on a phone.
1037+
1038+### 8.2 Palette fix (5b53859682)
1039+
1040+#### 8.2.1 What and why
1041+
1042+Stock Telegram already fails the contrast minimum on many grey texts. MultiGram also gives every install a generated accent colour, which can be very light. This commit fixes both. It has four parts:
1043+
1044+1. **Static values.** 11 grey default colours in `ThemeColors.java` and 36 grey values in the five bundled themes (Blue, Dark Blue, Arctic Blue, Day, Night) move to the nearest lightness that passes 4.5 : 1. 13 neutral keys join the list of keys the accent tint skips, so an accent cannot pull them back. Monet themes are not touched.
1045+2. **The on-accent rule** (new class `PaletteFix`). For runtime accents only, marks drawn on accent fills become white or near-black `#050505`, whichever is more readable. These marks include the floating pencil, unread digits, button texts, checkmarks and the send glyph. Stock presets, server themes, other people's cloud themes and Monet look exactly as in Forkgram.
1046+3. **Outgoing bubble texts.** For runtime accents, the code that picks black or white texts for outgoing bubbles always runs.
1047+4. **Bundled theme refresh.** The app re-copies a bundled theme file after an app update, because two edited files kept their size.
1048+
1049+All values come from overlay A, which the simulator makes from plain Forkgram (8.2.5).
1050+
1051+#### 8.2.2 New files
1052+
1053+| Path | Purpose | Upstream names it relies on |
1054+|---|---|---|
1055+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/PaletteFix.java` | All the runtime logic; upstream files only call it. It holds the rule, the contrast maths, and data tables that must equal overlay A (8.2.4). A runtime accent is `accent.id > 100 && (accent.info == null \|\| accent.info.creator)`. | `Theme.getColor(int)`, `Theme.getColor(int, boolean[], boolean)`, `getFallbackKeys()`, `getDefaultColor(int)`, `getActiveTheme()`, `DEFALT_THEME_ACCENT_ID` (upstream's spelling); `ThemeAccent.id`, `info`, `parentTheme`, `accentColor`; `ThemeInfo.isMonet()` (Forkgram only), `isDark()`, `getKey()`, `firstAccentIsDefault`, `themeAccentsMap`, `chatAccentsByThemeId`; 45 `Theme.key_*` colour keys; `ApplicationLoader.applicationContext`; Android's `PackageInfo.lastUpdateTime` |
1056+| `multigram/palette-fix/overlay_A.json` | The specification: 23 defaults, 36 theme values, 13 exclusions, the on-accent pairs and the muted badge table. Colours are unsigned 32-bit ARGB. A second, identical copy is in `multigram/tools/sim/palette-fix/`. | The key names (with the `key_` prefix), the five `.attheme` file names, and the theme names "Blue", "Arctic Blue", "Day", "Dark Blue", "Night" |
1057+| `multigram/tools/palette_fix_check.py` | Checks the tree against overlay A: the values, the tables in `PaletteFix.java`, and 12 hook lines (each exactly once). With `--base REV` it also rejects any other change to `ThemeColors.java` and the 7 `.attheme` files. Prints `palette-fix: OK`. | The file paths of the hooked files; the formats `defaultColors[key_X] = ...;` and `name=<decimal>`; the exact hook texts including their comments |
1058+| `multigram/palette-fix/README.md` | Human documentation: what changes, what a runtime accent is, the hooks, and what is not hooked on purpose. | Documentation only |
1059+
1060+#### 8.2.3 Hooks
1061+
1062+##### P1. ThemeColors: 11 grey default colours
1063+
1064+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java`
1065+
1066+**Where:** Class `ThemeColors`, method `createDefaultColors()`. Eleven `defaultColors[...] = ...;` lines, each changed in place. The values are listed in table 8.2.4.
1067+
1068+**Find this** (the method that holds the lines; it occurs once in the file, at line 20 in Forkgram 12.10.6):
1069+
1070+```java
1071+ public static int[] createDefaultColors() {
1072+```
1073+
1074+**Then find these 11 lines** (each occurs once in the file):
1075+
1076+```java
1077+ defaultColors[key_windowBackgroundWhiteGrayText] = 0xff808384;
1078+ defaultColors[key_windowBackgroundWhiteGrayText2] = 0xff82868a;
1079+ defaultColors[key_windowBackgroundWhiteGrayText4] = 0xff808080;
1080+ defaultColors[key_actionBarDefaultSubtitle] = 0xff79817e; // key_windowBackgroundWhiteGrayText
1081+ defaultColors[key_chats_message] = 0xff75787A;
1082+ defaultColors[key_chats_date] = 0xff848688;
1083+ defaultColors[key_chat_inViews] = 0xffa1aab3;
1084+ defaultColors[key_chat_inTimeText] = 0xffa1aab3;
1085+ defaultColors[key_chat_inFileInfoText] = 0xffa1aab3;
1086+ defaultColors[key_chat_messagePanelHint] = 0xff858a84;
1087+ defaultColors[key_chat_topPanelMessage] = 0xff767e7c;
1088+```
1089+
1090+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1091+
1092+```diff
1093+- defaultColors[key_windowBackgroundWhiteGrayText] = 0xff808384;
1094+- defaultColors[key_windowBackgroundWhiteGrayText2] = 0xff82868a;
1095++ defaultColors[key_windowBackgroundWhiteGrayText] = 0xff757778; // MultiGram: contrast fix, was 0xff808384
1096++ defaultColors[key_windowBackgroundWhiteGrayText2] = 0xff73777b; // MultiGram: contrast fix, was 0xff82868a
1097+- defaultColors[key_windowBackgroundWhiteGrayText4] = 0xff808080;
1098++ defaultColors[key_windowBackgroundWhiteGrayText4] = 0xff6f6e6e; // MultiGram: contrast fix, was 0xff808080
1099+- defaultColors[key_actionBarDefaultSubtitle] = 0xff79817e; // key_windowBackgroundWhiteGrayText
1100++ defaultColors[key_actionBarDefaultSubtitle] = 0xff717876; // key_windowBackgroundWhiteGrayText; MultiGram: contrast fix, was 0xff79817e
1101+- defaultColors[key_chats_message] = 0xff75787A;
1102++ defaultColors[key_chats_message] = 0xff747779; // MultiGram: contrast fix, was 0xff75787A
1103+- defaultColors[key_chats_date] = 0xff848688;
1104++ defaultColors[key_chats_date] = 0xff757778; // MultiGram: contrast fix, was 0xff848688
1105+- defaultColors[key_chat_inViews] = 0xffa1aab3;
1106++ defaultColors[key_chat_inViews] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
1107+- defaultColors[key_chat_inTimeText] = 0xffa1aab3;
1108++ defaultColors[key_chat_inTimeText] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
1109+- defaultColors[key_chat_inFileInfoText] = 0xffa1aab3;
1110++ defaultColors[key_chat_inFileInfoText] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
1111+- defaultColors[key_chat_messagePanelHint] = 0xff858a84;
1112++ defaultColors[key_chat_messagePanelHint] = 0xff737873; // MultiGram: contrast fix, was 0xff858a84
1113+- defaultColors[key_chat_topPanelMessage] = 0xff767e7c;
1114++ defaultColors[key_chat_topPanelMessage] = 0xff717877; // MultiGram: contrast fix, was 0xff767e7c
1115+```
1116+
1117+**Why:** These are the stock default colours. The default Blue theme uses them directly, and every theme without its own value inherits them. The stock greys reach only 2.36 to 4.45 : 1 contrast against their backgrounds. The new values reach 4.50 to 4.54 : 1 with the same hue. Each changed line ends with `// MultiGram: contrast fix, was <old value>`.
1118+
1119+**If the code moved:** Match each line by its key, not by its value. If Forkgram changed the stock value of one of these keys, the replay conflicts on that line. Keep MultiGram's line to finish the replay (5.3), but not for good: re-derive the values from the new plain Forkgram with the simulator (8.2.5), use the value it gives (or drop the line if the key now passes), and update the `was 0x...` comment. If a key is renamed, see the `key_...` row of 6.2. The 12 "on-accent" keys in `overlay_A.json` (for example `key_chats_actionIcon`) must **not** be copied here: the app keeps them stock white on purpose.
1120+
1121+
1122+##### P2. assets/arctic.attheme: grey values of the "Arctic Blue" theme
1123+
1124+**File:** `TMessagesProj/src/main/assets/arctic.attheme`
1125+
1126+**Where:** Bundled theme "Arctic Blue". `key=value` lines anywhere in the file; order does not matter. Values are signed decimal colours (ARGB). The values are listed in table 8.2.4.
1127+
1128+**Then find these 4 lines** (each occurs once in the file):
1129+
1130+```properties
1131+actionBarDefaultSubtitle=-7630182
1132+chat_topPanelMessage=-8354167
1133+chat_inFileInfoText=-6182221
1134+windowBackgroundWhiteGrayText=-7629665
1135+```
1136+
1137+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1138+
1139+```diff
1140+-actionBarDefaultSubtitle=-7630182
1141++actionBarDefaultSubtitle=-9341057
1142+-chat_topPanelMessage=-8354167
1143++chat_topPanelMessage=-9275526
1144+-chat_inFileInfoText=-6182221
1145++chat_inFileInfoText=-9472128
1146+-windowBackgroundWhiteGrayText=-7629665
1147++windowBackgroundWhiteGrayText=-9472126
1148+```
1149+
1150+**Why:** These greys fail the 4.5 : 1 contrast minimum on the "Arctic Blue" theme in stock Forkgram. The file keeps its size (9041 bytes), so users who update need hook P12 to see the new values.
1151+
1152+**If the code moved:** Find each line by its key. If the stock value of a line changed upstream (a replay conflict), or the key disappeared, re-derive the values with the simulator (8.2.5). Write values in decimal: the checker does not read `#hex`.
1153+
1154+
1155+##### P3. assets/bluebubbles.attheme: grey values of the "Blue" theme
1156+
1157+**File:** `TMessagesProj/src/main/assets/bluebubbles.attheme`
1158+
1159+**Where:** Bundled theme "Blue". `key=value` lines anywhere in the file; order does not matter. Values are signed decimal colours (ARGB). The values are listed in table 8.2.4.
1160+
1161+**Then find these 4 lines** (each occurs once in the file):
1162+
1163+```properties
1164+actionBarDefaultSubtitle=-8156010
1165+windowBackgroundWhiteGrayText=-7565423
1166+windowBackgroundWhiteGrayText2=-7565423
1167+windowBackgroundWhiteGrayText4=-7565423
1168+```
1169+
1170+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1171+
1172+```diff
1173+-actionBarDefaultSubtitle=-8156010
1174++actionBarDefaultSubtitle=-9472127
1175+-windowBackgroundWhiteGrayText=-7565423
1176++windowBackgroundWhiteGrayText=-9144455
1177+-windowBackgroundWhiteGrayText2=-7565423
1178++windowBackgroundWhiteGrayText2=-9144455
1179+-windowBackgroundWhiteGrayText4=-7565423
1180++windowBackgroundWhiteGrayText4=-9671056
1181+```
1182+
1183+**Why:** These greys fail the 4.5 : 1 contrast minimum on the "Blue" theme in stock Forkgram. The file keeps its size (6073 bytes), so users who update need hook P12 to see the new values.
1184+
1185+**If the code moved:** Find each line by its key. Match by key: the old value `-7565423` is on 9 lines in this file. Only the three P3 keys that have it change; the others (for example `windowBackgroundWhiteGrayText3`) stay unchanged. If the stock value of a line changed upstream (a replay conflict), or the key disappeared, re-derive the values with the simulator (8.2.5). Write values in decimal: the checker does not read `#hex`.
1186+
1187+
1188+##### P4. assets/darkblue.attheme: grey values of the "Dark Blue" theme
1189+
1190+**File:** `TMessagesProj/src/main/assets/darkblue.attheme`
1191+
1192+**Where:** Bundled theme "Dark Blue". `key=value` lines anywhere in the file; order does not matter. Values are signed decimal colours (ARGB). The values are listed in table 8.2.4.
1193+
1194+**Then find these 12 lines** (each occurs once in the file):
1195+
1196+```properties
1197+chat_messagePanelHint=1859907583
1198+actionBarDefaultSubtitle=2111566591
1199+chat_topPanelMessage=1859974399
1200+chat_inFileInfoText=-8812137
1201+windowBackgroundWhiteGrayText=-8549479
1202+chats_message=-8549479
1203+chats_date=-9207925
1204+chat_inViews=-8812137
1205+windowBackgroundWhiteGrayText2=1878130175
1206+windowBackgroundWhiteGrayText4=-931296359
1207+chat_inTimeText=-645885536
1208+chats_attachMessage=-8548712
1209+```
1210+
1211+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1212+
1213+```diff
1214+-chat_messagePanelHint=1859907583
1215++chat_messagePanelHint=-1763971073
1216+-actionBarDefaultSubtitle=2111566591
1217++actionBarDefaultSubtitle=-1831079169
1218+-chat_topPanelMessage=1859974399
1219++chat_topPanelMessage=-1814235905
1220+-chat_inFileInfoText=-8812137
1221++chat_inFileInfoText=-7035469
1222+-windowBackgroundWhiteGrayText=-8549479
1223++windowBackgroundWhiteGrayText=-7562327
1224+-chats_message=-8549479
1225++chats_message=-7562327
1226+-chats_date=-9207925
1227++chats_date=-7431514
1228+-chat_inViews=-8812137
1229++chat_inViews=-7035469
1230+-windowBackgroundWhiteGrayText2=1878130175
1231++windowBackgroundWhiteGrayText2=-2081292801
1232+-windowBackgroundWhiteGrayText4=-931296359
1233+-chat_inTimeText=-645885536
1234++windowBackgroundWhiteGrayText4=-209876071
1235++chat_inTimeText=-7035724
1236+-chats_attachMessage=-8548712
1237++chats_attachMessage=-6710887
1238+```
1239+
1240+**Why:** These greys fail the 4.5 : 1 contrast minimum on the "Dark Blue" theme in stock Forkgram.
1241+
1242+**If the code moved:** Find each line by its key. Keep `chats_attachMessage` a pure grey (red = green = blue) if you re-derive it: the key is accent blue on the light themes, so it cannot join the exclusion list, and a pure grey is never re-tinted. If the stock value of a line changed upstream (a replay conflict), or the key disappeared, re-derive the values with the simulator (8.2.5). Write values in decimal: the checker does not read `#hex`.
1243+
1244+
1245+##### P5. assets/day.attheme: grey values of the "Day" theme
1246+
1247+**File:** `TMessagesProj/src/main/assets/day.attheme`
1248+
1249+**Where:** Bundled theme "Day". `key=value` lines anywhere in the file; order does not matter. Values are signed decimal colours (ARGB). The values are listed in table 8.2.4.
1250+
1251+**Then find these 6 lines** (each occurs once in the file):
1252+
1253+```properties
1254+actionBarDefaultSubtitle=-8814210
1255+chat_topPanelMessage=-9011588
1256+chat_inFileInfoText=-7565679
1257+windowBackgroundWhiteGrayText=-8354940
1258+chat_inViews=-274882397
1259+chat_inTimeText=-7105127
1260+```
1261+
1262+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1263+
1264+```diff
1265+-actionBarDefaultSubtitle=-8814210
1266++actionBarDefaultSubtitle=-9340810
1267+-chat_topPanelMessage=-9011588
1268++chat_topPanelMessage=-9340809
1269+-chat_inFileInfoText=-7565679
1270++chat_inFileInfoText=-9671312
1271+-windowBackgroundWhiteGrayText=-8354940
1272++windowBackgroundWhiteGrayText=-9078920
1273+-chat_inViews=-274882397
1274++chat_inViews=-9736591
1275+-chat_inTimeText=-7105127
1276++chat_inTimeText=-9671311
1277+```
1278+
1279+**Why:** These greys fail the 4.5 : 1 contrast minimum on the "Day" theme in stock Forkgram.
1280+
1281+**If the code moved:** Find each line by its key. If the stock value of a line changed upstream (a replay conflict), or the key disappeared, re-derive the values with the simulator (8.2.5). Write values in decimal: the checker does not read `#hex`.
1282+
1283+
1284+##### P6. assets/night.attheme: grey values of the "Night" theme
1285+
1286+**File:** `TMessagesProj/src/main/assets/night.attheme`
1287+
1288+**Where:** Bundled theme "Night". `key=value` lines anywhere in the file; order does not matter. Values are signed decimal colours (ARGB). The values are listed in table 8.2.4.
1289+
1290+**Then find these 10 lines** (each occurs once in the file):
1291+
1292+```properties
1293+chat_messagePanelHint=1694498815
1294+actionBarDefaultSubtitle=1945301746
1295+chat_topPanelMessage=1694498815
1296+chat_inFileInfoText=-9013641
1297+windowBackgroundWhiteGrayText=1862270975
1298+chats_date=-8882056
1299+chat_inViews=-8881024
1300+windowBackgroundWhiteGrayText2=1862139391
1301+windowBackgroundWhiteGrayText4=-9803158
1302+chat_inTimeText=-8552575
1303+```
1304+
1305+**Change them to** (`-` is the Forkgram line, `+` the MultiGram line):
1306+
1307+```diff
1308+-chat_messagePanelHint=1694498815
1309++chat_messagePanelHint=1979711487
1310+-actionBarDefaultSubtitle=1945301746
1311++actionBarDefaultSubtitle=-2131561742
1312+-chat_topPanelMessage=1694498815
1313++chat_topPanelMessage=1962934271
1314+-chat_inFileInfoText=-9013641
1315++chat_inFileInfoText=-7763573
1316+-windowBackgroundWhiteGrayText=1862270975
1317++windowBackgroundWhiteGrayText=1962934271
1318+-chats_date=-8882056
1319++chats_date=-8289920
1320+-chat_inViews=-8881024
1321++chat_inViews=-8025458
1322+-windowBackgroundWhiteGrayText2=1862139391
1323++windowBackgroundWhiteGrayText2=1962802687
1324+-windowBackgroundWhiteGrayText4=-9803158
1325+-chat_inTimeText=-8552575
1326++windowBackgroundWhiteGrayText4=-9079692
1327++chat_inTimeText=-7894388
1328+```
1329+
1330+**Why:** These greys fail the 4.5 : 1 contrast minimum on the "Night" theme in stock Forkgram.
1331+
1332+**If the code moved:** Find each line by its key. Match by key: the old value `1694498815` is on 8 lines in this file. Only `chat_messagePanelHint` and `chat_topPanelMessage` change; the other six (for example `chat_messagePanelIcons`) stay unchanged. If the stock value of a line changed upstream (a replay conflict), or the key disappeared, re-derive the values with the simulator (8.2.5). Write values in decimal: the checker does not read `#hex`.
1333+
1334+
1335+##### P7. Per-chat themes: apply the on-accent rule
1336+
1337+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/EmojiThemes.java`
1338+
1339+**Where:** Class `EmojiThemes`, method `createColors(int currentAccount, int index)`, inside `if (accent != null) {`, after the gift-theme block and before `} else {`. In Forkgram 12.10.6 the text below starts at line 429.
1340+
1341+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1342+
1343+```java
1344+ public SparseIntArray createColors(int currentAccount, int index) {
1345+```
1346+
1347+The context lines of the change below occur twice in the file. Use the copy after this line (it begins at line 472 in Forkgram 12.10.6, inside `createColors`), not the one in `getPreviewColors` (line 397).
1348+
1349+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1350+
1351+```diff
1352+ if (isGiftTheme() && accent.parentTheme != null && accent.parentTheme.isLight()) {
1353+ accent.resetAccentColorsForMyMessagesGiftThemeLight(currentColors);
1354+ }
1355++ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
1356+ } else {
1357+ currentColors = currentColorsNoAccent;
1358+ }
1359+```
1360+
1361+**Why:** This builds the colour map of a per-chat theme (emoji or gift theme). With the hook, marks drawn on accent fills in a themed chat stay readable while the user's own accent is a runtime accent. With a stock accent the chat looks as in Forkgram.
1362+
1363+**If the code moved:** Anchor on the method `createColors`, not on the block: the same `if (accent != null) { ... resetAccentColorsForMyMessagesGiftThemeLight ... }` block also exists in `getPreviewColors`, which is deliberately **not** hooked (it only draws a small preview). The call goes after `fillAccentColors` and the gift-theme fix, and before the map is completed with fallbacks and returned.
1364+
1365+
1366+##### P8. Out-bubble text: always recompute for runtime accents
1367+
1368+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
1369+
1370+**Where:** Class `Theme.ThemeAccent`, method `fillAccentColors(SparseIntArray, SparseIntArray)`: the condition of the block that picks black or white texts for outgoing bubbles. In Forkgram 12.10.6 the text below starts at line 679.
1371+
1372+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1373+
1374+```java
1375+ if (!isMyMessagesGradientColorsNear) {
1376+```
1377+
1378+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1379+
1380+```diff
1381+ }
1382+ }
1383+ }
1384+- if (!isMyMessagesGradientColorsNear) {
1385++ if (!isMyMessagesGradientColorsNear || org.telegram.messenger.multigram.PaletteFix.isRuntimeAccent(this)) { // MultiGram: runtime accents (generated/custom) always get readable black/white out-bubble texts
1386+ if (myMessagesGradientAccentColor1 != 0) {
1387+ int textColor;
1388+ int subTextColor;
1389+```
1390+
1391+**Why:** Upstream picks readable black or white outgoing-bubble texts only when the custom bubble colour is not close to the theme's own bubble. A generated light bubble can be close to Blue's bubble and then keeps stock texts that fail contrast. For runtime accents the block now always runs.
1392+
1393+**If the code moved:** Wherever `fillAccentColors` decides whether to recompute out-bubble text colours, make runtime accents always take the recompute path. Keep the line exactly as it is: `palette_fix_check.py` matches it, and the simulator (`tgsrc.py`, `style_table.py`) looks for this exact guard to know the palette fix is in the tree.
1394+
1395+
1396+##### P9. Exported accent themes carry the rule
1397+
1398+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
1399+
1400+**Where:** Class `Theme.ThemeAccent`, method `saveToFile()`, right after `fillAccentColors(...)`. In Forkgram 12.10.6 the text below starts at line 1041.
1401+
1402+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1403+
1404+```java
1405+ SparseIntArray currentColorsNoAccent = getThemeFileValues(null, parentTheme.assetName, null);
1406+ SparseIntArray currentColors = currentColorsNoAccent.clone();
1407+ fillAccentColors(currentColorsNoAccent, currentColors);
1408+```
1409+
1410+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1411+
1412+```diff
1413+ SparseIntArray currentColorsNoAccent = getThemeFileValues(null, parentTheme.assetName, null);
1414+ SparseIntArray currentColors = currentColorsNoAccent.clone();
1415+ fillAccentColors(currentColorsNoAccent, currentColors);
1416++ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(parentTheme, this, currentColors); // MultiGram: on-accent contrast rule
1417+
1418+ String wallpaperLink = null;
1419+
1420+```
1421+
1422+**Why:** An accent saved as an `.attheme` file carries the same readable marks the user sees. Stock accents export unchanged.
1423+
1424+**If the code moved:** Wherever an accent's colour map is built for export, apply the rule after `fillAccentColors` and before the map is written.
1425+
1426+
1427+##### P10. Keep 13 neutral keys out of the accent tint
1428+
1429+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
1430+
1431+**Where:** Class `Theme`, static initializer: after the last `themeAccentExclusionKeys.add(...)` and before `themes = new ArrayList<>();`. In Forkgram 12.10.6 the text below starts at line 3886.
1432+
1433+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1434+
1435+```java
1436+ themeAccentExclusionKeys.add(key_stories_circle_closeFriends2);
1437+```
1438+
1439+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1440+
1441+```diff
1442+ themeAccentExclusionKeys.add(key_stories_circle_dialog2);
1443+ themeAccentExclusionKeys.add(key_stories_circle_closeFriends1);
1444+ themeAccentExclusionKeys.add(key_stories_circle_closeFriends2);
1445++ org.telegram.messenger.multigram.PaletteFix.addAccentExclusions(themeAccentExclusionKeys); // MultiGram: neutral greys stay readable under any accent
1446+
1447+
1448+ themes = new ArrayList<>();
1449+```
1450+
1451+**Why:** `fillAccentColors` shifts the hue of most colours towards the accent. Keys in `themeAccentExclusionKeys` are skipped. The hook adds 13 neutral keys (the 11 greys, `windowBackgroundGray` and `chat_messagePanelIcons`), so an accent cannot tint them back below the contrast minimum.
1452+
1453+**If the code moved:** Call it once, after upstream has filled `themeAccentExclusionKeys` and before any theme is loaded (still in the static initializer, after the `key_*` fields exist). If upstream turns the set into another type, change the parameter type of `PaletteFix.addAccentExclusions`.
1454+
1455+
1456+##### P11. The global hook: apply the rule to the app's colours
1457+
1458+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
1459+
1460+**Where:** Class `Theme`, method `refreshThemeColors(boolean bg, boolean messages)`: after the calculated table and article colours, before `reloadWallpaper` and `applyCommonTheme`. In Forkgram 12.10.6 the text below starts at line 6107.
1461+
1462+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1463+
1464+```java
1465+ applyCalculatedArticleCodeColors(currentColorsNoAccent, currentColors, currentTheme.isDark());
1466+```
1467+
1468+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1469+
1470+```diff
1471+ }
1472+ applyCalculatedTableColors(currentColorsNoAccent, currentColors, currentTheme.isDark());
1473+ applyCalculatedArticleCodeColors(currentColorsNoAccent, currentColors, currentTheme.isDark());
1474++ org.telegram.messenger.multigram.PaletteFix.applyToCurrentColors(currentTheme, accent, currentColors); // MultiGram: on-accent contrast rule
1475+ if (!messages) {
1476+ boolean async = !(LaunchActivity.getLastFragment() instanceof ChatActivity);
1477+ reloadWallpaper(async);
1478+```
1479+
1480+**Why:** This is the main hook. It decides whether the current theme and accent get the rule, and remembers the answer for hooks P14 to P18. When the rule is on, it sets the marks drawn on accent fills (the floating pencil, unread digits, checkmarks, button texts) to white or near-black `#050505`, whichever reads better, and gives the muted unread badge a matching fill.
1481+
1482+**If the code moved:** It must run every time the global colour map is rebuilt: after the accent tint and all calculated colours, and before `applyCommonTheme`, `applyDialogsTheme` and `applyChatTheme` copy colours into paints. The local variable `accent` must be in scope; pass `null` if upstream removes it.
1483+
1484+
1485+##### P12. Re-copy bundled themes after an app update
1486+
1487+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
1488+
1489+**Where:** Class `Theme`, method `getAssetFile(String assetName)`: the condition that decides whether to copy the asset again. In Forkgram 12.10.6 the text below starts at line 7251.
1490+
1491+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1492+
1493+```java
1494+ if (!file.exists() || size != 0 && file.length() != size) {
1495+```
1496+
1497+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1498+
1499+```diff
1500+ size = 0;
1501+ FileLog.e(e);
1502+ }
1503+- if (!file.exists() || size != 0 && file.length() != size) {
1504++ if (!file.exists() || size != 0 && file.length() != size || org.telegram.messenger.multigram.PaletteFix.isStaleAssetCopy(file)) { // MultiGram: also re-copy after an app update (same-size .attheme edits)
1505+ try (InputStream in = ApplicationLoader.applicationContext.getAssets().open(assetName)) {
1506+ AndroidUtilities.copyFile(in, file);
1507+ } catch (Exception e) {
1508+```
1509+
1510+**Why:** The app reads bundled themes from a copy in its files folder, and upstream refreshes that copy only when the asset's size changes. `arctic.attheme` and `bluebubbles.attheme` keep their size, so without this hook users who update would keep the old greys. `isStaleAssetCopy` returns true at most once per file per app start, when the copy is older than the app update.
1511+
1512+**If the code moved:** Wherever a bundled asset is copied to storage and reused, also refresh it after an app update. If upstream starts reading assets directly (no copy), drop this hook.
1513+
1514+
1515+##### P13. Channel colour screen
1516+
1517+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ChannelColorActivity.java`
1518+
1519+**Where:** Class `ChannelColorActivity`, method `updateThemeColors()`, inside `if (accent != null) {`, right after `fillAccentColors`. In Forkgram 12.10.6 the text below starts at line 2582.
1520+
1521+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1522+
1523+```java
1524+ accent.fillAccentColors(themeColors, currentColors);
1525+```
1526+
1527+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1528+
1529+```diff
1530+ Theme.ThemeAccent accent = themeInfo.getAccent(false);
1531+ if (accent != null) {
1532+ accent.fillAccentColors(themeColors, currentColors);
1533++ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
1534+ }
1535+ }
1536+ dividerPaint.setColor(Theme.getColor(Theme.key_divider, resourceProvider));
1537+```
1538+
1539+**Why:** This screen builds its own colour map for its previews. With the hook, the previews match what the app shows.
1540+
1541+**If the code moved:** Put the call right after `fillAccentColors` wherever a screen builds its own colour map to draw UI. After an update run `git grep -n 'fillAccentColors(' -- TMessagesProj/src/main/java ':!TMessagesProj/src/main/java/org/telegram/messenger/multigram'`. Today it prints 11 lines: the method itself and 10 calls. (Without the last pathspec it prints a 12th line on a MultiGram tree: a comment in `PaletteFix.java`.) Six calls are hooked (P7, P9, P11, P13, P16, P17). Four are left alone on purpose: `EmojiThemes.getPreviewColors` (twice), `ThemePreviewDrawable` and `Stories/recorder/PreviewView`. Decide for any new call.
1542+
1543+
1544+##### P14. Send button: loading arc colour
1545+
1546+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java`
1547+
1548+**Where:** Inner class `ChatActivityEnterView.SendButton`, method `onDraw(Canvas)`, inside `if (loadingShown > 0) {`. In Forkgram 12.10.6 the text below starts at line 15512.
1549+
1550+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1551+
1552+```java
1553+ loadingPaint.setColor(0xFFFFFFFF);
1554+```
1555+
1556+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1557+
1558+```diff
1559+ }
1560+ canvas.clipPath(path);
1561+ if (loadingShown > 0) {
1562+- loadingPaint.setColor(0xFFFFFFFF);
1563++ loadingPaint.setColor(multigramGlyphColor != 0 ? multigramGlyphColor : 0xFFFFFFFF); // MultiGram: was 0xFFFFFFFF
1564+ loadingPaint.setAlpha((int) (0xFF * loadingShown));
1565+ final float R = dp(8.66f);
1566+ AndroidUtilities.rectTmp.set(right - w / 2.0f - R, cy - R, right - w / 2.0f + R, cy + R);
1567+```
1568+
1569+**Why:** The progress arc drawn on the send button's fill uses the same readable colour as the icon. `multigramGlyphColor` is the field added in P15; 0 means "keep stock white".
1570+
1571+**If the code moved:** Any mark that `SendButton` draws in fixed white on its accent fill should use `multigramGlyphColor` when it is not 0. `palette_fix_check.py` does not check this line; check it by hand.
1572+
1573+
1574+##### P15. Send button: glyph colour on the fill
1575+
1576+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java`
1577+
1578+**Where:** Inner class `ChatActivityEnterView.SendButton`: a new field after `private int drawableColor;`, and method `updateColors()`. In Forkgram 12.10.6 the text below starts at line 15674.
1579+
1580+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1581+
1582+```java
1583+ private int drawableColor;
1584+
1585+ public void updateColors() {
1586+```
1587+
1588+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1589+
1590+```diff
1591+ }
1592+
1593+ private int drawableColor;
1594++ private int multigramGlyphColor; // MultiGram: glyph colour on the fill for generated styles, 0 = stock
1595+
1596+ public void updateColors() {
1597+- int color = isNewDesignSendButton ? Color.WHITE : Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider);
1598+- if (color != drawableColor) {
1599++ int glyph = org.telegram.messenger.multigram.PaletteFix.getGlyphColorOnFill(isNewDesignSendButton ? Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider) : shouldDrawBackground() ? getFillColor() : 0); // MultiGram: readable glyph on generated accent fills
1600++ int color = isNewDesignSendButton ? (glyph != 0 ? glyph : Color.WHITE) : Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider); // MultiGram: was Color.WHITE for the new design
1601++ if (color != drawableColor || glyph != multigramGlyphColor) { // MultiGram: was color != drawableColor
1602+ drawableColor = color;
1603++ multigramGlyphColor = glyph; // MultiGram
1604+ drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
1605+ int c = Theme.getColor(Theme.key_glass_defaultIcon, resourcesProvider);
1606+ inactiveDrawable.setColorFilter(new PorterDuffColorFilter(Color.argb(0xb4, Color.red(c), Color.green(c), Color.blue(c)), PorterDuff.Mode.SRC_IN));
1607+- drawableInverse.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoicePressed, resourcesProvider), PorterDuff.Mode.SRC_IN));
1608++ drawableInverse.setColorFilter(new PorterDuffColorFilter(glyph != 0 ? glyph : Theme.getColor(Theme.key_chat_messagePanelVoicePressed, resourcesProvider), PorterDuff.Mode.SRC_IN)); // MultiGram: was the voicePressed colour only
1609++ count.setTextColor(glyph != 0 && !isNewDesignSendButton ? glyph : 0xFFFFFFFF); // MultiGram: counter digits on the fill (old design)
1610++ priceText.setTextColor(glyph != 0 ? glyph : 0xFFFFFFFF); // MultiGram: star price on the fill
1611+ }
1612+ if (isNewDesignSendButton) {
1613+ backgroundPaint.setColor(Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider));
1614+```
1615+
1616+**Why:** Upstream draws the send button's glyph in fixed white on the accent fill. While the rule is on, `getGlyphColorOnFill` returns white or `#050505` for that fill (0 otherwise, which keeps stock). The glyph, the inverse glyph, the old-design counter digits and the star price follow it. The extra `|| glyph != multigramGlyphColor` makes a style change repaint even when `color` did not change.
1617+
1618+**If the code moved:** In the send button's colour update, compute the glyph colour from the colour of the fill it sits on, and use it for every mark drawn on that fill. It relies on these `SendButton` members: `isNewDesignSendButton`, `shouldDrawBackground()`, `getFillColor()`, `drawable`, `drawableInverse`, `count`, `priceText`, `resourcesProvider`. The checker looks only at the `int glyph = ...` and `drawableInverse...` lines; check the rest by hand.
1619+
1620+
1621+##### P16. Message-in-story preview
1622+
1623+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/MessageEntityView.java`
1624+
1625+**Where:** Class `MessageEntityView`, method `setupTheme(StoryEntry)`, inside `if (accent != null) {`, right after `fillAccentColors`. In Forkgram 12.10.6 the text below starts at line 1399.
1626+
1627+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1628+
1629+```java
1630+ accent.fillAccentColors(themeColors, currentColors);
1631+```
1632+
1633+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1634+
1635+```diff
1636+ Theme.ThemeAccent accent = themeInfo.getAccent(false);
1637+ if (accent != null) {
1638+ accent.fillAccentColors(themeColors, currentColors);
1639++ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
1640+ }
1641+ }
1642+
1643+```
1644+
1645+**Why:** The message shown inside a story builds its own colour map. With the hook, its marks follow the rule exactly when the app's colours do.
1646+
1647+**If the code moved:** As for P13.
1648+
1649+
1650+##### P17. Profile colour screen
1651+
1652+**File:** `TMessagesProj/src/main/java/org/telegram/ui/PeerColorActivity.java`
1653+
1654+**Where:** Class `PeerColorActivity`, method `updateThemeColors()`, inside `if (accent != null) {`, right after `fillAccentColors`. In Forkgram 12.10.6 the text below starts at line 1496.
1655+
1656+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1657+
1658+```java
1659+ accent.fillAccentColors(themeColors, currentColors);
1660+```
1661+
1662+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1663+
1664+```diff
1665+ Theme.ThemeAccent accent = themeInfo.getAccent(false);
1666+ if (accent != null) {
1667+ accent.fillAccentColors(themeColors, currentColors);
1668++ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
1669+ }
1670+
1671+ if (namePage != null && namePage.messagesCellPreview != null) {
1672+```
1673+
1674+**Why:** The name and profile colour screen builds its own colour map for its message previews. With the hook, they match the app.
1675+
1676+**If the code moved:** As for P13.
1677+
1678+
1679+##### P18. Stories and calls: marks on overridden fills
1680+
1681+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Stories/DarkThemeResourceProvider.java`
1682+
1683+**Where:** Class `DarkThemeResourceProvider`, method `getColor(int key)`: the last `return`, for keys the provider does not override. In Forkgram 12.10.6 the text below starts at line 205.
1684+
1685+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
1686+
1687+```java
1688+ return Theme.getColor(key);
1689+```
1690+
1691+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1692+
1693+```diff
1694+ if (!debugUnknownKeys.contains(key)) {
1695+ debugUnknownKeys.add(key);
1696+ }
1697+- return Theme.getColor(key);
1698++ return org.telegram.messenger.multigram.PaletteFix.getOverrideFallbackColor(sparseIntArray, key); // MultiGram: was Theme.getColor(key); marks on fills overridden here stay readable
1699+ }
1700+
1701+ Drawable msgOutMedia;
1702+```
1703+
1704+**Why:** This provider (used by stories and group calls) replaces some fills, such as `dialogRoundCheckBox`, but not the marks drawn on them. While the rule is on, the app's mark colour was chosen for the app's fill. For a mark whose fill this provider replaces, PaletteFix picks white or `#050505` again for the replaced fill. Every other key still returns `Theme.getColor(key)`.
1705+
1706+**If the code moved:** In resource providers that replace some colours and fall back to the global theme, send the fallback through `getOverrideFallbackColor`. `sparseIntArray` is the provider's map today; rename it in the call if upstream renames the field.
1707+
1708+
1709+#### 8.2.4 Data
1710+
1711+The values below are taken from the repository. The contrast columns come from the simulator's report (`overlay_A_rows.json`), rebuilt on Forkgram 12.10.6. Each is measured against the key's background in that theme.
1712+
1713+**`ThemeColors.java` defaults** (hook P1; used by the Blue theme and inherited by every theme without its own value):
1714+
1715+| Key | Forkgram 12.10.6 | MultiGram | Contrast before → after |
1716+|---|---|---|---|
1717+| `windowBackgroundWhiteGrayText` | `0xff808384` | `0xff757778` | 3.82 → 4.50 |
1718+| `windowBackgroundWhiteGrayText2` | `0xff82868a` | `0xff73777b` | 3.67 → 4.51 |
1719+| `windowBackgroundWhiteGrayText4` | `0xff808080` | `0xff6f6e6e` | 3.50 → 4.51 |
1720+| `actionBarDefaultSubtitle` | `0xff79817e` | `0xff717876` | 4.00 → 4.52 |
1721+| `chats_message` | `0xff75787A` | `0xff747779` | 4.45 → 4.51 |
1722+| `chats_date` | `0xff848688` | `0xff757778` | 3.65 → 4.50 |
1723+| `chat_inViews` | `0xffa1aab3` | `0xff6f7780` | 2.36 → 4.54 |
1724+| `chat_inTimeText` | `0xffa1aab3` | `0xff6f7780` | 2.36 → 4.54 |
1725+| `chat_inFileInfoText` | `0xffa1aab3` | `0xff6f7780` | 2.36 → 4.54 |
1726+| `chat_messagePanelHint` | `0xff858a84` | `0xff737873` | 3.52 → 4.50 |
1727+| `chat_topPanelMessage` | `0xff767e7c` | `0xff717877` | 4.16 → 4.51 |
1728+
1729+**Bundled themes** (hooks P2 to P6). The files store signed decimal numbers; the "As colours" column shows the same values as `#AARRGGBB`.
1730+
1731+**`arctic.attheme`** (theme "Arctic Blue")
1732+
1733+| Key | Forkgram 12.10.6 | MultiGram | As colours | Contrast before → after |
1734+|---|---|---|---|---|
1735+| `actionBarDefaultSubtitle` | `-7630182` | `-9341057` | #FF8B929A → #FF71777F | 3.15 → 4.52 |
1736+| `chat_topPanelMessage` | `-8354167` | `-9275526` | #FF808689 → #FF72777A | 3.69 → 4.53 |
1737+| `chat_inFileInfoText` | `-6182221` | `-9472128` | #FFA1AAB3 → #FF6F7780 | 2.36 → 4.54 |
1738+| `windowBackgroundWhiteGrayText` | `-7629665` | `-9472126` | #FF8B949F → #FF6F7782 | 3.07 → 4.53 |
1739+
1740+**`bluebubbles.attheme`** (theme "Blue")
1741+
1742+| Key | Forkgram 12.10.6 | MultiGram | As colours | Contrast before → after |
1743+|---|---|---|---|---|
1744+| `actionBarDefaultSubtitle` | `-8156010` | `-9472127` | #FF838C96 → #FF6F7781 | 3.41 → 4.53 |
1745+| `windowBackgroundWhiteGrayText` | `-7565423` | `-9144455` | #FF8C8F91 → #FF747779 | 3.25 → 4.51 |
1746+| `windowBackgroundWhiteGrayText2` | `-7565423` | `-9144455` | #FF8C8F91 → #FF747779 | 3.25 → 4.51 |
1747+| `windowBackgroundWhiteGrayText4` | `-7565423` | `-9671056` | #FF8C8F91 → #FF6C6E70 | 2.88 → 4.54 |
1748+
1749+**`darkblue.attheme`** (theme "Dark Blue")
1750+
1751+| Key | Forkgram 12.10.6 | MultiGram | As colours | Contrast before → after |
1752+|---|---|---|---|---|
1753+| `chat_messagePanelHint` | `1859907583` | `-1763971073` | #6EDBEFFF → #96DBEFFF | 3.49 → 4.52 |
1754+| `actionBarDefaultSubtitle` | `2111566591` | `-1831079169` | #7DDBF2FF → #92DBF2FF | 4.12 → 4.51 |
1755+| `chat_topPanelMessage` | `1859974399` | `-1814235905` | #6EDCF4FF → #93DCF4FF | 3.57 → 4.54 |
1756+| `chat_inFileInfoText` | `-8812137` | `-7035469` | #FF798997 → #FF94A5B3 | 3.83 → 4.54 |
1757+| `windowBackgroundWhiteGrayText` | `-8549479` | `-7562327` | #FF7D8B99 → #FF8C9BA9 | 4.33 → 4.53 |
1758+| `chats_message` | `-8549479` | `-7562327` | #FF7D8B99 → #FF8C9BA9 | 4.33 → 4.53 |
1759+| `chats_date` | `-9207925` | `-7431514` | #FF737F8B → #FF8E9AA6 | 3.70 → 4.50 |
1760+| `chat_inViews` | `-8812137` | `-7035469` | #FF798997 → #FF94A5B3 | 3.83 → 4.54 |
1761+| `windowBackgroundWhiteGrayText2` | `1878130175` | `-2081292801` | #6FF1FDFF → #83F1FDFF | 3.95 → 4.51 |
1762+| `windowBackgroundWhiteGrayText4` | `-931296359` | `-209876071` | #C87D8B99 → #F37D8B99 | 3.48 → 4.52 |
1763+| `chat_inTimeText` | `-645885536` | `-7035724` | #D98091A0 → #FF94A4B4 | 3.49 → 4.50 |
1764+| `chats_attachMessage` | `-8548712` | `-6710887` | #FF7D8E98 → #FF999999 | 4.46 → 4.53 |
1765+
1766+**`day.attheme`** (theme "Day")
1767+
1768+| Key | Forkgram 12.10.6 | MultiGram | As colours | Contrast before → after |
1769+|---|---|---|---|---|
1770+| `actionBarDefaultSubtitle` | `-8814210` | `-9340810` | #FF79817E → #FF717876 | 4.00 → 4.52 |
1771+| `chat_topPanelMessage` | `-9011588` | `-9340809` | #FF767E7C → #FF717877 | 4.16 → 4.51 |
1772+| `chat_inFileInfoText` | `-7565679` | `-9671312` | #FF8C8E91 → #FF6C6D70 | 2.88 → 4.54 |
1773+| `windowBackgroundWhiteGrayText` | `-8354940` | `-9078920` | #FF808384 → #FF757778 | 3.82 → 4.50 |
1774+| `chat_inViews` | `-274882397` | `-9736591` | #EF9DA0A3 → #FF6B6E71 | 2.17 → 4.50 |
1775+| `chat_inTimeText` | `-7105127` | `-9671311` | #FF939599 → #FF6C6D71 | 2.63 → 4.53 |
1776+
1777+**`night.attheme`** (theme "Night")
1778+
1779+| Key | Forkgram 12.10.6 | MultiGram | As colours | Contrast before → after |
1780+|---|---|---|---|---|
1781+| `chat_messagePanelHint` | `1694498815` | `1979711487` | #64FFFFFF → #75FFFFFF | 3.67 → 4.52 |
1782+| `actionBarDefaultSubtitle` | `1945301746` | `-2131561742` | #73F2F2F2 → #80F2F2F2 | 3.98 → 4.50 |
1783+| `chat_topPanelMessage` | `1694498815` | `1962934271` | #64FFFFFF → #74FFFFFF | 3.72 → 4.53 |
1784+| `chat_inFileInfoText` | `-9013641` | `-7763573` | #FF767677 → #FF89898B | 3.56 → 4.50 |
1785+| `windowBackgroundWhiteGrayText` | `1862270975` | `1962934271` | #6EFFFFFF → #74FFFFFF | 4.25 → 4.56 |
1786+| `chats_date` | `-8882056` | `-8289920` | #FF787878 → #FF818180 | 4.02 → 4.51 |
1787+| `chat_inViews` | `-8881024` | `-8025458` | #FF787C80 → #FF858A8E | 3.84 → 4.51 |
1788+| `windowBackgroundWhiteGrayText2` | `1862139391` | `1962802687` | #6EFDFDFF → #74FDFDFF | 4.20 → 4.50 |
1789+| `windowBackgroundWhiteGrayText4` | `-9803158` | `-9079692` | #FF6A6A6A → #FF757474 | 3.88 → 4.51 |
1790+| `chat_inTimeText` | `-8552575` | `-7894388` | #FF7D7F81 → #FF878A8C | 4.02 → 4.52 |
1791+
1792+**Keys the accent tint skips** (hook P10, `PaletteFix.addAccentExclusions`). The order does not matter.
1793+
1794+| # | Key |
1795+|---|---|
1796+| 1 | `key_windowBackgroundGray` |
1797+| 2 | `key_windowBackgroundWhiteGrayText` |
1798+| 3 | `key_windowBackgroundWhiteGrayText2` |
1799+| 4 | `key_chats_message` |
1800+| 5 | `key_chats_date` |
1801+| 6 | `key_actionBarDefaultSubtitle` |
1802+| 7 | `key_chat_inTimeText` |
1803+| 8 | `key_chat_inFileInfoText` |
1804+| 9 | `key_chat_messagePanelHint` |
1805+| 10 | `key_windowBackgroundWhiteGrayText4` |
1806+| 11 | `key_chat_topPanelMessage` |
1807+| 12 | `key_chat_inViews` |
1808+| 13 | `key_chat_messagePanelIcons` |
1809+
1810+**Marks and the fills they sit on** (`PaletteFix.Keys.ON_ACCENT`). While the rule is on, each mark becomes white or `#FF050505` for its fill. The order must match overlay A, with the extra voice pair last.
1811+
1812+| # | Mark (content key) | Fill it sits on |
1813+|---|---|---|
1814+| 1 | `key_chats_actionIcon` | `key_chats_actionBackground` |
1815+| 2 | `key_chats_unreadCounterText` | `key_chats_unreadCounter` |
1816+| 3 | `key_featuredStickers_buttonText` | `key_featuredStickers_addButton` |
1817+| 4 | `key_checkboxSquareCheck` | `key_checkboxSquareBackground` |
1818+| 5 | `key_dialogRoundCheckBoxCheck` | `key_dialogRoundCheckBox` |
1819+| 6 | `key_chat_goDownButtonCounter` | `key_chat_goDownButtonCounterBackground` |
1820+| 7 | `key_dialogCheckboxSquareCheck` | `key_dialogCheckboxSquareBackground` |
1821+| 8 | `key_chat_attachCheckBoxCheck` | `key_chat_attachCheckBoxBackground` |
1822+| 9 | `key_chat_inMediaIcon` | `key_chat_inLoader` |
1823+| 10 | `key_chat_inContactIcon` | `key_chat_inContactBackground` |
1824+| 11 | `key_picker_badgeText` | `key_picker_badge` |
1825+| 12 | `key_dialogFloatingIcon` | `key_dialogFloatingButton` |
1826+| 13 | `key_chat_messagePanelVoicePressed` | `key_chat_messagePanelVoiceBackground` (extra pair, only in `PaletteFix.java`) |
1827+
1828+**Muted unread badge** (`PaletteFix.MUTED_FILL`). While the rule is on, the badge (`key_chats_unreadCounterMuted`) gets a neutral fill that suits the digit colour. Four keys normally borrow the badge colour: `key_avatar_backgroundArchived`, `key_chats_archivePinBackground`, `key_chats_archivePullDownBackground` and `key_checkboxDisabled` (`MUTED_PINS`). Before it changes the badge, the rule writes their current colour explicitly, so they keep their look.
1829+
1830+| Theme | Badge fill with white digits | Badge fill with near-black digits |
1831+|---|---|---|
1832+| Blue | `#FF73777B` | `#FFBEC3C7` |
1833+| Arctic Blue | `#FF73777B` | `#FFBEC3C7` |
1834+| Day | `#FF73777B` | `#FFBEC3C7` |
1835+| Dark Blue | `#FF3E5263` | `#FF63798B` |
1836+| Night | `#FF454545` | `#FF777676` |
1837+
1838+#### 8.2.5 Regenerate and verify
1839+
1840+**1. Check the tree** (a few seconds). From the root of a checkout:
1841+
1842+```sh
1843+python3 multigram/tools/palette_fix_check.py
1844+python3 multigram/tools/palette_fix_check.py --base origin/forkgram
1845+```
1846+
1847+Success prints one line, `palette-fix: OK`, and exit status 0. A failure prints one line per problem, then `palette-fix: FAILED, N problem(s)`, and exit status 1. Two real problem lines, from a test copy with one value changed and one hook line deleted:
1848+
1849+```text
1850+palette-fix: ThemeColors key_chat_topPanelMessage = ['0xff717878'], expected 0xff717877
1851+palette-fix: hook 'PaletteFix\\.applyToColorMap\\(themeInfo, accent, currentColors\\); // MultiGram:' found 0 times in TMessagesProj/src/main/java/org/telegram/ui/PeerColorActivity.java
1852+```
1853+
1854+`--base` must name the snapshot the tree is built on. On the MultiGram tree, `--base dc780e81e` (plain Telegram) reports 1450 false problems, because Forkgram's Monet theme files then count as changes.
1855+
1856+This check compares the tree with overlay A. It cannot tell whether overlay A still suits the new Forkgram. The simulator does that.
1857+
1858+**2. Re-derive the values from the new Forkgram** (about 3 seconds). The simulator treats the tree it reads as stock, so give it the plain new snapshot, never the MultiGram tree:
1859+
1860+```sh
1861+rm -rf /tmp/fk-new /tmp/overlay-new
1862+mkdir -p /tmp/fk-new /tmp/overlay-new
1863+git archive origin/forkgram TMessagesProj/src/main/java/org/telegram/ui/ActionBar TMessagesProj/src/main/assets | tar -x -C /tmp/fk-new
1864+cd multigram/tools/sim
1865+TG_REPO=/tmp/fk-new CANVAS_OUT=/tmp/overlay-new python3 canvas.py --build > /tmp/overlay-new/build.log
1866+grep '^VARIANT A' /tmp/overlay-new/build.log
1867+cmp /tmp/overlay-new/overlay_A.json ../../palette-fix/overlay_A.json && cmp /tmp/overlay-new/overlay_A.json palette-fix/overlay_A.json && echo "overlay A unchanged"
1868+cd ../../..
1869+```
1870+
1871+- The first line clears what an earlier update left there. `tar` only adds files, so files Forkgram has since deleted would stay and be modelled.
1872+- The simulator needs only `ui/ActionBar` and the assets, so `git archive` of those two folders is enough.
1873+- Always set `CANVAS_OUT`. Without it, `--build` overwrites the committed `multigram/tools/sim/palette-fix/overlay_A.json` and `overlay_B.json`.
1874+- On Forkgram 12.10.6 it prints `VARIANT A (1.8 s): 23 defaults, 36 attheme lines, 13 exclusions`, and both `cmp` commands find no difference.
1875+
1876+If `overlay A unchanged` appears, there is nothing to do. If not, see what changed:
1877+
1878+```sh
1879+diff <(python3 -m json.tool --sort-keys multigram/palette-fix/overlay_A.json) <(python3 -m json.tool --sort-keys /tmp/overlay-new/overlay_A.json)
1880+```
1881+
1882+`/tmp/overlay-new/build.log` and `overlay_A_rows.json` explain each value: theme, key, old and new colour, contrast before and after, and the source file and line.
1883+
1884+To adopt a new overlay:
1885+
1886+1. Copy `/tmp/overlay-new/overlay_A.json` over **both** `multigram/palette-fix/overlay_A.json` and `multigram/tools/sim/palette-fix/overlay_A.json`.
1887+2. Write the new values into `ThemeColors.java` and the `.attheme` files, and the new tables into `PaletteFix.java`. `palette_fix_check.py` lists every line that differs, with the expected value. Remember the `// MultiGram: contrast fix, was <old value>` comments in `ThemeColors.java`.
1888+3. Run `python3 multigram/tools/palette_fix_check.py --base origin/forkgram` until it says OK.
1889+4. Regenerate the style table (8.3.5), because it is checked against overlay A.
1890+5. Fold the changes in with one fixup per commit (6.3): `multigram/palette-fix/overlay_A.json`, `ThemeColors.java`, the `.attheme` files and `PaletteFix.java` into "Fix palette contrast for generated accents"; `multigram/tools/sim/palette-fix/overlay_A.json` and a new `multigram_styles.bin` into "Add the style table and its generator".
1891+
1892+**3. Residual check on the finished tree.** Run the simulator on the MultiGram tree itself:
1893+
1894+```sh
1895+mkdir -p /tmp/overlay-in-tree
1896+cd multigram/tools/sim
1897+TG_REPO=$(git rev-parse --show-toplevel) CANVAS_OUT=/tmp/overlay-in-tree python3 canvas.py --build | grep '^VARIANT A'
1898+cd ../../..
1899+```
1900+
1901+On 4780eb095b it prints `VARIANT A (1.6 s): 12 defaults, 0 attheme lines, 13 exclusions` (the time varies). The 12 defaults are the on-accent marks the app keeps white on purpose. Any `.attheme` line, or a 13th default, means a grey below 4.5 : 1.
1902+
1903+**4. Measure generated accents** on the plain new Forkgram from step 2:
1904+
1905+```sh
1906+cd multigram/tools/sim
1907+TG_REPO=/tmp/fk-new python3 canvas.py --measure 200 --variant A --procs 4
1908+cd ../../..
1909+```
1910+
1911+On 12.10.6 the line `pass rate (generated, all pairs)` shows `100.00%` for all five themes, and `residual failing pairs` shows `(none)`. Use `--measure 5000` before a release.
1912+
1913+**On a device:** with a stock preset accent, only grey secondary texts differ from Forkgram. With a light runtime accent (pick light yellow with the colour wheel), marks on accent fills turn near-black: the chat list pencil, unread digits, Join buttons, checkboxes, the send button. With a dark accent they stay white. To test hook P12, install a build from before this commit, select Arctic Blue, then install the new build over it: the darker greys must appear without clearing data.
1914+
1915+#### 8.2.6 Gotchas
1916+
1917+- **Only 11 of the 23 defaults in overlay A go into `ThemeColors.java`.** The other 12 are the on-accent marks. Overlay A stores them as `#FF050505` (the rule applied to the Blue theme inside the simulator), but the app keeps them stock white and sets them at run time. Copying them would turn every stock user's pencil and badge digits near-black. The check flags it.
1918+- **The values sit right at the minimum** (4.50 to 4.56 : 1). An upstream change to a background, bubble or the accent maths causes no conflict, but can push a grey below 4.5 : 1. Re-derive the overlay after every update (step 2 above). As a second line of defence, `check_style_table.py` re-checks every generated style on the new sources.
1919+- **If upstream changes the stock value of a replaced line**, the replay conflicts on that line. Do not just keep MultiGram's value: re-derive it, or drop the line if the new stock value passes. Update the `was 0x...` comment.
1920+- **The simulator only exists from cddd62df98.** When you re-apply 5b53859682 alone, run the simulator from a full MultiGram checkout with `TG_REPO` pointing at the tree to model.
1921+- **The checker matches hook lines exactly, including their comments.** Reformatting a hook line fails the check. But it checks only 2 of the 9 MultiGram lines in `ChatActivityEnterView.java` (the `int glyph = ...` line and the `drawableInverse` line), and it does not check whether hooks sit in the right method. Check the rest by hand (10.3).
1922+- **Order matters in `PaletteFix`.** `Keys.ON_ACCENT` and `MUTED_PINS` must follow overlay A's order. The exclusions are compared as a set.
1923+- **`Theme.key_*` values are counted at run time** (`colorsCount++`), not compile-time constants. They cannot be used in `switch` statements, and `PaletteFix` reads them through a lazy holder class (`Keys`).
1924+- **`getColor(key, null, true)`** becomes ambiguous if upstream adds another three-argument `getColor`. Cast the `null` to `(boolean[])`.
1925+- **`isActiveFor` copies the private `ThemeInfo.isDefaultMainAccent()`.** If upstream changes that method, change the copy.
1926+- **Accent ids.** The rule depends on these conventions: presets have ids up to 99, the migrated old custom accent is 100, new accents get 101 and up, server and cloud accents carry a `TL_theme` in `accent.info`, and chat-theme accents have small ids. If upstream changes them, stock accents could get the rule, or generated ones could lose it.
1927+- **`EmojiThemes` has two identical `fillAccentColors` blocks.** Hook only the one in `createColors`, not the one in `getPreviewColors`.
1928+- **Hook P11's place matters:** after the accent tint and all computed colours, before the colours are copied into paints.
1929+- **Value-only edits to bundled themes reach users only through hook P12**, because upstream re-copies a theme file only when its size changes.
1930+- **Theme files use signed decimals without the `key_` prefix.** Overlay A uses unsigned numbers with the prefix. For a value with the top bit set, subtract 2^32. Write decimals; the checker ignores `#hex`. Match lines by key, because the same old value can occur on several lines.
1931+- **`chats_attachMessage` on Dark Blue is a pure grey (`#FF999999`) on purpose.** The key is accent-coloured on the light themes, so it cannot join the global exclusion list, and a pure grey is never tinted. `chat_messagePanelIcons` is excluded without a value change. Do not tidy either.
1932+- **New calls of `fillAccentColors`** are not found automatically. After an update, run the `git grep` of P13. On 4780eb095b it prints 11 lines: the method itself and 10 calls, 6 of them hooked (P7, P9, P11, P13, P16, P17). Decide for any new call.
1933+
1934+### 8.3 Style table and random style (cddd62df98 and 27d32d57b4)
1935+
1936+#### 8.3.1 What and why
1937+
1938+These two commits give every fresh install its own day and night colour style.
1939+
1940+The colours are not made on the device. Commit **cddd62df98** adds a precomputed table, `TMessagesProj/src/main/assets/multigram_styles.bin`. It holds 4,096 styles for each of the five bundled themes (Blue, Arctic Blue, Day, Dark Blue, Night), 20,480 in all. A style is an accent colour, 1 to 4 outgoing bubble colours and a wallpaper gradient of 2 to 4 colours, without a pattern. The commit also adds the Python tools that make the table and check that every style is readable. They include the simulator. This commit touches no upstream file.
1941+
1942+Commit **27d32d57b4** adds the Java side and 15 hooks in 7 upstream files:
1943+
1944+- **Stage A** (hook S1, at app start): a fresh install gets a random seed, which picks one day and one night style. The choice is stored in the preferences file `rebrand_style`.
1945+- **Stage B** (hook S3, while the `Theme` class starts): the two styles become local accents (ids 101 and up, no pattern) and are selected before the first frame.
1946+- **Chat Settings**: a "Shuffle my style" row with Undo. "Reset to defaults" returns to the generated style.
1947+- **Privacy**: generated accents are never uploaded, shared or turned into cloud themes.
1948+
1949+Installs that existed before are never restyled automatically.
1950+
1951+#### 8.3.2 New files
1952+
1953+| Path | Purpose | Upstream names it relies on |
1954+|---|---|---|
1955+| `TMessagesProj/src/main/assets/multigram_styles.bin` | The style table, format version 1: 819,384 bytes, 20,480 records. Made by `make_style_table.py`; never edit it by hand. The same sources always give the same bytes. | The theme keys "Blue", "Arctic Blue", "Day", "Dark Blue", "Night" and their night flag; `Theme.java`, `ThemeColors.java` and the five `.attheme` files it was checked against; the `ThemeAccent` fields each record fills |
1956+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/StyleTable.java` | Reads and fully verifies the table (magic, version, sizes, directory, CRC-32). `get(night, n)` returns the n-th day or night style. It never touches `Theme`, so it is safe before `Theme` exists. | `AssetManager.open()` (not `openFd()`, because the asset is compressed) |
1957+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyle.java` | The logic: Stage A, Stage B, Shuffle, Undo, Reset, the privacy records, and the seed API that the knobs use. | `Theme.getActiveTheme()`, `getCurrentTheme()`, `getCurrentNightTheme()`, `setCurrentNightTheme()`, `getTheme(String)`, `saveThemeAccents(...)` (5- and 6-argument forms), `deleteThemeAccent(...)`, `selectedAutoNightType`; `ThemeInfo.getAccent(boolean)`, `themeAccents`, `themeAccentsMap`, `prevAccentId`, `currentAccentId`, `overrideWallpaper`, `assetName`, `isDark()`, `getKey()`, `isMonet()` (Forkgram only); the `ThemeAccent` colour fields; `NotificationCenter.needSetDayNightTheme`; the preference files `themeconfig` and `mainconfig`; `PaletteFix.LAST_STOCK_ACCENT_ID` |
1958+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyleUi.java` | The Chat Settings side: the Shuffle row, Undo, the custom theme tile refresh, and the "stays on this device" messages. | `TextCell`, `BulletinFactory`, `EmojiThemes.createPreviewCustom(int)` and its tile emoji, `ChatThemeBottomSheet.ChatThemeItem`, `R.drawable.menu_random`, `R.raw.info`, `Utilities.globalQueue` |
1959+| `TMessagesProj/src/main/res/values/multigram_strings.xml` | Five English strings, `MultiGram*`. They are read with `Context.getString`, never through `LocaleController`. | Telegram's build turns only `strings.xml` files into its translation assets, so this separate file stays a plain resource. The names must not clash with other strings. |
1960+| `multigram/random-style/README.md` | Design document: lifecycle, preference keys, privacy, the hook table, and a 15-item device checklist. | Documentation only |
1961+| `multigram/tools/random_style_check.py` | Checks the 15 hooks and where they sit, that the feature makes no server calls and saves without upload, the strings, the asset, and the backup policy. Prints `random-style: OK`. | The landmark lines of each hook; `forkgram/SettingsBackup.kt` and its `ALLOWED_PREFS` (Forkgram only: the check crashes without this file); `BackupAgent.java`; `android:backupAgent=".BackupAgent"` in the manifest |
1962+| `multigram/tools/README.md` | The table format, how an entry is applied, the readability rule, generation, the checks, and "Replaying onto a new Forkgram release". | Documentation only |
1963+| `multigram/tools/.gitignore` | Ignores `__pycache__/`, `*.pyc` and `sim/cache/`. | None |
1964+| `multigram/tools/check.sh` | The checks CI runs after the compile (10.2). | bash and python3 |
1965+| `multigram/tools/make_style_table.py` | Makes the table. `--check` only compares. Exit status: 0 written or identical, 3 different, 4 stalled, 1 error. | Everything `style_table.py` and the simulator read |
1966+| `multigram/tools/check_style_table.py` | Checks the table's format and the readability of every entry on this tree's theme sources. Exit status 0 on PASS, 1 on FAIL. | Same as the simulator; `TG_REPO` to model another checkout |
1967+| `multigram/tools/style_table.py` | Shared code: the binary format, the generator's settings, and how overlay A is applied. | overlay A; the palette fix's exact guard line in `fillAccentColors` (hook P8) |
1968+| `multigram/tools/sim/tgsrc.py` | Reads the theme engine's inputs straight from the Java and `.attheme` sources, and caches the result. Raises `... update the simulator` when a structure changed. | Many exact forms in `Theme.java` and `ThemeColors.java`: key declarations, fallbacks, exclusions, accent presets, `fillAccentColors`, `createDefaultWallpaper`, `createDefaultColors`, `createColorKeysMap` |
1969+| `multigram/tools/sim/engine.py` | A port of Telegram's accent engine to Python. | The logic of `fillAccentColors`, `changeColorAccent`, `getAccentColor`, `useBlackText`, `changeBrightness` and others, as in Telegram 12.10.5. A change inside these methods is **not** detected; see 8.3.6. |
1970+| `multigram/tools/sim/readability.py` | The 99 readability pairs and the contrast maths. | Colour key names; models of the bubble, wallpaper and service message drawing |
1971+| `multigram/tools/sim/style_sim.py`, `multigram/tools/sim/canvas.py`, `multigram/tools/sim/detmath.py`, `multigram/tools/sim/detmath_tables.py` | The style generator, the overlay builder and measurer (`canvas.py`), and portable maths that keep results identical on every machine. | None beyond the files above |
1972+| `multigram/tools/sim/palette-fix/overlay_A.json`, `multigram/tools/sim/palette-fix/overlay_B.json` | overlay A (a copy, used when the modelled tree has none) and the rejected variant B. | Must stay byte-identical to `multigram/palette-fix/overlay_A.json` |
1973+| `multigram/tools/sim/README.md` | How the simulator works and what it reads. | Documentation only |
1974+
1975+27d32d57b4 also adds the random style step to `check.sh` and a two-line note about it to `multigram/tools/README.md`.
1976+
1977+#### 8.3.3 Hooks
1978+
1979+##### S1. Stage A: pick this install's style at first start
1980+
1981+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java`
1982+
1983+**Where:** Class `ApplicationLoader`, method `onCreate()`: right after the second `if (applicationContext == null) { applicationContext = getApplicationContext(); }`, before `NativeLoader.initNativeLibs(...)`. In Forkgram 12.10.6 the text below starts at line 368.
1984+
1985+**Find this** (its first line occurs twice in the file, the whole block once; text from Forkgram 12.10.6):
1986+
1987+```java
1988+ applicationContext = getApplicationContext();
1989+ }
1990+
1991+ NativeLoader.initNativeLibs(ApplicationLoader.applicationContext);
1992+```
1993+
1994+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
1995+
1996+```diff
1997+ if (applicationContext == null) {
1998+ applicationContext = getApplicationContext();
1999+ }
2000++ org.telegram.messenger.multigram.RandomStyle.onApplicationCreate(applicationContext); // MultiGram: per-install style seed, before anything touches Theme
2001+
2002+ NativeLoader.initNativeLibs(ApplicationLoader.applicationContext);
2003+
2004+```
2005+
2006+**Why:** It runs on every start but does its work only once per install. An install that already has theme settings is marked `existing` and never restyled. A fresh install gets a random seed and picks one day and one night entry from the style table. It never throws.
2007+
2008+**If the code moved:** It must run in `Application.onCreate` after `applicationContext` is set, and **before anything loads the `Theme` class**. Theme's static initializer writes a theme setting on every start; after that, every fresh install would count as `existing` and silently get no style. Keep it the first MultiGram line in `onCreate`: K1 goes directly below it. The lines `applicationContext = getApplicationContext();` and `NativeLoader.initNativeLibs(` each occur twice in the file; the 4-line block shown occurs once. `random_style_check.py` checks the order.
2009+
2010+
2011+##### S2. Never upload a generated style
2012+
2013+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java`
2014+
2015+**Where:** Class `MessagesController`, method `saveThemeToServer(Theme.ThemeInfo, Theme.ThemeAccent)`: the first statement. In Forkgram 12.10.6 the text below starts at line 9131.
2016+
2017+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2018+
2019+```java
2020+ public void saveThemeToServer(Theme.ThemeInfo themeInfo, Theme.ThemeAccent accent) {
2021+```
2022+
2023+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2024+
2025+```diff
2026+ }
2027+
2028+ public void saveThemeToServer(Theme.ThemeInfo themeInfo, Theme.ThemeAccent accent) {
2029++ if (org.telegram.messenger.multigram.RandomStyle.isUploadBlocked(themeInfo, accent)) return; // MultiGram: a generated style never leaves the device
2030+ if (themeInfo == null) {
2031+ return;
2032+ }
2033+```
2034+
2035+**Why:** Every theme or accent upload passes through this method. A generated accent, or an edit or copy of one, is never uploaded.
2036+
2037+**If the code moved:** Put it at the very start of whatever method builds the theme file and uploads a local accent. If upstream splits the upload into several methods, guard each one. The check requires it to be the first statement of `saveThemeToServer`.
2038+
2039+
2040+##### S3. Stage B: turn the picked entries into accents before the first frame
2041+
2042+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
2043+
2044+**Where:** Class `Theme`, static initializer: after the saved accents are loaded and the old-accent migration is committed, before the auto-night setting is read. In Forkgram 12.10.6 the text below starts at line 4279.
2045+
2046+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2047+
2048+```java
2049+ oldEditorNew.commit();
2050+ }
2051+
2052+ selectedAutoNightType = preferences.getInt("selectedAutoNightType", Build.VERSION.SDK_INT >= 29 ? AUTO_NIGHT_TYPE_SYSTEM : AUTO_NIGHT_TYPE_NONE);
2053+```
2054+
2055+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2056+
2057+```diff
2058+ oldEditor.commit();
2059+ oldEditorNew.commit();
2060+ }
2061++ applyingTheme = org.telegram.messenger.multigram.RandomStyle.onThemeInit(applyingTheme); // MultiGram: a fresh install gets its generated style before the first frame
2062+
2063+ selectedAutoNightType = preferences.getInt("selectedAutoNightType", Build.VERSION.SDK_INT >= 29 ? AUTO_NIGHT_TYPE_SYSTEM : AUTO_NIGHT_TYPE_NONE);
2064+ autoNightScheduleByLocation = preferences.getBoolean("autoNightScheduleByLocation", false);
2065+```
2066+
2067+**Why:** On a fresh install this creates the day and night accents from the picked entries (new ids from 101 up, no pattern), saves them without uploading, selects them and returns the day theme. The stock code then applies it, so the first frame already shows the style. On every start it also tidies its records of generated accents.
2068+
2069+**If the code moved:** Place it inside the static initializer where all of these hold: (1) every bundled theme's accents are loaded (the loop with `String accents = themeConfig.getString("accents_" + info.assetName, null);`) and the old-accent migration is committed; (2) the local `applyingTheme` was read from the `theme` setting; (3) `selectedAutoNightType` is not read yet, and neither `currentDayTheme = applyingTheme;` nor the first `applyTheme(applyingTheme, false, false, switchToTheme == 2);` has run. If the local variable is renamed, assign the result to the new name. The check tests this order with the landmark patterns in `order(THEME, ...)` inside `check_placement()` of `random_style_check.py`. If upstream changes one of those landmark lines, the check fails although the hook is right (10.2).
2070+
2071+
2072+##### S4. A setter for the private day theme
2073+
2074+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
2075+
2076+**Where:** Class `Theme`, between `getCurrentTheme()` and `getCurrentNightTheme()`: a new one-line method and a blank line. In Forkgram 12.10.6 the text below starts at line 6448.
2077+
2078+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2079+
2080+```java
2081+ public static ThemeInfo getCurrentNightTheme() {
2082+```
2083+
2084+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2085+
2086+```diff
2087+ return currentDayTheme != null ? currentDayTheme : defaultTheme;
2088+ }
2089+
2090++ public static void setCurrentDayTheme(ThemeInfo theme) { currentDayTheme = theme; } // MultiGram: "Shuffle my style" sets the day theme while the night theme is showing
2091++
2092+ public static ThemeInfo getCurrentNightTheme() {
2093+ return currentNightTheme;
2094+ }
2095+```
2096+
2097+**Why:** `currentDayTheme` is private. When Shuffle, Undo or Reset runs while the night theme is showing, MultiGram records the new day style here for the next switch to day.
2098+
2099+**If the code moved:** Anywhere in class `Theme`. The check requires this exact line with 4 spaces of indentation. If upstream renames the field, keep the method's name and point it at the new field. Then change the old field name in two patterns of `multigram/tools/random_style_check.py`, both in "Give each install its own random style": the S4 entry in `HOOKS`, and the landmark `currentDayTheme = applyingTheme;` in `check_placement()`. Git often replays this hook without a conflict after such a rename, because the rename changes only other lines. Then only the check or the compile shows it.
2100+
2101+
2102+##### S5. "Create new theme" refuses the generated style
2103+
2104+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java`
2105+
2106+**Where:** Class `AlertsCreator`, method `createThemeCreateDialog(BaseFragment, int, Theme.ThemeInfo, Theme.ThemeAccent)`: right after the `fragment == null` check. In Forkgram 12.10.6 the text below starts at line 8296.
2107+
2108+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2109+
2110+```java
2111+ public static void createThemeCreateDialog(BaseFragment fragment, int type, Theme.ThemeInfo switchToTheme, Theme.ThemeAccent switchToAccent) {
2112+```
2113+
2114+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2115+
2116+```diff
2117+ if (fragment == null || fragment.getParentActivity() == null) {
2118+ return;
2119+ }
2120++ if (org.telegram.messenger.multigram.RandomStyleUi.interceptThemeCreate(fragment, switchToAccent)) return; // MultiGram: a cloud theme cannot start from the generated style
2121+ Context context = fragment.getParentActivity();
2122+ final EditTextBoldCursor editText = new EditTextBoldCursor(context);
2123+ editText.setBackground(null);
2124+```
2125+
2126+**Why:** Creating a cloud theme would upload the accent's colours. For a generated accent the dialog is replaced by a short message that the style stays on this device.
2127+
2128+**If the code moved:** At the start of the flow that creates a cloud theme from an accent, after the fragment check (the call shows a message on the fragment). The check requires it directly after `if (fragment == null || fragment.getParentActivity() == null) { return; }`.
2129+
2130+
2131+##### S6. Rebuild the custom theme tile after Shuffle
2132+
2133+**File:** `TMessagesProj/src/main/java/org/telegram/ui/DefaultThemesPreviewCell.java`
2134+
2135+**Where:** Class `DefaultThemesPreviewCell`, method `updateDayNightMode()`: the first statement. In Forkgram 12.10.6 the text below starts at line 352.
2136+
2137+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2138+
2139+```java
2140+ public void updateDayNightMode() {
2141+```
2142+
2143+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2144+
2145+```diff
2146+ }
2147+
2148+ public void updateDayNightMode() {
2149++ org.telegram.messenger.multigram.RandomStyleUi.refreshCustomTile(adapter.items, parentFragment.getCurrentAccount()); // MultiGram: Shuffle, Undo and Reset replace the accents the custom tile points to
2150+ if (currentType == ThemeActivity.THEME_TYPE_BASIC || currentType == TYPE_CUSTOM_LIST) {
2151+ themeIndex = !Theme.isCurrentThemeDay() ? 2 : 0;
2152+ } else {
2153+```
2154+
2155+**Why:** Chat Settings shows a strip of colour themes with a custom tile (the palette emoji). Shuffle, Undo and Reset replace the accents that tile points to; this rebuilds the tile so it never applies a deleted accent.
2156+
2157+**If the code moved:** Call it wherever the strip refreshes after a theme change. It finds the tile by the emoji that `EmojiThemes.createPreviewCustom` sets; if upstream changes that emoji, change `RandomStyleUi.CUSTOM_TILE_EMOJI` too. The check requires it as the first line of `updateDayNightMode`.
2158+
2159+
2160+##### S7. The accent editor marks copies of the generated style
2161+
2162+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java`
2163+
2164+**Where:** Class `ThemePreviewActivity`, constructor, branch `if (screenType == SCREEN_TYPE_ACCENT_COLOR)`, right after the accent is obtained. In Forkgram 12.10.6 the text below starts at line 562.
2165+
2166+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2167+
2168+```java
2169+ accent = applyingTheme.getAccent(!edit);
2170+```
2171+
2172+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2173+
2174+```diff
2175+
2176+ if (screenType == SCREEN_TYPE_ACCENT_COLOR) {
2177+ accent = applyingTheme.getAccent(!edit);
2178++ org.telegram.messenger.multigram.RandomStyle.onAccentCopied(applyingTheme, accent, !edit); // MultiGram: a copy of the generated style stays on this device too
2179+ if (accent != null) {
2180+ useDefaultThemeForButtons = false;
2181+ backupAccentColor = accent.accentColor;
2182+```
2183+
2184+**Why:** The accent editor either edits the current accent or creates a copy. If the source is a generated accent, the result is marked too, so it also stays on the device.
2185+
2186+**If the code moved:** Right after the editor gets its accent and before anything can save or upload it. The check requires it on the line directly after `accent = applyingTheme.getAccent(!edit);`.
2187+
2188+
2189+##### S8. Chat Settings: a field for the new row
2190+
2191+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2192+
2193+**Where:** Class `ThemeActivity`, the row index fields. In Forkgram 12.10.6 the text below starts at line 223.
2194+
2195+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2196+
2197+```java
2198+ private int createNewThemeRow;
2199+```
2200+
2201+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2202+
2203+```diff
2204+ private int editThemeRow;
2205+ @Keep
2206+ private int createNewThemeRow;
2207++ private int shuffleStyleRow = -1; // MultiGram: "Shuffle my style"
2208+ private int lastShadowRow;
2209+ @Keep
2210+ private int stickersRow;
2211+```
2212+
2213+**Why:** The row index of "Shuffle my style". It is -1 when the row is not shown.
2214+
2215+**If the code moved:** Any field position works. Keep the initial value -1, because only the Chat Settings branch of `updateRows` sets it.
2216+
2217+
2218+##### S9. Chat Settings: reset the row index
2219+
2220+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2221+
2222+**Where:** Class `ThemeActivity`, method `updateRows(boolean)`: the block that sets every row index to -1. In Forkgram 12.10.6 the text below starts at line 607.
2223+
2224+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2225+
2226+```java
2227+ createNewThemeRow = -1;
2228+```
2229+
2230+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2231+
2232+```diff
2233+ themePreviewRow = -1;
2234+ editThemeRow = -1;
2235+ createNewThemeRow = -1;
2236++ shuffleStyleRow = -1; // MultiGram: "Shuffle my style"
2237+
2238+ appIconHeaderRow = -1;
2239+ appIconSelectorRow = -1;
2240+```
2241+
2242+**Why:** Resets the row every time the rows are rebuilt, so other screens that share this class never show it.
2243+
2244+**If the code moved:** With the other `xxxRow = -1;` lines at the top of the method that counts the rows.
2245+
2246+
2247+##### S10. Chat Settings: place the row under the colour themes
2248+
2249+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2250+
2251+**Where:** Class `ThemeActivity`, method `updateRows(boolean)`, branch `else if (currentType == THEME_TYPE_BASIC)` (Chat Settings), after the colour theme strip. In Forkgram 12.10.6 the text below starts at line 663.
2252+
2253+**Find this** (its first line occurs twice in the file, the whole block once; text from Forkgram 12.10.6):
2254+
2255+```java
2256+ themeListRow2 = rowCount++;
2257+ themeInfoRow = rowCount++;
2258+```
2259+
2260+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2261+
2262+```diff
2263+ themeHeaderRow = rowCount++;
2264+
2265+ themeListRow2 = rowCount++;
2266++ shuffleStyleRow = rowCount++; // MultiGram: under the colour themes
2267+ themeInfoRow = rowCount++;
2268+
2269+ bubbleRadiusHeaderRow = rowCount++;
2270+```
2271+
2272+**Why:** Shows "Shuffle my style" directly under the colour theme strip.
2273+
2274+**If the code moved:** In the branch that builds Chat Settings (`THEME_TYPE_BASIC`), right after the row that holds the theme strip. `themeListRow2 = rowCount++;` also occurs in the themes browser branch; do not use that one. The 2-line block shown occurs once. The check requires `themeListRow2 = rowCount++;` directly followed by `shuffleStyleRow = rowCount++;`.
2275+
2276+
2277+##### S11. Sharing refuses the generated style
2278+
2279+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2280+
2281+**Where:** Class `ThemeActivity`, method `didReceivedNotification(int, int, Object...)`, branch `NotificationCenter.needShareTheme`, after the `isPaused` check. In Forkgram 12.10.6 the text below starts at line 910.
2282+
2283+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2284+
2285+```java
2286+ } else if (id == NotificationCenter.needShareTheme) {
2287+ if (getParentActivity() == null || isPaused) {
2288+ return;
2289+ }
2290+```
2291+
2292+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2293+
2294+```diff
2295+ if (getParentActivity() == null || isPaused) {
2296+ return;
2297+ }
2298++ if (org.telegram.messenger.multigram.RandomStyleUi.interceptShare(this, (Theme.ThemeAccent) args[1])) return; // MultiGram: the generated style stays on this device
2299+ sharingTheme = (Theme.ThemeInfo) args[0];
2300+ sharingAccent = (Theme.ThemeAccent) args[1];
2301+ sharingProgressDialog = new AlertDialog(getParentActivity(), AlertDialog.ALERT_TYPE_SPINNER);
2302+```
2303+
2304+**Why:** Sharing an accent needs an upload, which S2 refuses. Without this hook a spinner would wait forever. The hook shows the "stays on this device" message instead.
2305+
2306+**If the code moved:** At the start of the handler that begins sharing a theme or accent (the one that stores `sharingTheme` and `sharingAccent` and shows the spinner), before the spinner. `args[1]` is the accent.
2307+
2308+
2309+##### S12. "Reset to defaults" also resets the colours
2310+
2311+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2312+
2313+**Where:** Class `ThemeActivity`, action bar menu handler, `} else if (id == reset_settings) {`: the last statement of the confirm button's lambda. In Forkgram 12.10.6 the text below starts at line 1041.
2314+
2315+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2316+
2317+```java
2318+ Theme.reloadWallpaper(true);
2319+ }
2320+ }
2321+ });
2322+```
2323+
2324+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2325+
2326+```diff
2327+ Theme.reloadWallpaper(true);
2328+ }
2329+ }
2330++ org.telegram.messenger.multigram.RandomStyle.resetToGeneratedStyle(); // MultiGram: also reset the colours, to this install's generated style (the branch above needs the theme list, which Chat Settings lacks)
2331+ });
2332+ builder1.setNegativeButton(getString("Cancel", R.string.Cancel), null);
2333+ AlertDialog alertDialog = builder1.create();
2334+```
2335+
2336+**Why:** Stock "Reset to defaults" in Chat Settings resets only text size and bubble radius. This returns the colours to the install's generated style. It does nothing when the install has no generated style, or when a custom, cloud or Monet theme is involved.
2337+
2338+**If the code moved:** Make it the last statement of the confirm handler, **outside** the `if (themesHorizontalListCell != null) { ... }` block (that cell is null in Chat Settings). K16 edits another line in the same lambda.
2339+
2340+
2341+##### S13. A tap on the row runs Shuffle
2342+
2343+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2344+
2345+**Where:** Class `ThemeActivity`, method `createView(Context)`: the first statement of the list's item click listener. In Forkgram 12.10.6 the text below starts at line 1105.
2346+
2347+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2348+
2349+```java
2350+ listView.setOnItemClickListener((view, position, x, y) -> {
2351+```
2352+
2353+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2354+
2355+```diff
2356+ frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
2357+ actionBar.setAdaptiveBackground(listView);
2358+ listView.setOnItemClickListener((view, position, x, y) -> {
2359++ if (position == shuffleStyleRow) { org.telegram.messenger.multigram.RandomStyleUi.shuffle(ThemeActivity.this); return; } // MultiGram: "Shuffle my style"
2360+ if (position == enableAnimationsRow) {
2361+ SharedPreferences preferences = MessagesController.getGlobalMainSettings();
2362+ boolean animations = preferences.getBoolean("view_animations", true);
2363+```
2364+
2365+**Why:** A tap runs Shuffle: a new seed and style, applied at once, with an Undo message when the generated style was showing.
2366+
2367+**If the code moved:** First statement of the click listener of ThemeActivity's main list. The check requires that position.
2368+
2369+
2370+##### S14. Draw the row
2371+
2372+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2373+
2374+**Where:** Inner class `ThemeActivity.ListAdapter`, method `onBindViewHolder`, `case TYPE_TEXT_PREFERENCE:`, after `cell.heightDp = 48;`. In Forkgram 12.10.6 the text below starts at line 2668.
2375+
2376+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2377+
2378+```java
2379+ case TYPE_TEXT_PREFERENCE: {
2380+ TextCell cell = (TextCell) holder.itemView;
2381+ cell.heightDp = 48;
2382+```
2383+
2384+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2385+
2386+```diff
2387+ case TYPE_TEXT_PREFERENCE: {
2388+ TextCell cell = (TextCell) holder.itemView;
2389+ cell.heightDp = 48;
2390++ if (position == shuffleStyleRow) { org.telegram.messenger.multigram.RandomStyleUi.bindShuffleRow(cell); break; } // MultiGram: "Shuffle my style"
2391+ if (position == backgroundRow) {
2392+ cell.setSubtitle(null);
2393+ cell.setColors(Theme.key_windowBackgroundWhiteBlueText4, Theme.key_windowBackgroundWhiteBlueText4);
2394+```
2395+
2396+**Why:** Binds the row like "Change chat background": blue text, the dice icon `R.drawable.menu_random`, and the label `R.string.MultiGramShuffleStyle`.
2397+
2398+**If the code moved:** In the binding of the view type that creates a `TextCell` for "Change chat background". The `break` leaves the `switch` case.
2399+
2400+
2401+##### S15. Give the row its view type
2402+
2403+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2404+
2405+**Where:** Inner class `ThemeActivity.ListAdapter`, method `getItemViewType(int)`: the first statement. In Forkgram 12.10.6 the text below starts at line 2732.
2406+
2407+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2408+
2409+```java
2410+ if (position == scheduleFromRow || position == distanceRow ||
2411+```
2412+
2413+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2414+
2415+```diff
2416+
2417+ @Override
2418+ public int getItemViewType(int position) {
2419++ if (position == shuffleStyleRow) return TYPE_TEXT_PREFERENCE; // MultiGram: "Shuffle my style"
2420+ if (position == scheduleFromRow || position == distanceRow ||
2421+ position == scheduleToRow || position == scheduleUpdateLocationRow ||
2422+ position == contactsReimportRow || position == contactsSortRow ||
2423+```
2424+
2425+**Why:** Gives the row the `TextCell` view type, so the adapter creates and binds it.
2426+
2427+**If the code moved:** The file has two `getItemViewType(int position)` methods (in `ThemeAccentsListAdapter` and in `ListAdapter`). Use the one whose body starts with `if (position == scheduleFromRow`.
2428+
2429+
2430+#### 8.3.4 Data
2431+
2432+**The table file.** All numbers are big-endian. Size = 24 + 32 × themes + 40 × records.
2433+
2434+| Part | Size | Content |
2435+|---|---|---|
2436+| Header | 24 bytes | magic `MGST`, version 1, header size 24, theme count, directory entry size 32, record size 40, a zero, record count, CRC-32 of everything after the header |
2437+| Directory | 32 bytes per theme | theme key (20 bytes, UTF-8, padded with zeros), flags (bit 0 = night), first record, record count |
2438+| Record | 40 bytes | 9 colours (accent, bubble, bubble gradient 1 to 3, wallpaper 1 to 4), rotation (a multiple of 45), flags (bit 0 animated bubble gradient, bit 1 pattern motion), a zero. Bubble gradient 2 and 3 and wallpaper 3 and 4 are 0 when unused; bubble gradient 1 equals the bubble for a plain bubble. |
2439+
2440+The themes are, in order: Blue, Arctic Blue and Day (day), then Dark Blue and Night (night). On 4780eb095b the CRC-32 is `884B1657`. `StyleTable.java` accepts only this version and these sizes. A format change means changing `style_table.py`, `StyleTable.java` and `multigram/tools/README.md` together.
2441+
2442+**The pick.** Day index = `(mix64(seed + 1 × 0x9E3779B97F4A7C15) >>> 1) % day count`, and the night index uses 2 in place of 1.
2443+
2444+**Preferences file `rebrand_style`:** `state`, `fresh`, `seed`, `version`, `table_crc`, `day_theme`, `night_theme`, `day_style`, `night_style` (80 hex digits each), `day_index`, `night_index`, `day_accent`, `night_accent`, `generated`. The file `themeconfig` also gets `multigram_generated_accents`.
2445+
2446+#### 8.3.5 Regenerate and verify
2447+
2448+From the root of a checkout:
2449+
2450+```sh
2451+bash multigram/tools/check.sh
2452+python3 multigram/tools/random_style_check.py --base origin/forkgram
2453+python3 multigram/tools/check_style_table.py
2454+python3 multigram/tools/make_style_table.py --check
2455+```
2456+
2457+- `random_style_check.py` prints `random-style: OK`. A failure lists each problem, then `random-style: FAILED, N problem(s)`.
2458+- `check_style_table.py` ends with `style table check: PASS (0 failures, 0 warnings, ...)`. Lines starting with `note:` are expected.
2459+- `make_style_table.py --check` ends with `identical to .../multigram_styles.bin`.
2460+
2461+**When to make a new table.** Run `check.sh` after every update, once the palette fix and this unit are back:
2462+
2463+| `check.sh` says | Meaning | What to do |
2464+|---|---|---|
2465+| `FAIL: <theme>: N entries are unreadable on the current theme sources` | The new sources pushed some styles below the minimum. | Make a new table. If the generator then stalls, the palette fix must change first. |
2466+| `WARNING: <theme>: N entries lost the generator's headroom but still pass WCAG` | The styles still pass, but with less margin. | Make a new table when convenient. |
2467+| `WARNING: make_style_table.py would now generate a different table` (exit status 3) | The theme sources changed since the table was made. CI still passes. | Make a new table when convenient. |
2468+| `FAIL: the style table can no longer be generated on these theme sources (see STALLED above).` (exit status 4) | A colour the styles cannot move fell below its minimum. A new table cannot help. | Fix the palette first (8.2.5). |
2469+| `... update the simulator` | The simulator cannot read the new `Theme.java` or `ThemeColors.java`. | Update `multigram/tools/sim/tgsrc.py`. |
2470+| A Python traceback that ends in `KeyError: 'key_...'` | Upstream renamed a colour key that the simulator names. | Rename it in the simulator and the other MultiGram files (6.2, the `key_...` row). |
2471+
2472+To make a new table (about 40 seconds on 4 cores):
2473+
2474+```sh
2475+python3 multigram/tools/make_style_table.py
2476+bash multigram/tools/check.sh
2477+```
2478+
2479+Then fold the new `multigram_styles.bin` into "Add the style table and its generator" with a fixup (6.3). Installed phones keep the styles they stored. Only later Shuffles draw from the new table.
2480+
2481+**Preview a new Forkgram before replaying.** This checks 64 styles per theme against the plain new snapshot (the folder `/tmp/fk-new` from step 2 of 8.2.5). It models that tree, but reads the table from this checkout:
2482+
2483+```sh
2484+TG_REPO=/tmp/fk-new python3 multigram/tools/check_style_table.py --sample 64
2485+```
2486+
2487+On Forkgram 12.10.6 it ended with `style table check: PASS (0 failures, 0 warnings, 4.5 s)`.
2488+
2489+**On a device** (the full list is "Device test checklist" in `multigram/random-style/README.md`):
2490+
2491+1. Fresh install (or `adb shell pm clear <applicationId>`): the first frame already shows a non-stock accent and a plain gradient wallpaper, with no flash of the Blue theme. With a dark system theme (Android 10 and later), the night style shows.
2492+2. Chat Settings shows "Shuffle my style" under the colour themes. A tap changes the colours at once and offers Undo.
2493+3. Menu > "Reset to defaults" brings the install's generated style back.
2494+4. Share, or "Create new theme", on the generated accent shows the "stays on this device" message and no spinner.
2495+5. Installing over an existing install changes nothing.
2496+6. On a debuggable build, `adb shell run-as <applicationId> cat shared_prefs/rebrand_style.xml` shows `state` (`applied` for a fresh install, `existing` for an old one), a `seed`, and two styles of 80 hex digits.
2497+
2498+#### 8.3.6 Gotchas
2499+
2500+- **Order.** The palette fix must be in the tree first: `RandomStyle` uses `PaletteFix.LAST_STOCK_ACCENT_ID`, and the styles are readable only with the palette fix. The knobs (3b941c01de) come after.
2501+- **Stage A must run before anything loads `Theme`.** `Theme` writes to `themeconfig` on every start, and any key there marks the install as existing. If upstream starts to touch `Theme` earlier, every fresh install silently gets no style. Nothing crashes, so test a fresh install on a device.
2502+- **Stage B has three order rules** inside `Theme`'s static initializer: after all bundled accents are loaded (so new ids start at 101), before `selectedAutoNightType` is read, and before the first `applyTheme(...)`. `random_style_check.py` checks this order.
2503+- **`ThemeActivity.java` has two lines `themeListRow2 = rowCount++;` and two `getItemViewType` methods.** The row goes in the Chat Settings branch, and the view type in `ListAdapter`'s method (S10, S15).
2504+- **The Reset hook (S12) goes after the `if (themesHorizontalListCell != null) { ... }` block**, not inside it: that cell is null in Chat Settings.
2505+- **Every hook is one line ending in `// MultiGram: ...`**, apart from the blank line after S4. The check's placement patterns are strict. If upstream reformats the code around a hook, update `check_placement()` in `random_style_check.py` too.
2506+- **Never merge the binary table in a conflict.** Take either side, finish the source changes, run `make_style_table.py`, and commit its output.
2507+- **The simulator copies Telegram's accent maths by hand.** `tgsrc.py` fails loudly when a structure it reads changes, but a change of logic inside a method passes silently. After each update, read the upstream changes to these files and look for edits inside the engine methods listed in 8.3.2:
2508+
2509+ ```sh
2510+ git diff $(git merge-base origin/multigram origin/forkgram) origin/forkgram -- TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java TMessagesProj/src/main/java/org/telegram/ui/ActionBar/MessageDrawable.java TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java
2511+ ```
2512+
2513+ The `merge-base` is the snapshot `multigram` is built on. That works until `multigram` moves to the new release. Afterwards, put `multigram-before-<version>` in place of `origin/multigram`, with the new release as `<version>`: promote puts that tag on the old tip (4.1).
2514+
2515+- **Exit status 4 (stalled)** cannot be fixed by making a new table. Exit status 3 and the headroom warning only mean the table is older than the sources.
2516+- **Forkgram-only dependencies:** `ThemeInfo.isMonet()`, and `forkgram/SettingsBackup.kt`, which the check reads. Never add `rebrand_style` to `ALLOWED_PREFS` or to `BackupAgent`: by design a seed must not follow the user to another device (see "Backup and settings export" in `multigram/random-style/README.md`).
2517+- **The custom theme tile** is found by its emoji (`RandomStyleUi.CUSTOM_TILE_EMOJI`, which must equal what `EmojiThemes.createPreviewCustom` sets). If upstream changes the emoji, the refresh silently does nothing.
2518+- **`addAccent` relies on `ThemeInfo.getAccent(true)`**: it increments `lastAccentId`, copies the current accent and sets `prevAccentId`. If upstream changes that method, re-check `addAccent`, `removeAccent` and `onAccentCopied` in `RandomStyle.java`.
2519+- **Stage B uses the 6-argument `saveThemeAccents`**, so that no notifications fire inside the static initializer. Every call in the feature must pass `false` for upload; the check enforces it.
2520+- **The five theme names are built into the table.** If upstream renames, adds or removes a bundled theme, the checks fail and Stage B leaves the stock style. Update `DAY_THEMES` and `NIGHT_THEMES` in `style_table.py`, and make a new table.
2521+- **Copying `RandomStyle.java` or `RandomStyleUi.java` from the tip** brings the knob calls with it. Re-apply the knobs (8.4) too, or the build fails on `StyleKnobs`.
2522+- **Testing the first start** needs a fresh install or cleared data. An update over an old build only tests the "existing" path. An Android backup restore counts as a fresh install.
2523+- **The simulator's cache** (`multigram/tools/sim/cache/`) is keyed by the source files. If you change `tgsrc.py`'s parsing, raise `PARSER_VERSION` or delete the cache folder.
2524+
2525+### 8.4 Style knobs and CI self-test (3b941c01de and 4780eb095b)
2526+
2527+#### 8.4.1 What and why
2528+
2529+Commit **3b941c01de** adds `StyleKnobs`. It uses the install's seed from the random style to pick shapes and a chat list layout, using only options the app already has. Each knob gets its own random numbers from `RandomStyle.deriveSeed(seed, "<knob name>")`, so adding a knob never changes the others. The values are stored in the preferences file `rebrand_style_knobs` together with the seed. So an app update never restyles an install; only a new seed (Shuffle or Undo) derives them again.
2530+
2531+Installs without a seed keep every stock value, because each hook returns the stock value it is given. Two knobs are real Forkgram settings (`bubbleRadius` and `useThreeLinesLayout`). They are written only while they still hold the stock value or the value `StyleKnobs` last wrote, so a user's own choice is never overwritten.
2532+
2533+Commit **4780eb095b** adds the rebrand self-test to `check.sh`. A Forkgram release that moves what the rebrand generator changes then fails the sync's compile job, instead of breaking the next rebrand.
2534+
2535+#### 8.4.2 New files
2536+
2537+| Path | Purpose | Upstream names it relies on |
2538+|---|---|---|
2539+| `TMessagesProj/src/main/java/org/telegram/messenger/multigram/StyleKnobs.java` | The whole feature: derives, stores and loads the knob values, writes the two settings, and holds the functions the hooks call. None of them throws, and each returns the stock value when there is no seed. | `SharedConfig.bubbleRadius`, `useThreeLinesLayout`, `fontSize`, `setUseThreeLinesLayout(boolean)`; the `mainconfig` keys `bubbleRadius`, `useThreeLinesLayout`, `fons_size`; `AndroidUtilities.dp(float)`; `Theme.createSimpleSelectorCircleDrawable` and `createSimpleSelectorRoundRectDrawable`; `RandomStyle.hasSeed()`, `getSeed()`, `deriveSeed(long, String)` |
2540+| `multigram/style-knobs/README.md` | Design document: the knobs, why each range is safe, the lifecycle, the hooks, and a 10-item device checklist. | Documentation only |
2541+| `multigram/tools/style_knobs_check.py` | Checks every knob hook (exact text, count, marker, full class name), that the stock lines are gone, the placement, and that `StyleKnobs` writes only its own settings and stays in safe ranges. Prints `style-knobs: OK`. | The paths of the 10 hooked files; the exact stock arguments and indentation of each hooked line; `public void onCreate() {` and `NativeLoader.initNativeLibs(` in `ApplicationLoader`; `reloadWallpaper` and `previousPhase = 0;` in `Theme`; `} else if (id == reset_settings) {` in `ThemeActivity` |
2542+
2543+3b941c01de also changes MultiGram's own `RandomStyle.java` and `RandomStyleUi.java` (hooks K18 to K22), `check.sh` (K23), `multigram/random-style/README.md` and `multigram/tools/README.md`. 4780eb095b changes only `check.sh` (K24).
2544+
2545+#### 8.4.3 Hooks
2546+
2547+K1 to K17 are in upstream files. K18 to K24 are in MultiGram's own files. Section 7 copies those files from each commit, so there their lines are already in place. Use the K18 to K24 blocks to review a file, or to rebuild one by hand.
2548+
2549+##### K1. Start: derive and apply this install's knobs
2550+
2551+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java`
2552+
2553+**Where:** Class `ApplicationLoader`, method `onCreate()`: directly below S1.
2554+
2555+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
2556+
2557+```java
2558+ org.telegram.messenger.multigram.RandomStyle.onApplicationCreate(applicationContext); // MultiGram: per-install style seed, before anything touches Theme
2559+```
2560+
2561+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2562+
2563+```diff
2564+ applicationContext = getApplicationContext();
2565+ }
2566+ org.telegram.messenger.multigram.RandomStyle.onApplicationCreate(applicationContext); // MultiGram: per-install style seed, before anything touches Theme
2567++ org.telegram.messenger.multigram.StyleKnobs.onApplicationCreate(applicationContext); // MultiGram: per-install shapes and chat list layout, before SharedConfig and Theme read them
2568+
2569+ NativeLoader.initNativeLibs(ApplicationLoader.applicationContext);
2570+
2571+```
2572+
2573+**Why:** Runs on every start. Installs without a seed return at once. Otherwise it loads (or, on a fresh install, derives) the knob values, writes the bubble radius and chat list layout settings where they still hold MultiGram's or the stock value, and copies them into `SharedConfig`. On tablets `SharedConfig` is loaded before this point, so the copy is needed there.
2574+
2575+**If the code moved:** Early in `Application.onCreate`, after `applicationContext` is set and after S1 (which creates the seed), before `Theme` or anything that draws. `style_knobs_check.py` requires it on the line directly after S1 and before `NativeLoader.initNativeLibs(`.
2576+
2577+**On plain Telegram (DrKLO):** Applies only on top of S1, because S1's line is its context.
2578+
2579+
2580+##### K2. Wallpaper gradient starts at this install's phase
2581+
2582+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java`
2583+
2584+**Where:** Class `Theme`, method `reloadWallpaper(boolean async)`: after the `if/else` that takes `previousPhase` from the old wallpaper, before `wallpaper = null;`. In Forkgram 12.10.6 the text below starts at line 9268.
2585+
2586+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2587+
2588+```java
2589+ previousPhase = ((MotionBackgroundDrawable) wallpaper).getPhase();
2590+ } else {
2591+ previousPhase = 0;
2592+ }
2593+```
2594+
2595+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2596+
2597+```diff
2598+ } else {
2599+ previousPhase = 0;
2600+ }
2601++ previousPhase = org.telegram.messenger.multigram.StyleKnobs.wallpaperPhase(previousPhase, wallpaper instanceof MotionBackgroundDrawable); // MultiGram: the gradient starts at this install's phase, not always at 0
2602+ wallpaper = null;
2603+ themedWallpaper = null;
2604+ loadWallpaper(async);
2605+```
2606+
2607+**Why:** The generated gradient starts at the install's phase (0 to 7) when there is no old gradient to continue: the first load after start, and once after Shuffle, Undo or Reset. Otherwise the stock behaviour stays, so sending messages still turns the gradient.
2608+
2609+**If the code moved:** Find where the new wallpaper's starting phase is taken from the old wallpaper (`previousPhase`, later passed to `setPhase` in `loadWallpaper`). The hook must see the **old** wallpaper, so it goes before `wallpaper = null`. It belongs in `reloadWallpaper`, not `loadWallpaper`: `LaunchActivity` calls `loadWallpaper` directly, and that returns early when a wallpaper is already loaded.
2610+
2611+
2612+##### K3. Service pill: outer and inner corners
2613+
2614+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java`
2615+
2616+**Where:** Class `ChatActionCell`, method `drawBackground(Canvas, boolean)`, inside `if (invalidatePath) {`, where the path around service and date pills is built. In Forkgram 12.10.6 the text below starts at line 3341.
2617+
2618+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2619+
2620+```java
2621+ final int corner = dp(11);
2622+ final int cornerIn = dp(8);
2623+```
2624+
2625+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2626+
2627+```diff
2628+ backgroundRight = 0;
2629+ lineWidths.clear();
2630+ final int count = textLayout == null ? 0 : textLayout.getLineCount();
2631+- final int corner = dp(11);
2632+- final int cornerIn = dp(8);
2633++ final int corner = org.telegram.messenger.multigram.StyleKnobs.servicePillRadius(dp(11)); // MultiGram: pill corners follow this install's bubble radius
2634++ final int cornerIn = org.telegram.messenger.multigram.StyleKnobs.servicePillRadius(dp(8)); // MultiGram: scaled with the corner
2635+
2636+ int prevLineWidth = 0;
2637+ for (int a = 0; a < count; a++) {
2638+```
2639+
2640+**Why:** Pill corners follow the install's bubble radius: about 0.65 of it, between 4 and 11 dp, never above stock, so pills never cross themselves. It follows the live slider value, but only on installs with a seed.
2641+
2642+**If the code moved:** Find the code that builds the rounded path around service messages and date pills (outer corner 11 dp, inner corners 8 and 6 dp, offset 3 dp). Wrap each corner constant in `servicePillRadius(stock)` and keep K4 in step.
2643+
2644+
2645+##### K4. Service pill: keep the stock side padding
2646+
2647+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java`
2648+
2649+**Where:** Same method and block as K3, just before `final int cornerRest = corner - cornerOffset;`. In Forkgram 12.10.6 the text below starts at line 3370.
2650+
2651+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2652+
2653+```java
2654+ final int cornerOffset = dp(3);
2655+ final int cornerInSmall = dp(6);
2656+ final int cornerRest = corner - cornerOffset;
2657+```
2658+
2659+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2660+
2661+```diff
2662+ int x = getMeasuredWidth() / 2;
2663+ int previousLineBottom = 0;
2664+
2665+- final int cornerOffset = dp(3);
2666+- final int cornerInSmall = dp(6);
2667++ final int cornerOffset = org.telegram.messenger.multigram.StyleKnobs.servicePillCornerOffset(dp(3), corner, dp(11)); // MultiGram: keeps the stock side padding (corner - cornerOffset) when the corner is smaller
2668++ final int cornerInSmall = org.telegram.messenger.multigram.StyleKnobs.servicePillRadius(dp(6)); // MultiGram: scaled with the corner
2669+ final int cornerRest = corner - cornerOffset;
2670+
2671+ lineHeights.clear();
2672+```
2673+
2674+**Why:** The pill's side padding is `corner - cornerOffset` (8 dp in stock). The new offset keeps that padding when the corner is smaller, so the pill keeps its stock size. A negative offset (corner under 8 dp) is intended.
2675+
2676+**If the code moved:** Change the offset together with the corner. Changing only the corner makes the text touch the pill's edge.
2677+
2678+
2679+##### K5. Floating button: shadow follows the shape
2680+
2681+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentFloatingButton.java`
2682+
2683+**Where:** Class `FragmentFloatingButton`, constructor, `if (!isSubButton)` block. In Forkgram 12.10.6 the text below starts at line 73.
2684+
2685+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2686+
2687+```java
2688+ setOutlineProvider(ViewOutlineProviderImpl.BOUNDS_OVAL);
2689+```
2690+
2691+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2692+
2693+```diff
2694+
2695+ ScaleStateListAnimator.apply(this);
2696+ if (!isSubButton) {
2697+- setOutlineProvider(ViewOutlineProviderImpl.BOUNDS_OVAL);
2698++ setOutlineProvider(android.view.ViewOutlineProvider.BACKGROUND); // MultiGram: the shadow follows the background, a circle or this install's rounded square (stock: BOUNDS_OVAL)
2699+ setTranslationZ(dpf2(0.5f));
2700+ }
2701+
2702+```
2703+
2704+**Why:** The shadow follows the background: a circle (as in stock) or the install's rounded square.
2705+
2706+**If the code moved:** Wherever the main chat list button sets its outline, make the outline follow the background. The `ViewOutlineProviderImpl` import becomes unused; leave it.
2707+
2708+
2709+##### K6. Floating button: the small button above it
2710+
2711+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentFloatingButton.java`
2712+
2713+**Where:** Class `FragmentFloatingButton`, method `updateColors()`, branch `if (isSubButton)`. In Forkgram 12.10.6 the text below starts at line 160.
2714+
2715+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2716+
2717+```java
2718+ int rad = dp(18);
2719+ int pressedColor = Theme.getColor(Theme.key_listSelector, resourcesProvider);
2720+```
2721+
2722+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2723+
2724+```diff
2725+ invalidate();
2726+
2727+- int rad = dp(18);
2728++ int rad = org.telegram.messenger.multigram.StyleKnobs.floatingSubButtonRadius(dp(18)); // MultiGram: same shape as the main button
2729++ iBlur3Background.setRadius(rad); // MultiGram: the constructor's dp(18), updated after a Shuffle
2730+ int pressedColor = Theme.getColor(Theme.key_listSelector, resourcesProvider);
2731+ setBackground(Theme.createInsetRoundRectDrawable(pressedColor, rad, dp(6)));
2732+```
2733+
2734+**Why:** The small button gets the main button's shape, scaled by 36/48. Its blurred background (radius set once in the constructor) is updated here, so a Shuffle changes it without rebuilding the view.
2735+
2736+**If the code moved:** Apply the same radius to the selector and to `iBlur3Background`, somewhere that runs again on a theme change. It relies on `BlurredBackgroundDrawable.setRadius(float)`.
2737+
2738+
2739+##### K7. Floating button: circle or rounded square
2740+
2741+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentFloatingButton.java`
2742+
2743+**Where:** Class `FragmentFloatingButton`, method `updateColors()`, the `else` branch (main button). In Forkgram 12.10.6 the text below starts at line 166.
2744+
2745+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2746+
2747+```java
2748+ setBackground(Theme.createSimpleSelectorCircleDrawable(dp(48),
2749+```
2750+
2751+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2752+
2753+```diff
2754+ imageView.setColorFilter(Theme.getColor(Theme.key_chats_actionIcon, resourcesProvider), PorterDuff.Mode.SRC_IN);
2755+ progressView.setProgressColor(Theme.getColor(Theme.key_chats_actionIcon, resourcesProvider));
2756+- setBackground(Theme.createSimpleSelectorCircleDrawable(dp(48),
2757++ setBackground(org.telegram.messenger.multigram.StyleKnobs.floatingButtonBackground(dp(48), // MultiGram: a circle or this install's rounded square (stock: Theme.createSimpleSelectorCircleDrawable)
2758+ Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider),
2759+ Theme.getColor(Theme.key_featuredStickers_addButtonPressed, resourcesProvider)
2760+```
2761+
2762+**Why:** Returns the stock circle or a rounded square (14, 16 or 18 dp) with the same colours. It runs on every theme update, so Shuffle changes it at once.
2763+
2764+**If the code moved:** Find where the main floating button's round background is created (a 48 dp circle with `key_featuredStickers_addButton`) and route it through `floatingButtonBackground(size, color, pressedColor)`.
2765+
2766+
2767+##### K8. Premium gradient button keeps 8 dp corners
2768+
2769+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/Premium/boosts/GradientButtonWithCounterView.java`
2770+
2771+**Where:** Class `GradientButtonWithCounterView`, constructor: the last statement. In Forkgram 12.10.6 the text below starts at line 28.
2772+
2773+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2774+
2775+```java
2776+ flickerDrawable.repeatProgress = 4f;
2777+ }
2778+```
2779+
2780+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2781+
2782+```diff
2783+ flickerDrawable.animationSpeedScale = 1.2f;
2784+ flickerDrawable.drawFrame = false;
2785+ flickerDrawable.repeatProgress = 4f;
2786++ setRoundRadius(8); // MultiGram: stock 8dp corners, which the gradient and shimmer below are drawn with (StyleKnobs.ctaRadiusDp would change only the background and ripple)
2787+ }
2788+
2789+ @Override
2790+```
2791+
2792+**Why:** This button draws its gradient and shimmer with a fixed 8 dp corner. Without this line its inherited background (K13) would not match the gradient.
2793+
2794+**If the code moved:** Any subclass of `ButtonWithCounterView` that draws its own shape at a fixed radius needs `setRoundRadius(<that radius>)` at the end of its constructor. Check with `git grep -n 'extends ButtonWithCounterView'`. Today: `FoundStickerPackButton` calls `setRound()` and is fine; `UpdateReactionsButton` draws nothing of its own; `StickerCutOutBtn` keeps its own 8 dp selector while its loading shimmer follows the knob (not handled; check on a device).
2795+
2796+
2797+##### K9. Reactions: particle clip
2798+
2799+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/Reactions/ReactionsLayoutInBubble.java`
2800+
2801+**Where:** Inner class `ReactionsLayoutInBubble.ReactionButton`, method `drawOverlay(...)`: the particle clip. In Forkgram 12.10.6 the text below starts at line 997.
2802+
2803+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2804+
2805+```java
2806+ AndroidUtilities.rectTmp.set(x, y, x + width, y + height);
2807+ float rad = height / 2f;
2808+```
2809+
2810+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2811+
2812+```diff
2813+ if (!LiteMode.isEnabled(LiteMode.FLAG_ANIMATED_EMOJI_REACTIONS) || !LiteMode.isEnabled(LiteMode.FLAG_PARTICLES)) return false;
2814+
2815+ AndroidUtilities.rectTmp.set(x, y, x + width, y + height);
2816+- float rad = height / 2f;
2817++ float rad = org.telegram.messenger.multigram.StyleKnobs.reactionChipRadius(height / 2f); // MultiGram: pill or this install's rounded chip
2818+
2819+ particles.bounds.set(AndroidUtilities.rectTmp);
2820+ particles.bounds.inset(-dp(4), -dp(4));
2821+```
2822+
2823+**Why:** Selection particles are clipped to the same chip shape as K10 draws.
2824+
2825+**If the code moved:** The line `float rad = height / 2f;` occurs twice in the file. This one follows `rectTmp.set(x, y, x + width, y + height)`. Both must use the same radius.
2826+
2827+
2828+##### K10. Reactions: chip shape
2829+
2830+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/Reactions/ReactionsLayoutInBubble.java`
2831+
2832+**Where:** Inner class `ReactionsLayoutInBubble.ReactionButton`, method `draw(...)`: the chip background. In Forkgram 12.10.6 the text below starts at line 1109.
2833+
2834+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2835+
2836+```java
2837+ canvas.scale(bounceScale, bounceScale, x + w / 2f, y + height / 2f);
2838+ }
2839+ float rad = height / 2f;
2840+```
2841+
2842+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2843+
2844+```diff
2845+ canvas.save();
2846+ canvas.scale(bounceScale, bounceScale, x + w / 2f, y + height / 2f);
2847+ }
2848+- float rad = height / 2f;
2849++ float rad = org.telegram.messenger.multigram.StyleKnobs.reactionChipRadius(height / 2f); // MultiGram: pill or this install's rounded chip
2850+ if (getDrawServiceShaderBackground() > 0 && !drawBgOnlyIfChosen) {
2851+ Paint paint1 = Theme.getThemePaint(Theme.key_paint_chatActionBackground, resourcesProvider);
2852+ Paint paint2 = Theme.getThemePaint(Theme.key_paint_chatActionBackgroundDarken, resourcesProvider);
2853+```
2854+
2855+**Why:** Returns the stock pill radius, or a rounded chip of about 0.6 of the bubble radius (6 to 10 dp). Saved-message tags keep their notched shape.
2856+
2857+**If the code moved:** Find where the reaction chip under messages gets its corner radius (stock: half its height) and wrap it in `reactionChipRadius(stock)`.
2858+
2859+
2860+##### K11. Settings cards: corner radius (RecyclerListView)
2861+
2862+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java`
2863+
2864+**Where:** Class `RecyclerListView`, the default overloads `setSections()` and `setSections(boolean topPadding)`. In Forkgram 12.10.6 the text below starts at line 3309.
2865+
2866+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2867+
2868+```java
2869+ public void setSections() {
2870+ setSections(dp(12), dp(16), false);
2871+ }
2872+```
2873+
2874+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2875+
2876+```diff
2877+ }
2878+
2879+ public void setSections() {
2880+- setSections(dp(12), dp(16), false);
2881++ setSections(dp(12), org.telegram.messenger.multigram.StyleKnobs.sectionRadius(dp(16)), false); // MultiGram: this install's card corners
2882+ }
2883+ public void setSections(boolean topPadding) {
2884+- setSections(dp(12), dp(16), topPadding);
2885++ setSections(dp(12), org.telegram.messenger.multigram.StyleKnobs.sectionRadius(dp(16)), topPadding); // MultiGram: this install's card corners
2886+ }
2887+ public void setSections(int padding, float roundRadius, boolean topPadding) {
2888+ setSections(
2889+```
2890+
2891+**Why:** Sets the default corner radius of settings cards to 12, 14, 16 or 20 dp (stock 16). A screen reads it when it builds its list, so after a Shuffle each screen changes the next time it opens.
2892+
2893+**If the code moved:** Wrap the default card radius in `sectionRadius(stock)`. Calls that pass their own radius are left alone (there are none today). Find new copies with `git grep -n 'setSections(dp(12), dp(16)'`.
2894+
2895+
2896+##### K12. Settings cards: corner radius (UniversalRecyclerView)
2897+
2898+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Components/UniversalRecyclerView.java`
2899+
2900+**Where:** Class `UniversalRecyclerView`, its own copies of the two default `setSections` overloads. In Forkgram 12.10.6 the text below starts at line 430.
2901+
2902+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2903+
2904+```java
2905+ public void setSections() {
2906+ setSections(dp(12), dp(16), false);
2907+ }
2908+```
2909+
2910+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2911+
2912+```diff
2913+ }
2914+
2915+ public void setSections() {
2916+- setSections(dp(12), dp(16), false);
2917++ setSections(dp(12), org.telegram.messenger.multigram.StyleKnobs.sectionRadius(dp(16)), false); // MultiGram: this install's card corners, as in RecyclerListView
2918+ }
2919+ public void setSections(boolean topPadding) {
2920+- setSections(dp(12), dp(16), topPadding);
2921++ setSections(dp(12), org.telegram.messenger.multigram.StyleKnobs.sectionRadius(dp(16)), topPadding); // MultiGram: this install's card corners, as in RecyclerListView
2922+ }
2923+ public void setSections(int padding, float roundRadius, boolean topPadding) {
2924+ super.setSections(
2925+```
2926+
2927+**Why:** `UniversalRecyclerView` repeats both default overloads with its own `dp(16)`. It is used by the main Settings screen, Forkgram's settings and others, so hooking `RecyclerListView` alone is not enough.
2928+
2929+**If the code moved:** Every class that repeats the default overloads with a fixed radius needs the same change. `git grep -n 'setSections(dp(12), dp(16)'` should print nothing on a finished tree.
2930+
2931+
2932+##### K13. Buttons: default corner radius
2933+
2934+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Stories/recorder/ButtonWithCounterView.java`
2935+
2936+**Where:** Class `ButtonWithCounterView`, the field `radiusDp`. In Forkgram 12.10.6 the text below starts at line 44.
2937+
2938+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2939+
2940+```java
2941+ private int radiusDp = 8;
2942+```
2943+
2944+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2945+
2946+```diff
2947+
2948+ private Theme.ResourcesProvider resourcesProvider;
2949+
2950+- private int radiusDp = 8;
2951++ private int radiusDp = org.telegram.messenger.multigram.StyleKnobs.ctaRadiusDp(8); // MultiGram: this install's button corners (explicit setRoundRadius/setRound still win)
2952+
2953+ private final Paint paint;
2954+ public final AnimatedTextView.AnimatedTextDrawable text;
2955+```
2956+
2957+**Why:** The default corner radius of the filled buttons in sheets and many screens: 50, 75 or 100 % of the bubble radius, between 6 and 12 dp. Buttons that call `setRound()` or `setRoundRadius(n)` keep their values.
2958+
2959+**If the code moved:** Find the default corner radius of this shared button. It must stay a field initialiser (or be set before the constructor's first `setBackground`), so the constructor's background uses it too.
2960+
2961+
2962+##### K14. Buttons: the constructor uses the field
2963+
2964+**File:** `TMessagesProj/src/main/java/org/telegram/ui/Stories/recorder/ButtonWithCounterView.java`
2965+
2966+**Where:** Class `ButtonWithCounterView`, constructor, `if (filled)`. In Forkgram 12.10.6 the text below starts at line 115.
2967+
2968+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2969+
2970+```java
2971+ setBackground(Theme.createRoundRectDrawable(dp(8), backgroundColor = Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider)));
2972+```
2973+
2974+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
2975+
2976+```diff
2977+ addView(rippleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
2978+
2979+ if (filled) {
2980+- setBackground(Theme.createRoundRectDrawable(dp(8), backgroundColor = Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider)));
2981++ setBackground(Theme.createRoundRectDrawable(dp(radiusDp), backgroundColor = Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider))); // MultiGram: radiusDp (stock: dp(8), the same default)
2982+ }
2983+
2984+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
2985+```
2986+
2987+**Why:** The constructor had a fixed `dp(8)` instead of the field. Using `radiusDp` makes the first background match the ripple and loading shimmer. Stock behaviour is the same, because the stock default is 8.
2988+
2989+**If the code moved:** Any place in this class that draws the default shape with a literal 8 instead of `radiusDp` needs the same change.
2990+
2991+
2992+##### K15. Chat Settings: rebind the rows the knobs change
2993+
2994+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
2995+
2996+**Where:** Class `ThemeActivity`: a new public one-line method (and a blank line) before `setBubbleRadius(int, boolean)`. In Forkgram 12.10.6 the text below starts at line 463.
2997+
2998+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
2999+
3000+```java
3001+ private boolean setBubbleRadius(int size, boolean layout) {
3002+```
3003+
3004+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3005+
3006+```diff
3007+ updateRows(true);
3008+ }
3009+
3010++ public void refreshStyleKnobRows() { if (listAdapter != null) { for (int row : new int[] {textSizeRow, bubbleRadiusRow, chatListRow}) { if (row >= 0) listAdapter.notifyItemChanged(row); } } } // MultiGram: Shuffle and Undo change the bubble radius and chat list layout these rows show (StyleKnobs); the rebind updates their cells (onBindViewHolder)
3011++
3012+ private boolean setBubbleRadius(int size, boolean layout) {
3013+ if (size != SharedConfig.bubbleRadius) {
3014+ SharedConfig.bubbleRadius = size;
3015+```
3016+
3017+**Why:** After Shuffle and Undo, `RandomStyleUi` calls this. It asks the list to rebind the text size preview, the message corners slider and the chat list picker, so they show the new radius and layout.
3018+
3019+**If the code moved:** It must be public (it is called from another package). It relies on the fields `textSizeRow`, `bubbleRadiusRow`, `chatListRow` and `listAdapter`. Place it anywhere in the outer class; the check wants the one-line form.
3020+
3021+
3022+##### K16. "Reset to defaults" uses this install's radius
3023+
3024+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
3025+
3026+**Where:** Class `ThemeActivity`, the `reset_settings` confirm lambda (the same lambda as S12). In Forkgram 12.10.6 the text below starts at line 1013.
3027+
3028+**Find this** (it occurs once in the file; text from Forkgram 12.10.6):
3029+
3030+```java
3031+ if (setBubbleRadius(17, true)) {
3032+```
3033+
3034+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3035+
3036+```diff
3037+ if (setFontSize(AndroidUtilities.isTablet() ? 18 : 16)) {
3038+ changed = true;
3039+ }
3040+- if (setBubbleRadius(17, true)) {
3041++ if (setBubbleRadius(org.telegram.messenger.multigram.StyleKnobs.resetBubbleRadius(17), true)) { // MultiGram: this install's radius, not always 17
3042+ changed = true;
3043+ }
3044+ if (changed) {
3045+```
3046+
3047+**Why:** Reset returns the bubble radius to the install's value instead of 17, and marks it as MultiGram's again. With no seed it returns 17.
3048+
3049+**If the code moved:** Find the Chat Settings reset of the bubble radius to 17 and pass 17 through `resetBubbleRadius`. The check requires it inside the `} else if (id == reset_settings) {` branch.
3050+
3051+
3052+##### K17. Chat Settings: bind the three knob rows
3053+
3054+**File:** `TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java`
3055+
3056+**Where:** Inner class `ThemeActivity.ListAdapter`, method `onBindViewHolder(RecyclerView.ViewHolder, int)`: the first case of the `switch`. In Forkgram 12.10.6 the text below starts at line 2471.
3057+
3058+**Find this** (its first line occurs twice in the file, the whole block once; text from Forkgram 12.10.6):
3059+
3060+```java
3061+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
3062+ switch (holder.getItemViewType()) {
3063+```
3064+
3065+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3066+
3067+```diff
3068+ @Override
3069+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
3070+ switch (holder.getItemViewType()) {
3071++ case TYPE_TEXT_SIZE: case TYPE_BUBBLE_RADIUS: case TYPE_CHAT_LIST: org.telegram.messenger.multigram.RandomStyleUi.bindStyleKnobRow(holder.itemView); break; // MultiGram: these cells read the bubble radius and chat list layout only when created or measured; Shuffle and Undo change them (StyleKnobs)
3072+ case TYPE_TEXT_SETTING: {
3073+ TextSettingsCell cell = (TextSettingsCell) holder.itemView;
3074+ if (position == nightThemeRow) {
3075+```
3076+
3077+**Why:** In stock these three view types are never bound: their cells read the settings only when created or measured. This brings a visible cell up to date after Shuffle or Undo.
3078+
3079+**If the code moved:** `TYPE_TEXT_SIZE`, `TYPE_BUBBLE_RADIUS` and `TYPE_CHAT_LIST` are private to `ListAdapter`, so the line must be inside `ListAdapter`. The file has two `onBindViewHolder` methods; the 2-line block shown occurs once. If a future version adds its own `case` for one of these types, call `bindStyleKnobRow` from that case instead: duplicate case labels do not compile.
3080+
3081+
3082+##### K18. RandomStyle.shuffle: apply the knobs before the switch
3083+
3084+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyle.java`
3085+
3086+**Where:** MultiGram's own file. Method `shuffle(Context)`, just before `show(pair);`.
3087+
3088+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3089+
3090+```java
3091+ undo = previous;
3092+ show(pair);
3093+```
3094+
3095+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3096+
3097+```diff
3098+ previous.shuffledSeed = seed;
3099+ }
3100+ undo = previous;
3101++ StyleKnobs.onStyleApplying(seed); // before the theme switch, which reloads the wallpaper at once
3102+ show(pair);
3103+ notifyListeners(seed, REASON_SHUFFLE);
3104+ return true;
3105+```
3106+
3107+**Why:** Derives every knob from the new seed and applies the two settings before the theme switch. The switch reloads the wallpaper at once, so K2 must already see the new phase.
3108+
3109+The commit also adds two lines to the class description at the top of the file, after the list item that ends in `(stock Chat Settings only resets text size and bubble radius).</li>`:
3110+
3111+```diff
3112++ * <li>{@link StyleKnobs} (shapes and chat list layout from the same seed) is updated right before Shuffle, Undo and
3113++ * Reset show the style, because the theme switch reloads the wallpaper synchronously; listeners run after.</li>
3114+```
3115+
3116+**If the code moved:** Keep the call directly before `show(pair);`; `style_knobs_check.py` checks it. A `RandomStyle.Listener` would be too late, because listeners run after `show`.
3117+
3118+
3119+##### K19. RandomStyle.undoShuffle: same
3120+
3121+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyle.java`
3122+
3123+**Where:** MultiGram's own file. Method `undoShuffle()`, just before `show(pair);`.
3124+
3125+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3126+
3127+```java
3128+ putPair(editor, pair).commit();
3129+ show(pair);
3130+ notifyListeners(u.seed, REASON_UNDO);
3131+```
3132+
3133+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3134+
3135+```diff
3136+ putStyle(editor, DAY, u.day, u.dayIndex);
3137+ putStyle(editor, NIGHT, u.night, u.nightIndex);
3138+ putPair(editor, pair).commit();
3139++ StyleKnobs.onStyleApplying(u.seed);
3140+ show(pair);
3141+ notifyListeners(u.seed, REASON_UNDO);
3142+ return true;
3143+```
3144+
3145+**Why:** Undo restores the previous seed, and the knobs follow it.
3146+
3147+**If the code moved:** Directly before `show(pair);`, with the restored seed `u.seed`.
3148+
3149+
3150+##### K20. RandomStyle.resetToGeneratedStyle: same
3151+
3152+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyle.java`
3153+
3154+**Where:** MultiGram's own file. Method `resetToGeneratedStyle()`, just before `show(pair);`.
3155+
3156+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3157+
3158+```java
3159+ putPair(p.edit(), pair).commit();
3160+ show(pair);
3161+```
3162+
3163+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3164+
3165+```diff
3166+ return false;
3167+ }
3168+ putPair(p.edit(), pair).commit();
3169++ StyleKnobs.onStyleApplying(p.getLong(KEY_SEED, 0));
3170+ show(pair);
3171+ notifyListeners(p.getLong(KEY_SEED, 0), REASON_RESET);
3172+ return true;
3173+```
3174+
3175+**Why:** Reset keeps the seed; this applies the two settings again and restarts the wallpaper at the install's phase.
3176+
3177+**If the code moved:** Directly before `show(pair);`, with `p.getLong(KEY_SEED, 0)`.
3178+
3179+
3180+##### K21. RandomStyleUi: imports
3181+
3182+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyleUi.java`
3183+
3184+**Where:** MultiGram's own file. The import block.
3185+
3186+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3187+
3188+```java
3189+import android.os.SystemClock;
3190+```
3191+
3192+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3193+
3194+```diff
3195+ package org.telegram.messenger.multigram;
3196+
3197+ import android.os.SystemClock;
3198++import android.view.View;
3199++import android.view.ViewGroup;
3200+
3201+ import org.telegram.messenger.ApplicationLoader;
3202+ import org.telegram.messenger.FileLog;
3203++import org.telegram.messenger.MessageObject;
3204+ import org.telegram.messenger.R;
3205++import org.telegram.messenger.SharedConfig;
3206+ import org.telegram.messenger.Utilities;
3207+ import org.telegram.ui.ActionBar.BaseFragment;
3208+ import org.telegram.ui.ActionBar.EmojiThemes;
3209+ import org.telegram.ui.ActionBar.Theme;
3210++import org.telegram.ui.Cells.ChatListCell;
3211++import org.telegram.ui.Cells.ChatMessageCell;
3212+ import org.telegram.ui.Cells.TextCell;
3213++import org.telegram.ui.Cells.ThemePreviewMessagesCell;
3214+ import org.telegram.ui.Components.BulletinFactory;
3215+ import org.telegram.ui.Components.ChatThemeBottomSheet;
3216++import org.telegram.ui.Components.RadioButton;
3217+ import org.telegram.ui.DialogsActivity;
3218++import org.telegram.ui.ThemeActivity;
3219+
3220++import java.util.ArrayList;
3221+ import java.util.List;
3222+
3223+ /**
3224+```
3225+
3226+**Why:** Imports for K22.
3227+
3228+**If the code moved:** MultiGram's own file: copy it from the commit. The upstream classes these imports name are listed in K22.
3229+
3230+
3231+##### K22. RandomStyleUi: refresh after Shuffle and Undo, and the bind helper
3232+
3233+**File:** `TMessagesProj/src/main/java/org/telegram/messenger/multigram/RandomStyleUi.java`
3234+
3235+**Where:** MultiGram's own file. Method `shuffle(BaseFragment)` (two calls), and three new methods before the javadoc of the `DefaultThemesPreviewCell.updateDayNightMode` hook.
3236+
3237+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3238+
3239+```java
3240+ if (fragment != null && RandomStyle.canUndoShuffle()) {
3241+```
3242+
3243+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3244+
3245+```diff
3246+ }
3247+ return;
3248+ }
3249++ refreshStyleKnobRows(fragment);
3250+ if (fragment != null && RandomStyle.canUndoShuffle()) {
3251+ BulletinFactory.of(fragment).createUndoBulletin(str(R.string.MultiGramStyleShuffled), () -> {
3252+ if (!RandomStyle.undoShuffle()) {
3253+ BulletinFactory.of(fragment).createErrorBulletin(str(R.string.MultiGramUndoFailed)).show();
3254++ } else {
3255++ refreshStyleKnobRows(fragment);
3256+ }
3257+ }, null).show();
3258+ }
3259+ }
3260+
3261++ /**
3262++ * Shuffle and Undo may change the bubble radius and the chat list layout ({@link StyleKnobs}); Chat Settings
3263++ * rebinds the rows that show them ({@link #bindStyleKnobRow}).
3264++ */
3265++ private static void refreshStyleKnobRows(BaseFragment fragment) {
3266++ try {
3267++ if (fragment instanceof ThemeActivity) {
3268++ ((ThemeActivity) fragment).refreshStyleKnobRows();
3269++ }
3270++ } catch (Throwable e) {
3271++ FileLog.e(e);
3272++ }
3273++ }
3274++
3275++ /**
3276++ * ThemeActivity.ListAdapter.onBindViewHolder hook for the text size preview, the message corners slider and the
3277++ * chat list picker. Their cells read the bubble radius and chat list layout only when created (the picker's
3278++ * radio buttons) or measured (the slider's position, the preview's bubbles), and binding them does nothing in
3279++ * stock, so after Shuffle or Undo ({@link ThemeActivity#refreshStyleKnobRows}) a visible row, or one kept
3280++ * off screen, would still show the old values. This brings the cell in line with SharedConfig the way the
3281++ * stock slider does: the preview messages are laid out again, the cell is measured again, and the picker's
3282++ * radio buttons are re-checked. A cell never laid out was just created from the current values and is left
3283++ * alone, and so is every cell while the install has no style knobs. Never throws.
3284++ */
3285++ public static void bindStyleKnobRow(View itemView) {
3286++ try {
3287++ if (itemView == null || !StyleKnobs.isActive() || itemView.getWidth() == 0) {
3288++ return;
3289++ }
3290++ if (itemView instanceof ChatListCell) {
3291++ // Its two options in order (two lines, three lines), each with one radio button; a button animates
3292++ // only while attached, and does nothing when it already shows the value.
3293++ List<RadioButton> buttons = new ArrayList<>();
3294++ collect(itemView, RadioButton.class, buttons);
3295++ if (buttons.size() == 2) {
3296++ buttons.get(0).setChecked(!SharedConfig.useThreeLinesLayout, true);
3297++ buttons.get(1).setChecked(SharedConfig.useThreeLinesLayout, true);
3298++ }
3299++ } else {
3300++ List<ThemePreviewMessagesCell> previews = new ArrayList<>();
3301++ collect(itemView, ThemePreviewMessagesCell.class, previews);
3302++ for (ThemePreviewMessagesCell preview : previews) {
3303++ ChatMessageCell[] cells = preview.getCells();
3304++ for (int i = 0; cells != null && i < cells.length; i++) {
3305++ MessageObject message = cells[i] == null ? null : cells[i].getMessageObject();
3306++ if (message != null) {
3307++ message.resetLayout();
3308++ cells[i].requestLayout();
3309++ }
3310++ }
3311++ }
3312++ itemView.requestLayout(); // the corners slider takes its position from SharedConfig in onMeasure
3313++ }
3314++ itemView.invalidate();
3315++ } catch (Throwable e) {
3316++ FileLog.e(e);
3317++ }
3318++ }
3319++
3320++ /** Adds view and its descendants that are instances of type to out, depth first. */
3321++ private static <T> void collect(View view, Class<T> type, List<T> out) {
3322++ if (type.isInstance(view)) {
3323++ out.add(type.cast(view));
3324++ }
3325++ if (view instanceof ViewGroup) {
3326++ ViewGroup group = (ViewGroup) view;
3327++ for (int i = 0; i < group.getChildCount(); i++) {
3328++ collect(group.getChildAt(i), type, out);
3329++ }
3330++ }
3331++ }
3332++
3333+ /**
3334+ * DefaultThemesPreviewCell.updateDayNightMode hook. The custom tile of Chat Settings' theme strip is built once,
3335+ * from themeconfig's lastDay/DarkCustomTheme(AccentId), but Shuffle, Undo and Reset replace the accents it points
3336+```
3337+
3338+**Why:** After Shuffle, and after a successful Undo, Chat Settings rebinds its three knob rows (K15). `bindStyleKnobRow` is the body of K17: it re-checks the chat list picker's radio buttons, lays the preview messages out again and re-measures the slider.
3339+
3340+**If the code moved:** MultiGram's own file. The check requires exactly two `refreshStyleKnobRows(fragment);` calls. Upstream names it relies on: `ThemeActivity.refreshStyleKnobRows` (K15), `BaseFragment`, `ChatListCell` (two options, one `RadioButton` each, in the order two lines, three lines), `RadioButton.setChecked(boolean, boolean)`, `ThemePreviewMessagesCell.getCells()`, `ChatMessageCell.getMessageObject()`, `MessageObject.resetLayout()`, and the slider cell reading `SharedConfig.bubbleRadius` in `onMeasure`. If `ChatListCell` gains a third option, the `buttons.size() == 2` guard makes this do nothing; adjust it.
3341+
3342+
3343+##### K23. check.sh: run the knobs check
3344+
3345+**File:** `multigram/tools/check.sh`
3346+
3347+**Where:** MultiGram's own file. After the random style block.
3348+
3349+**Find this** (it occurs once in the file; text from the file as commit 27d32d57b4 left it):
3350+
3351+```sh
3352+ python3 multigram/tools/random_style_check.py
3353+fi
3354+```
3355+
3356+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3357+
3358+```diff
3359+ python3 multigram/tools/random_style_check.py
3360+ fi
3361+
3362++if [ -f multigram/tools/style_knobs_check.py ]; then
3363++ # Per-install shapes and chat list layout: hooks and their placement, owned settings only, safe ranges.
3364++ echo "== style knobs: hooks, owned settings and safe ranges"
3365++ python3 multigram/tools/style_knobs_check.py
3366++fi
3367++
3368+ echo "== style table: format and readability on the current theme sources"
3369+ python3 multigram/tools/check_style_table.py
3370+
3371+```
3372+
3373+**Why:** CI runs `style_knobs_check.py` on every build and every sync candidate.
3374+
3375+**If the code moved:** Order does not matter. The `[ -f ... ]` guard lets older trees without the file pass.
3376+
3377+
3378+##### K24. check.sh: run the rebrand self-test (commit 4780eb095b)
3379+
3380+**File:** `multigram/tools/check.sh`
3381+
3382+**Where:** MultiGram's own file. After the style knobs block, before the style table checks.
3383+
3384+**Find this** (it occurs once in the file; text from the file as commit 3b941c01de left it):
3385+
3386+```sh
3387+ python3 multigram/tools/style_knobs_check.py
3388+fi
3389+```
3390+
3391+**Change it to** (lines starting with `-` go, lines starting with `+` are added, the rest stays):
3392+
3393+```diff
3394+ python3 multigram/tools/style_knobs_check.py
3395+ fi
3396+
3397++if [ -f multigram/rebrand/selftest.py ]; then
3398++ # Rebrand toolkit: generate two identities on a scratch copy, check the output, then --clean back to the commit.
3399++ echo "== rebrand toolkit: self-test"
3400++ python3 multigram/rebrand/selftest.py
3401++fi
3402++
3403+ echo "== style table: format and readability on the current theme sources"
3404+ python3 multigram/tools/check_style_table.py
3405+
3406+```
3407+
3408+**Why:** Runs the rebrand self-test in CI. A Forkgram release that moves what the rebrand generator changes (the account type code, app name strings, launcher, splash or notification icons) then fails the sync's compile job, instead of breaking the next rebrand.
3409+
3410+**If the code moved:** Order does not matter. The self-test copies part of the tree to a throwaway folder in the system temp folder and removes it afterwards. It needs `git`; `keytool` is optional.
3411+
3412+
3413+#### 8.4.4 Data
3414+
3415+The knob tables are Java constants in `StyleKnobs.java`. "r" is the bubble radius in dp.
3416+
3417+| Knob (its name in `StyleKnobs.java`) | Values | Stock | What it drives | Hooks |
3418+|---|---|---|---|---|
3419+| `bubble-radius` | 8, 10, 12, 14, 17 dp | 17 | Setting `bubbleRadius` (Chat Settings > Message corners). "Reset to defaults" returns to it. | K1, K16, K18–K20 |
3420+| `chat-list-density` | three lines in 2 of 5 cases, only when the screen's long side is at least 640 dp and the font size is below 19 | two lines | Setting `useThreeLinesLayout` (Chat Settings > chat list view) | K1, K18–K20 |
3421+| (none: follows the live `bubbleRadius`) | outer corner round(r × 0.65), between 4 and 11 dp | 11 dp | Service and date pills | K3, K4 |
3422+| `reaction-chip-shape` | pill, or rounded: round(r × 0.6), between 6 and 10 dp | pill | Reaction chips under messages | K9, K10 |
3423+| `cta-button-radius` | 50, 75 or 100 % of r, between 6 and 12 dp | 8 dp | Default corners of filled buttons | K8, K13, K14 |
3424+| `fab-shape` | circle (3 of 6 cases), or a rounded square of 14, 16 or 18 dp | circle | Chat list floating button, its shadow, and the small button above it (× 36/48) | K5–K7 |
3425+| `settings-cards` | 12, 14, 16, 20 dp | 16 dp | Default corners of settings cards | K11, K12 |
3426+| `wallpaper-phase` | 0 to 7 | continues the old phase | Where the generated wallpaper gradient starts | K2 |
3427+
3428+**Preferences file `rebrand_style_knobs`:** `seed`, `version` (1), `bubble_radius`, `three_lines`, `reaction_pill`, `cta_scale`, `fab_radius`, `section_radius`, `wallpaper_phase`, and `set_bubble_radius` and `set_three_lines` (the values `StyleKnobs` last wrote).
3429+
3430+#### 8.4.5 Regenerate and verify
3431+
3432+Nothing is generated in this unit. To verify, from the root of a checkout:
3433+
3434+```sh
3435+python3 multigram/tools/style_knobs_check.py
3436+python3 multigram/tools/style_knobs_check.py --base origin/forkgram
3437+bash multigram/tools/check.sh
3438+```
3439+
3440+- Both checks print `style-knobs: OK`. With `--base`, every non-blank line added to the 10 hooked files must carry `MultiGram:`.
3441+- A failure lists each problem, then `style-knobs: FAILED, N problem(s)`. For example, with K3's first line put back to stock: `style-knobs: TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java: stock line 'final int corner = dp\\(11\\);' is back (the hook must replace it)`. Patterns are printed as Python strings, so each backslash shows doubled.
3442+- `check.sh` runs the knobs check and the rebrand self-test, and ends with `== all MultiGram checks passed`. On 4780eb095b it took 1 minute 8 seconds on 4 cores.
3443+
3444+**On a device** (the full list is "Device test checklist" in `multigram/style-knobs/README.md`). Knobs work only on installs with a seed: a fresh install, or an old install after "Shuffle my style".
3445+
3446+1. `adb shell run-as <applicationId> cat shared_prefs/rebrand_style_knobs.xml` (debuggable build) shows the keys above.
3447+2. From the first frame, on a phone and on a tablet, Chat Settings shows the install's corner radius and chat list layout.
3448+3. With a small radius, pills have smaller corners but the stock size. Reactions are pills or rounded chips, with particles inside. Sheet buttons have 6 to 12 dp corners. The floating button is a circle or a rounded square, with a matching shadow. Settings cards have the install's corners. The wallpaper gradient starts at the same phase on every start.
3449+4. Shuffle, then Undo: the preview, slider and chat list picker update at once. Move the slider, then Shuffle: your value stays. "Reset to defaults" brings back the install's radius, not 17.
3450+5. Avatars keep Forkgram's own shape setting.
3451+
3452+#### 8.4.6 Gotchas
3453+
3454+- **Apply this unit after 27d32d57b4.** K1's context is S1's line, so K1 does not apply on plain Forkgram.
3455+- **Keep every upstream hook one line**, with the full class name `org.telegram.messenger.multigram.StyleKnobs.` and the exact `// MultiGram: ` marker, at the stock indentation. The check matches these exactly, including leading spaces and stock arguments. The now unused import `ViewOutlineProviderImpl` in `FragmentFloatingButton.java` stays on purpose.
3456+- **Start order.** `StyleKnobs.onApplicationCreate` must come after `RandomStyle.onApplicationCreate` and before anything draws. On tablets, `SharedConfig` is already loaded at that point, so `StyleKnobs` copies the two values into it. Without that copy, tablets show stock values until the next start.
3457+- **K2 must read the old wallpaper,** so it goes before `wallpaper = null`, and in `reloadWallpaper`, not `loadWallpaper`.
3458+- **K3 and K4 go together.** The pill's side padding is corner minus offset. Changing only the corner puts the text against the edge.
3459+- **`ReactionsLayoutInBubble.java` has two identical lines `float rad = height / 2f;`.** Hook both (K9, K10).
3460+- **`ButtonWithCounterView` needs both K13 and K14.** Subclasses that draw a fixed radius must opt out, as `GradientButtonWithCounterView` does (K8). Today: `FoundStickerPackButton` calls `setRound()` and is fine; `UpdateReactionsButton` draws nothing of its own; `StickerCutOutBtn` keeps its own 8 dp selector while its loading shimmer follows the knob (not handled; check it on a device). After an update, run `git grep -n 'extends ButtonWithCounterView'`.
3461+- **`UniversalRecyclerView` repeats the `setSections` overloads.** Hooking only `RecyclerListView` misses the main Settings screen.
3462+- **K17 must stay inside `ListAdapter`**, because the three view types are private to it. If upstream adds its own `case` for one of them, call `bindStyleKnobRow` from there instead: duplicate case labels do not compile. `refreshStyleKnobRows` (K15) must be public.
3463+- **K18 to K20 must come right before `show(pair);`.** The theme switch reloads the wallpaper at once, so a listener would be too late.
3464+- **Never rename a knob name** (the `KNOB_*` constants): that re-rolls the knob for every new seed. Keep the tables inside the check's ranges.
3465+- **Nothing changes on an existing install until it presses Shuffle.** Test on a fresh install.
3466+- **Never add Forkgram's avatar settings** (`avatarCorners`, `squareAvatars`) to the knobs. The check fails on them.
3467+- **The rebrand self-test in `check.sh`** takes about a third of the script's time (about 26 seconds on 4 cores; `make_style_table.py --check` takes the most, about 30 seconds). It writes a temporary folder in the system temp folder (and removes it), and skips the signing checks without `keytool`.
3468+
3469+## 9. Plain Telegram (DrKLO)
3470+
3471+This section says what happens if you put the stack on Telegram's own code (DrKLO/Telegram) instead of Forkgram. It was tested by applying the six exported patches (5.7) with `git am -3`, one at a time, to DrKLO dc780e81e ("update to 12.10.5 (7105)"), the Telegram base of Forkgram 12.10.6.
3472+
3473+### 9.1 What applies
3474+
3475+| Commit | Applies? | Conflicts | Forkgram-only dependencies |
3476+|---|---|---|---|
3477+| d457ac74ae rebrand | No: `git am -3` stops with three conflicts. The new files and hook R1 go in without trouble. | `DialogsActivity.java` (hook R2: Forkgram's custom title code does not exist); `ForkSettingsActivity.java` (the file does not exist); `TMessagesProj_App/build.gradle` (DrKLO's file ends with `apply plugin: 'com.google.gms.google-services'`: keep both lines) | `forkCustomTitle` and `ForkSettingsActivity`; the generator's icon names and file types |
3478+| 5b53859682 palette | Yes, cleanly. `palette_fix_check.py` passes. | none | `ThemeInfo.isMonet()` in `PaletteFix.java`: a compile error |
3479+| cddd62df98 style table | Yes, cleanly. The table checks pass, and `make_style_table.py --check` finds the table identical. | none | none: `ThemeColors.java` and the five `.attheme` files are the same in both |
3480+| 27d32d57b4 random style | Yes, cleanly. Every hook lands where the check expects it. | none | `ThemeInfo.isMonet()` in `RandomStyle.java` (a compile error); `random_style_check.py` crashes because `forkgram/SettingsBackup.kt` is missing |
3481+| 3b941c01de knobs | Yes, cleanly. `style_knobs_check.py` passes. | none | none |
3482+| 4780eb095b CI | Yes, cleanly. | none | Indirect: `check.sh` stops at the random style check, and the rebrand self-test fails on DrKLO's icons (9.2, item 2). |
3483+
3484+So the text applies almost completely, but the result does not build or pass `check.sh` as it is.
3485+
3486+### 9.2 What would have to change
3487+
3488+1. **Rebrand hooks.** The rule of 5.2 does not fit these conflicts, because the MultiGram side holds Forkgram code that Telegram does not have. 9.4 gives the commands.
3489+ - Drop hooks R3 and R4: `ForkSettingsActivity.java` does not exist. `git am -3` leaves Forkgram's whole file in the tree anyway (`deleted in HEAD and modified in Add the rebrand toolkit`). Remove it; it does not compile on DrKLO.
3490+ - Drop hook R2 as it is: DrKLO has no `forkCustomTitle`. The HEAD side of the conflict is empty, and the other side is R2 plus Forkgram's own title code. Keep DrKLO's side. Putting R2 back would even replace Telegram's logo title with the text "Fork Client" in a stock build. DrKLO's chat list title draws the picture `telegram_logo_2` over the text of `R.string.AppName` (`DialogsActivity.java` line 3517 in dc780e81e). Renaming `AppName` therefore does not change what is shown. A rebranded build would need a new hook after `actionBar.setTitle(ssb, statusDrawable);` (line 3519) that shows `Rebrand.defaultTitle(getString(R.string.AppName))` as plain text when `Rebrand.isActive()`. `Rebrand.defaultTitle` reads `AppName` through Android, as 8.1.6 requires. That hook has not been written or tested.
3491+ - In `TMessagesProj_App/build.gradle`, keep DrKLO's `apply plugin` line and add the `apply from:` line (R5) after it.
3492+2. **Rebrand generator** (`multigram/rebrand/generate_rebrand.py` and `selftest.py`). DrKLO has no `icon_01_*` icons and no `splash_fork_320`. Its default icon uses `icon_foreground_sa` (PNG files) and `icon_background_sa` (a `<shape>` XML file). Its `ic_launcher_dr` and `notification` are `.webp` files, where Forkgram has `.png`. The self-test fails with `error: unexpected drawable-hdpi/ic_launcher_dr.webp: update ICON_TARGETS in multigram/rebrand/generate_rebrand.py`, prints more `FAIL` lines, and then stops with a Python traceback (`KeyError: 'REBRAND_KEYSTORE'`) instead of a `FAILED` line.
3493+ - Editing `ICON_TARGETS` is not enough. `write_overlay()` writes only PNG files, adaptive icon XML and splash XML, and stops with `unexpected ...: update ICON_TARGETS` on anything else, such as the `.webp` icons or `icon_background_sa.xml`. Either teach `write_overlay()` those formats, or leave those three icons out of `ICON_TARGETS`; a rebranded build then keeps Telegram's account icon, notification icon and icon background.
3494+ - `selftest.py` must change with it: it looks for `notification.png` (line 247) and requires the "same file type and pixel size as upstream".
3495+ - DrKLO's `auth.xml` and `sync_contacts.xml` say `org.telegram.messenger`, so change `STOCK_XML_ACCOUNT_TYPE` too.
3496+ - A test with only `ic_launcher`, `ic_launcher_round`, `icon_foreground_sa` and `tg_splash_320` in `ICON_TARGETS`, and the new account type, ended with `FAILED: 4 failure(s)`: `seed A: no warnings` (the google-services warning of item 6) and three times `0 notification icons are white on transparent`.
3497+3. **Monet.** Replace `theme.isMonet()` at `PaletteFix.java` lines 165 and 225 and `RandomStyle.java` lines 449 and 624 with `false`. DrKLO has no Monet themes.
3498+4. **`random_style_check.py`.** Make the `SettingsBackup.kt` part optional. DrKLO has no settings export. The Android backup part applies as it is.
3499+5. **API id and hash.** DrKLO sets `APP_ID = 4` and Telegram's `APP_HASH` in `BuildVars.java`, and ignores the `APP_ID` and `APP_HASH` Gradle properties that the APK job writes. Forkgram copies them from `BuildConfig` (`BuildVars.java` lines 66 and 67). On DrKLO, edit `BuildVars.java` or port Forkgram's way.
3500+6. **Application id and Firebase.** DrKLO's id is `org.telegram.messenger`, the official app's id. A rebrand, or at least a new id, is required. DrKLO applies the google-services plugin in two modules: `TMessagesProj/build.gradle` (line 253) and `TMessagesProj_App/build.gradle` (line 208). Both `google-services.json` files list only `org.telegram.messenger`, `.beta` and `.web`. For a new id, put a `google-services.json` from your own Firebase project that lists `<id>`, `<id>.beta` and `<id>.web` in `TMessagesProj_App/`, or delete the `apply plugin` line of `TMessagesProj_App/build.gradle`. While that line is there, the rebrand generator warns about it (item 2).
3501+
3502+### 9.3 Build differences
3503+
3504+- **`-x :TMessagesProj:buildNativeDeps`** in `build.yml` must go: the task exists only in Forkgram (`TMessagesProj/build.gradle` line 282), and Gradle rejects excluding a task that does not exist. DrKLO links prebuilt native libraries from `TMessagesProj/jni/prebuild/`.
3505+- **Modules.** DrKLO's `settings.gradle` also includes `:TMessagesProj_AppHuawei`, `:TMessagesProj_AppHockeyApp`, `:TMessagesProj_AppStandalone` and `:TMessagesProj_AppTests`, and its root `build.gradle` adds Huawei and Firebase build plugins.
3506+- **Flavors.** The flavor `afat`, the tasks `compileAfatReleaseJavaWithJavac` and `assembleAfatRelease`, and the APK output folder exist in both.
3507+- **Signing.** DrKLO signs every build type with `TMessagesProj/config/release.keystore`, a public key whose passwords are in its `gradle.properties` (`RELEASE_STORE_PASSWORD=android`, `RELEASE_KEY_ALIAS=androidkey`, `RELEASE_KEY_PASSWORD=android`). It ignores Forkgram's `RELEASE_KEYSTORE_FILE`, but reads the other three properties. So with the `MULTIGRAM_KEYSTORE_*` secrets set (10.4), the APK job passes your passwords for DrKLO's keystore, which they do not open. Without the secrets, the APK is signed with DrKLO's public key. Port Forkgram's `signingConfigs` in `TMessagesProj_App/build.gradle`, which read `RELEASE_KEYSTORE_FILE`.
3508+- The APK job's extra tools (meson, nasm, rust targets) serve Forkgram's native build and are probably not needed for DrKLO. This was not tested.
3509+
3510+### 9.4 How to try it
3511+
3512+```sh
3513+git clone --recursive --shallow-submodules https://github.com/DrKLO/Telegram.git
3514+cd Telegram
3515+git fetch --depth 1 https://github.com/forkgram/TelegramAndroid.git tag 12.10.6.0
3516+git switch -c multigram dc780e81ed1261c369c27870e8e0999a1eb0b600
3517+git am -3 /path/to/multigram-patches/0001-*.patch
3518+```
3519+
3520+- The start point of `git switch` is the commit this section was tested on: the `Telegram-Base` line of the snapshot the patches were made on (`git log -1 --format=%B origin/forkgram`). DrKLO's `master` moves on; on any other commit, expect other conflicts and line numbers than in 9.1 and 9.2.
3521+- The `git fetch` of Forkgram's release gives `git am -3` the old file versions it needs. Use the tag that `APPLY.txt` names (the Forkgram version plus `.0`).
3522+
3523+Patch 1 stops with three conflicts (9.1). Settle them as 9.2 item 1 says:
3524+
3525+```sh
3526+git rm -q TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java
3527+git checkout --ours TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java
3528+sed -i -e '/^<<<<<<< /d' -e '/^=======$/d' -e '/^>>>>>>> /d' TMessagesProj_App/build.gradle
3529+git add TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java TMessagesProj_App/build.gradle
3530+git am --continue
3531+git am -3 /path/to/multigram-patches/000[2-6]-*.patch
3532+```
3533+
3534+- `git rm` drops R3 and R4 with Forkgram's file. `git checkout --ours` keeps DrKLO's `DialogsActivity.java`, without R2. The `sed` deletes the three marker lines, the only conflict in `build.gradle`: DrKLO's `apply plugin` line stays first and R5 follows.
3535+- Patches 2 to 6 then apply without conflicts.
3536+
3537+Then make changes 2 to 6 of 9.2 and commit them; without change 3 the Java code does not compile. To check the result:
3538+
3539+```sh
3540+python3 multigram/tools/palette_fix_check.py --base dc780e81e
3541+python3 multigram/tools/style_knobs_check.py --base dc780e81e
3542+python3 multigram/tools/random_style_check.py --base dc780e81e
3543+git grep -n -e 'multigram\.Rebrand\.' -e 'multigram/rebrand/rebrand.gradle' -- TMessagesProj TMessagesProj_App
3544+```
3545+
3546+- The first two print `OK` right after the patches. `random_style_check.py` stops with `FileNotFoundError` on `SettingsBackup.kt` until change 4 is done; then it prints `OK` too.
3547+- The `git grep` prints 2 lines here (R1 and R5), not the 5 of 5.4.
3548+- `bash multigram/tools/check.sh` stops at the random style check until change 4 is done, and at the rebrand self-test until change 2 is done and the google-services `apply plugin` line of `TMessagesProj_App/build.gradle` is gone (change 6). While that line is there, the self-test fails `seed A: no warnings`.
3549+
3550+## 10. Verifying
3551+
3552+### 10.1 The compile check on Actions
3553+
3554+In the browser: **Actions > Build MultiGram > Run workflow**. Leave "Use workflow from" on `main`, because the workflow file lives there. Type the branch to build in **ref**: `multigram-next` for a fix, `multigram` otherwise. Tick **APK** only for a release build (10.4).
3555+
3556+From a terminal (not run while writing this guide):
3557+
3558+```sh
3559+gh workflow run build.yml -R iko-soy/Multigram --ref main -f ref=multigram-next
3560+gh run list -R iko-soy/Multigram --workflow build.yml -L 5
3561+gh run watch <run-id> -R iko-soy/Multigram
3562+gh run view <run-id> -R iko-soy/Multigram --log-failed
3563+```
3564+
3565+The job **Compile Java and Kotlin**:
3566+
3567+1. checks out the ref, and fetches the two submodules the Java code needs (`TMessagesProj/lib/jlatexmath` and `TMessagesProj_Modules/media`);
3568+2. sets up Java 17 (Temurin) and Gradle;
3569+3. compiles the Java and Kotlin code of both modules, without the native code (step **Compile**);
3570+4. runs `bash multigram/tools/check.sh` if the file exists (step **Run MultiGram tool checks**).
3571+
3572+Past runs took about 8 to 9 minutes: about 4.5 minutes to compile and about 3 minutes for the checks (from the Actions history, not measured again for this guide). The sync's compile job calls this same workflow.
3573+
3574+### 10.2 check.sh
3575+
3576+`bash multigram/tools/check.sh` needs only bash and python3. It took 1 minute 8 seconds on 4 cores locally, and 2 to 3 minutes on CI. It stops at the first failure. Each part prints a header line that starts with `==`:
3577+
3578+| Header | What it checks | Passes with | A failure usually means |
3579+|---|---|---|---|
3580+| `== palette fix: ...` | `palette_fix_check.py`: colour values, `PaletteFix.java` tables, the 12 palette hook lines | `palette-fix: OK` | A palette hook is missing or doubled, or upstream changed a colour that overlay A replaces. See 8.2.5. |
3581+| `== random style: ...` | `random_style_check.py`: the 15 hooks and their places, privacy, strings, backup policy | `random-style: OK` | A random style hook is missing or in the wrong place. A Python `FileNotFoundError` means Forkgram moved `forkgram/SettingsBackup.kt`. A line `<file>: <X> not found after <Y>` can also mean that upstream changed a line the check uses as a landmark (`check_placement()` in `random_style_check.py`): if the hook still sits where its "If the code moved" says, update that pattern. See 8.3.5. |
3582+| `== style knobs: ...` | `style_knobs_check.py`: the knob hooks, stock lines gone, safe ranges | `style-knobs: OK` | A knob hook is missing, a stock line came back, or upstream reformatted a hooked line. Placement failures can also come from a changed landmark (8.4.2 lists them in `style_knobs_check.py`). See 8.4.5. |
3583+| `== rebrand toolkit: self-test` | `selftest.py`: two generated identities and `--clean` | `PASSED: 0 failure(s)` | Forkgram renamed or re-encoded an icon, string or account type literal the generator targets. The error names it, for example `update ICON_TARGETS`. See 8.1.5. |
3584+| `== style table: format and readability ...` | `check_style_table.py`: all 20,480 styles on this tree's theme sources | `style table check: PASS (...)` | Upstream colours changed so much that some styles are unreadable. Make a new table (8.3.5). |
3585+| `== style table: does this tree regenerate the same asset?` | `make_style_table.py --check` | `identical to ...` | Status 3 prints a `WARNING` and passes: make a new table when convenient. Status 4 fails: fix the palette first. Any other status fails with `FAIL: make_style_table.py exited with status N`. |
3586+
3587+The last line on success is `== all MultiGram checks passed`.
3588+
3589+A fix to a check script goes into the commit that added the script (6.3): `random_style_check.py` into "Give each install its own random style", `style_knobs_check.py` into "Randomise shapes and chat list layout per install", `palette_fix_check.py` into "Fix palette contrast for generated accents".
3590+
3591+### 10.3 Hooks no script checks
3592+
3593+- The four rebrand Java hooks: run the `git grep` of 8.1.5 and expect 5 lines (with R5).
3594+- Seven of the nine MultiGram lines in `ChatActivityEnterView.java` (P14, and all of P15 except the `int glyph = ...` and `drawableInverse` lines). Read them with `git grep -n MultiGram -- TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java`. It must print 9 lines.
3595+
3596+### 10.4 Release APK
3597+
3598+Run **Build MultiGram** with **APK** ticked (or add `-f apk=true` to the `gh` command). The job **Release APK** starts after the compile job and takes about 70 minutes, most of it for the native code; the whole run takes about 80 minutes ([run 36306473313](https://github.com/iko-soy/Multigram/actions/runs/36306473313), 2026-09-27). Its time limit is 300 minutes.
3599+
3600+Secrets, in **Settings > Secrets and variables > Actions**:
3601+
3602+| Secret | Needed? | What it is |
3603+|---|---|---|
3604+| `MULTIGRAM_APP_ID`, `MULTIGRAM_APP_HASH` | Yes | Your API id and hash from https://my.telegram.org. Without them the APK cannot log in. |
3605+| `MULTIGRAM_KEYSTORE_BASE64` | For APKs you give to others | Your release keystore, as `base64 -w0 release.keystore`. |
3606+| `MULTIGRAM_KEYSTORE_PASSWORD`, `MULTIGRAM_KEY_ALIAS` | With the keystore | The keystore password and the key's alias. |
3607+| `MULTIGRAM_KEY_PASSWORD` | Only if it differs from the keystore password | The key's password. |
3608+
3609+Without the keystore secrets the job signs the APK with Forkgram's public test key (`TMessagesProj/config/test.keystore`) and prints a warning. That is fine for testing, but anyone could sign an "update" for such an APK. Keep one key for every build you give out, or phones cannot update. The rebrand toolkit can make a key (see "Signing and updates" in `multigram/rebrand/README.md`).
3610+
3611+The job adds 8 GB of swap and smaller Java memory settings, because runners for private repositories have about 8 GB of memory. It builds with `./gradlew :TMessagesProj_App:assembleAfatRelease`. The APK is attached to the run as the artifact `multigram-apk`. To download it from a terminal: `gh run download <run-id> -R iko-soy/Multigram -n multigram-apk`.
3612+
3613+### 10.5 Device checklist
3614+
3615+After each update:
3616+
3617+1. **Update over the previous MultiGram build** (same key): it installs as an update, the style does not change, and "Shuffle my style" still works.
3618+2. **Fresh install, light system theme:** the first frame already shows the day style, with no flash of the Blue theme. **Dark system theme:** the night style. Bubble corners and the chat list layout are right from the first frame.
3619+3. **Chat Settings:** Shuffle, then Undo. The preview, radius slider and chat list picker update at once. "Reset to defaults" returns to the install's style and radius. The custom theme tile applies the current generated style.
3620+4. **Sharing the generated accent** shows the "stays on this device" message and uploads nothing. "Create new theme" shows the message instead of the dialog.
3621+5. **Shapes:** service and date pills, reaction chips, sheet buttons, the chat list floating button with its shadow, settings cards, and the wallpaper gradient's start phase.
3622+6. **Readability:** grey texts, times in bubbles, the message field hint, and the send and voice glyphs on the accent, in day and night.
3623+7. **Monet:** a selected Monet theme survives a restart. Shuffle replaces it with a generated style (by design). "Reset to defaults" leaves it alone.
3624+8. **Rebranded build only:** app name, launcher, splash and notification icons, a single icon in the icon picker, the chat list title, and the account in **Settings > Accounts** with working contact sync.
3625+9. **Tablet or unfolded foldable:** knob values from the first frame.
3626+
3627+The full lists are in `multigram/random-style/README.md` (15 items) and `multigram/style-knobs/README.md` (10 items).
3628+
3629+## 11. Keeping this guide current
3630+
3631+### 11.1 Check and refresh the hooks
3632+
3633+**After every upstream update**, even one that replayed with no conflict, the "Find this" texts and the line numbers in section 8 can go stale. No check script looks at them, but this does. Run it in the folder that holds `PATCH.md` (the `docs` branch, 11.2), in a clone that has `origin/forkgram`:
3634+
3635+```sh
3636+python3 - origin/forkgram <<'EOF'
3637+import re, subprocess, sys
3638+rev = sys.argv[1]
3639+if subprocess.run(['git', 'rev-parse', '--verify', '-q', rev + '^{commit}'], stdout=subprocess.DEVNULL).returncode:
3640+ sys.exit('unknown revision: ' + rev)
3641+doc = open('PATCH.md', encoding='utf-8').read()
3642+for part in re.split(r'^##### ', doc, flags=re.M)[1:]:
3643+ hook = part.split('.')[0]
3644+ path = re.search(r'\*\*File:\*\* `([^`]+)`', part).group(1)
3645+ if 'multigram' in path.split('/'):
3646+ continue # MultiGram's own files (K18 to K24)
3647+ show = subprocess.run(['git', 'show', rev + ':' + path], capture_output=True, text=True)
3648+ if show.returncode:
3649+ print(hook, path, 'is missing')
3650+ continue
3651+ src = show.stdout
3652+ for kind, block in re.findall(r'\*\*(Find this|Then find these)[^\n]*\n\n```[a-z]*\n(.*?)```', part, re.S):
3653+ for text in (block.splitlines() if kind.startswith('Then') else [block]):
3654+ if src.count(text) != 1:
3655+ print(hook, 'found', src.count(text), 'times:', ' '.join(text.split())[:60])
3656+ elif kind == 'Find this':
3657+ line = src[:src.index(text)].count('\n') + 1
3658+ said = re.search(r'starts at line (\d+)', part)
3659+ if said and int(said.group(1)) != line:
3660+ print(hook, 'now starts at line', line, 'not', said.group(1))
3661+EOF
3662+```
3663+
3664+It prints one line for each hook text that no longer occurs exactly once, and one for each hook whose text now starts at another line than its "Where" says. On `forkgram` 4543767655 it prints a single line, because K1's text comes from the stack (1.5):
3665+
3666+```text
3667+K1 found 0 times: org.telegram.messenger.multigram.RandomStyle.onApplicationCr
3668+```
3669+
3670+Refresh every other hook it names with the commands at the end of this section.
3671+
3672+**After the stack changes** (a fixup after an update, say), also compare these lists with section 3 and section 8. Run them from any checkout:
3673+
3674+```sh
3675+git fetch origin
3676+git diff --stat --diff-filter=M origin/forkgram origin/multigram -- . ':!multigram' ':!TMessagesProj/src/main/java/org/telegram/messenger/multigram'
3677+git diff --name-only --diff-filter=A origin/forkgram origin/multigram
3678+git diff -U0 --diff-filter=M origin/forkgram origin/multigram -- . ':!multigram' ':!TMessagesProj/src/main/java/org/telegram/messenger/multigram'
3679+```
3680+
3681+1. The first command lists the upstream files the stack touches. On 4780eb095b it ends with `30 files changed, 114 insertions(+), 73 deletions(-)`: 78 and 37 lines in Java and Gradle files, plus 36 and 36 in the `.attheme` files.
3682+2. The second lists the new files: 37 on 4780eb095b.
3683+3. The third shows every hook line with no context.
3684+
3685+The code blocks of each hook come straight from git:
3686+
3687+- "Change it to" is `git diff -U3 <commit>^ <commit> -- <file>` for the commit that adds the hook. The value lists P1 to P6 use `-U0`, which shows only the changed lines.
3688+- "Find this" is the matching text in `git show origin/forkgram:<file>`, and the line number in "Where" is its line there. For hooks in MultiGram's own files (K18 to K24), and for K1, which sits on the line after S1, it is the text in the file as the previous stack commit left it.
3689+
3690+When a hook changes, refresh its blocks with these commands. Do not type code by hand.
3691+
3692+### 11.2 Update the stamp
3693+
3694+Change the table in 1.1:
3695+
3696+- the `multigram` commit: `git rev-parse --short=10 origin/multigram`;
3697+- the Forkgram version and commit, and the Telegram base: `git log -1 --format=%B origin/forkgram`;
3698+- the `forkgram` and `main` commits;
3699+- the date.
3700+
3701+Then update the data that belongs to one version. Most of it is marked "on 4780eb095b", "in Forkgram 12.10.6" or "today"; `grep -n -e '4780eb095b' -e '12\.10\.6' -e '[Tt]oday' PATCH.md` finds those places. In short:
3702+
3703+- the counts in 3.1, and the stat line and file count in 11.1;
3704+- the "starts at line" of each hook (the script in 11.1 names the stale ones);
3705+- line numbers in MultiGram's own files: 6.2 and 9.2 (`PaletteFix.java` 165, 225 and 316; `RandomStyle.java` 449 and 624), and `selftest.py` line 270 (8.1.4, 8.1.6);
3706+- the problem counts in 7.4 (59, 27 and 35), and the line counts of the `git grep` commands in 5.4, 8.1.5, P13 and 8.2.6;
3707+- 8.1.4 (string lines, 54 icon files), the old-value counts in P3 and P6, and the values and contrasts in 8.2.4;
3708+- the expected outputs in 8.1.5, 8.2.5 and 8.3.5, and the CRC in 8.3.4;
3709+- the timings in 4.4, 8.1.5, 8.3.5, 8.4.5, 8.4.6, 10.1 and 10.2;
3710+- the DrKLO commit and line numbers in section 9.
3711+
3712+This guide lives on the branch `docs`. If you keep a separate worktree for that branch, run these commands there instead of switching:
3713+
3714+```sh
3715+git switch docs
3716+git add PATCH.md
3717+git commit -m "Update PATCH.md for Forkgram <version>"
3718+git push origin docs
3719+```