on main help sign in
Fix palette contrast for generated accents

Stock Telegram colours already fail WCAG contrast on many text and background pairs, so randomly generated accents are only readable with this fix (overlay A from the colour-engine simulation). - ThemeColors: 11 grey secondary-text defaults darkened to pass 4.5:1. The 12 on-accent content defaults keep their stock value. - Bundled .attheme files: 36 grey text values adjusted (Blue, Dark Blue, Arctic Blue, Day, Night). Monet themes are untouched. Theme.getAssetFile also re-copies a bundled theme after an app update, since two of the edited files keep their size. - 13 neutral keys join the accent exclusion set, so accent re-tints no longer pull them below threshold. - New PaletteFix class: for runtime accents only (id > 100 and no foreign server theme: generated or made on this device; never stock presets, Telegram's server themes, other people's cloud themes or Monet) marks drawn on accent fills become white or #050505, whichever contrasts more, and the muted unread badge gets a matching neutral fill. Besides overlay A's 12 pairs this covers the voice-record glyphs and the send-button glyphs, which upstream draws in hard-coded white. Applied to the global colours in refreshThemeColors and to colour maps (per-chat themes while a runtime accent is active, colour previews, exported accent themes, DarkThemeResourceProvider fill overrides). - fillAccentColors runs the black/white out-bubble text block for runtime accents even when the bubble is near the theme's own. Upstream files only get small hooks marked "MultiGram:". multigram/tools/palette_fix_check.py verifies the values and hooks against multigram/palette-fix/overlay_A.json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mVy1fu8zBn38wHgJqmLNz

commit047bcc1d7b85d329812474643aa870747d5cd5a4 authorClaude <noreply@anthropic.com> dateSun 27 Sep 2026, 00:53 (2 weeks ago) parentdc7ddc7
17 files changed+866−54
TMessagesProj/src/main/assets/arctic.attheme+4−4 TMessagesProj/src/main/assets/bluebubbles.attheme+4−4 TMessagesProj/src/main/assets/darkblue.attheme+12−12 TMessagesProj/src/main/assets/day.attheme+6−6 TMessagesProj/src/main/assets/night.attheme+10−10 TMessagesProj/src/main/java/org/telegram/messenger/multigram/PaletteFix.java+362 TMessagesProj/src/main/java/org/telegram/ui/ActionBar/EmojiThemes.java+1 TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java+5−2 TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java+11−11 TMessagesProj/src/main/java/org/telegram/ui/ChannelColorActivity.java+1 TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java+9−4 TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/MessageEntityView.java+1 TMessagesProj/src/main/java/org/telegram/ui/PeerColorActivity.java+1 TMessagesProj/src/main/java/org/telegram/ui/Stories/DarkThemeResourceProvider.java+1−1 multigram/palette-fix/README.md+70 multigram/palette-fix/overlay_A.json+178 multigram/tools/palette_fix_check.py+190
TMessagesProj/src/main/assets/arctic.attheme+4−4
@@ -59,7 +59,7 @@ chat_outInstantSelected=-1
5959chat_outSentCheck=-922746881
6060key_graySectionText=-8156525
6161dialogButton=-13987604
62−actionBarDefaultSubtitle=-7630182
62+actionBarDefaultSubtitle=-9341057
6363contextProgressInner2=-826024458
6464chat_inContactPhoneText=-6182221
6565chat_inlineResultIcon=-13328651
@@ -99,7 +99,7 @@ chat_inVenueInfoSelectedText=-7099204
9999dialogTextBlue2=-14772773
100100dialogTextBlue4=-15625752
101101actionBarTabActiveText=-13590803
102−chat_topPanelMessage=-8354167
102+chat_topPanelMessage=-9275526
103103calls_callReceivedGreenIcon=-13645978
104104chats_pinnedOverlay=100663296
105105chat_inForwardedNameText=-13464859
@@ -162,7 +162,7 @@ color_orange=-881607
162162chat_topPanelTitle=-281900315
163163chat_inAudioCacheSeekbar=1071179248
164164chat_outContactIcon=-13332255
165−chat_inFileInfoText=-6182221
165+chat_inFileInfoText=-9472128
166166avatar_subtitleInProfileBlue=-7500403
167167chat_inPreviewLine=-12739084
168168profile_creatorIcon=-15626754
@@ -190,7 +190,7 @@ listSelectorSDK21=251658240
190190chat_outFileNameText=-1
191191picker_enabledButton=-14180619
192192avatar_nameInMessagePink=-3453050
193−windowBackgroundWhiteGrayText=-7629665
193+windowBackgroundWhiteGrayText=-9472126
194194chat_serviceIcon=-1
195195avatar_nameInMessageBlue=-13464859
196196actionBarDefault=-1
TMessagesProj/src/main/assets/bluebubbles.attheme+4−4
@@ -36,7 +36,7 @@ chat_outInstantSelected=-14770977
3636chat_outSentCheck=-14967834
3737key_graySectionText=-7762802
3838dialogButton=-13200935
39−actionBarDefaultSubtitle=-8156010
39+actionBarDefaultSubtitle=-9472127
4040chat_inContactPhoneText=-6182221
4141dialogLineProgress=-12083745
4242chat_outPreviewInstantText=-14514998
@@ -122,7 +122,7 @@ chat_attachLocationText=-12801233
122122listSelectorSDK21=218103808
123123chat_outFileNameText=-14799042
124124avatar_nameInMessagePink=-2987883
125−windowBackgroundWhiteGrayText=-7565423
125+windowBackgroundWhiteGrayText=-9144455
126126avatar_nameInMessageBlue=-14643754
127127actionBarDefault=-1
128128actionBarDefaultSelector=-855310
@@ -158,11 +158,11 @@ chat_outAudioCacheSeekbar=-1029190425
158158chats_sentClock=-4670273
159159chat_inPreviewInstantText=-14446113
160160chat_outLoaderSelected=-12277262
161−windowBackgroundWhiteGrayText2=-7565423
161+windowBackgroundWhiteGrayText2=-9144455
162162chat_outAudioSeekbarSelected=1196665821
163163windowBackgroundWhiteGrayText6=-8683904
164164windowBackgroundWhiteGrayText3=-7565423
165−windowBackgroundWhiteGrayText4=-7565423
165+windowBackgroundWhiteGrayText4=-9671056
166166chat_BlurAlpha=-16777216
167167chat_wallpaper_gradient_to=-5519136
168168chat_inVoiceSeekbarFill=-11489304
TMessagesProj/src/main/assets/darkblue.attheme+12−12
@@ -60,7 +60,7 @@ key_sheet_other=1140850687
6060chat_inContactNameText=-8796932
6161chats_menuPhoneCats=-8613724
6262chat_outPreviewLine=-6631937
63−chat_messagePanelHint=1859907583
63+chat_messagePanelHint=-1763971073
6464windowBackgroundGray=-15393241
6565chat_inViaBotNameText=-8796932
6666chat_outVoiceSeekbar=-429551165
@@ -113,7 +113,7 @@ picker_disabledButton=-11047552
113113groupcreate_spanBackground=-13878194
114114dialogButton=-10177041
115115contextProgressInner1=1686212251
116−actionBarDefaultSubtitle=2111566591
116+actionBarDefaultSubtitle=-1831079169
117117chat_inContactPhoneText=-8812393
118118chat_inlineResultIcon=-8796932
119119chat_outBubbleGradientSelectedOverlay=352321535
@@ -173,7 +173,7 @@ chat_attachUnactiveTab=-9596506
173173windowBackgroundWhiteGreenText=-10371737
174174actionBarTabActiveText=-9781249
175175chat_emojiPanelIcon=-9996665
176−chat_topPanelMessage=1859974399
176+chat_topPanelMessage=-1814235905
177177statisticChartSignatureAlpha=-1946157057
178178chat_emojiPanelTrendingDescription=-8549479
179179calls_callReceivedGreenIcon=-12001930
@@ -259,7 +259,7 @@ color_orange=-881607
259259chat_topPanelTitle=-9719066
260260chat_inAudioCacheSeekbar=-11443856
261261chat_outContactIcon=-1
262−chat_inFileInfoText=-8812137
262+chat_inFileInfoText=-7035469
263263avatar_subtitleInProfileBlue=-7628894
264264chat_inPreviewLine=-8796932
265265profile_creatorIcon=-10177041
@@ -297,7 +297,7 @@ chat_outFileNameText=-1
297297picker_enabledButton=-9781249
298298inappPlayerBackground=-14602949
299299avatar_nameInMessagePink=-1543014
300−windowBackgroundWhiteGrayText=-8549479
300+windowBackgroundWhiteGrayText=-7562327
301301statisticChartSignature=-1214008894
302302actionBarDefaultSubmenuItemIcon=-1
303303chat_attachPollBackground=-2183099
@@ -358,7 +358,7 @@ chat_outTimeText=-7357217
358358chat_outBubble=-12689014
359359statisticChartChevronColor=-9012091
360360chats_menuItemText=-184549377
361−chats_message=-8549479
361+chats_message=-7562327
362362chat_outReplyNameText=-6631937
363363chats_menuTopBackgroundCats=-14471105
364364chat_outReplyMediaMessageText=-7357217
@@ -366,7 +366,7 @@ chat_textSelectBackground=1516415459
366366chat_messagePanelVoiceDelete=-1
367367chat_inAudioProgress=-1
368368chat_inReactionButtonTextSelected=-1
369−chats_date=-9207925
369+chats_date=-7431514
370370chat_messagePanelText=-1
371371actionBarTabSelector=926307434
372372key_sheet_scrollUp=637534207
@@ -393,7 +393,7 @@ dialogSearchIcon=-8945521
393393chat_inPreviewInstantText=-8796932
394394chats_archiveBackground=-11036980
395395dialog_liveLocationProgress=-9919529
396−chat_inViews=-8812137
396+chat_inViews=-7035469
397397chat_outLoaderSelected=-9919529
398398dialogButtonSelector=-13483690
399399chats_archivePinBackground=-13746613
@@ -401,14 +401,14 @@ player_actionBarItems=-1
401401chat_sentError=-633010
402402player_progressBackground=-13023400
403403chat_inAudioDurationSelectedText=-7490861
404−windowBackgroundWhiteGrayText2=1878130175
404+windowBackgroundWhiteGrayText2=-2081292801
405405chat_outAudioSeekbarSelected=-9461802
406406windowBackgroundWhiteGrayText8=-8549479
407407windowBackgroundWhiteGrayText5=-8549479
408408windowBackgroundWhiteGrayText6=-8549479
409409windowBackgroundWhiteGrayText3=-8549479
410−windowBackgroundWhiteGrayText4=-931296359
411−chat_inTimeText=-645885536
410+windowBackgroundWhiteGrayText4=-209876071
411+chat_inTimeText=-7035724
412412dialogRadioBackground=-11245959
413413statisticChartRipple=748994002
414414chat_BlurAlpha=-1308622848
@@ -438,7 +438,7 @@ chat_inMenu=2039722445
438438chat_recordVoiceCancel=-9192978
439439chat_outReplyMediaMessageSelectedText=-1
440440chat_messageTextIn=-328966
441−chats_attachMessage=-8548712
441+chats_attachMessage=-6710887
442442chat_topPanelBackground=-14602949
443443chat_outSentClock=-8213557
444444dialogBackgroundGray=-14932431
TMessagesProj/src/main/assets/day.attheme+6−6
@@ -63,7 +63,7 @@ chat_outInstantSelected=-1
6363chat_outSentCheck=-167772161
6464key_graySectionText=-8091766
6565dialogButton=-13987604
66−actionBarDefaultSubtitle=-8814210
66+actionBarDefaultSubtitle=-9340810
6767contextProgressInner2=-826024458
6868chat_inContactPhoneText=-7565679
6969chat_inlineResultIcon=-13328651
@@ -105,7 +105,7 @@ chat_inVenueInfoSelectedText=-7565679
105105dialogTextBlue2=-14772773
106106dialogTextBlue4=-15625752
107107actionBarTabActiveText=-14054705
108−chat_topPanelMessage=-9011588
108+chat_topPanelMessage=-9340809
109109chat_stickerNameText=-1
110110calls_callReceivedGreenIcon=-13645978
111111chat_botButtonText=-13816531
@@ -174,7 +174,7 @@ color_orange=-881607
174174chat_topPanelTitle=-12478747
175175chat_inAudioCacheSeekbar=1071179248
176176chat_outContactIcon=-14707997
177−chat_inFileInfoText=-7565679
177+chat_inFileInfoText=-9671312
178178avatar_subtitleInProfileBlue=-7500403
179179featuredStickers_addButton=-11689749
180180chat_inPreviewLine=-12805393
@@ -207,7 +207,7 @@ listSelectorSDK21=251658240
207207chat_outFileNameText=-1
208208picker_enabledButton=-14180619
209209avatar_nameInMessagePink=-3453050
210−windowBackgroundWhiteGrayText=-8354940
210+windowBackgroundWhiteGrayText=-9078920
211211chat_serviceIcon=-13615543
212212avatar_nameInMessageBlue=-14317866
213213actionBarDefault=-1
@@ -272,14 +272,14 @@ chats_sentClock=2073474246
272272chat_inAudioSeekbar=-2762017
273273chats_archiveBackground=-11294989
274274chat_inPreviewInstantText=-15300135
275−chat_inViews=-274882397
275+chat_inViews=-9736591
276276chat_outLoaderSelected=-1
277277chats_archivePinBackground=-4472113
278278chat_messagePanelCursor=-12214815
279279chat_sentError=-65536
280280chat_inAudioDurationSelectedText=-7565679
281281chat_outAudioSeekbarSelected=1688061683
282−chat_inTimeText=-7105127
282+chat_inTimeText=-9671311
283283chat_outBubbleGradient=-10701088
284284chat_outReplyMessageText=-1
285285chat_recordedVoiceDot=-833978
TMessagesProj/src/main/assets/night.attheme+10−10
@@ -57,7 +57,7 @@ chat_inContactNameText=-8796932
5757chats_menuPhoneCats=-8224126
5858chat_outPreviewLine=-6631937
5959dialogScrollGlow=-657931
60−chat_messagePanelHint=1694498815
60+chat_messagePanelHint=1979711487
6161windowBackgroundGray=-16777216
6262chat_inViaBotNameText=-8796932
6363chat_outVoiceSeekbar=-10245406
@@ -112,7 +112,7 @@ picker_disabledButton=-11047552
112112groupcreate_spanBackground=-13816531
113113dialogButton=-10177041
114114contextProgressInner1=1526726655
115−actionBarDefaultSubtitle=1945301746
115+actionBarDefaultSubtitle=-2131561742
116116chat_inContactPhoneText=-8486783
117117chat_inlineResultIcon=-8796932
118118chat_outBubbleGradientSelectedOverlay=352321535
@@ -178,7 +178,7 @@ chat_attachUnactiveTab=-9596506
178178windowBackgroundWhiteGreenText=-10371737
179179actionBarTabActiveText=-10698241
180180chat_emojiPanelIcon=-9539985
181−chat_topPanelMessage=1694498815
181+chat_topPanelMessage=1962934271
182182statisticChartSignatureAlpha=-1946157057
183183chat_emojiSearchIcon=-9211020
184184chat_emojiPanelTrendingDescription=-8553090
@@ -271,7 +271,7 @@ color_orange=-881607
271271chat_topPanelTitle=-10440716
272272chat_inAudioCacheSeekbar=-10461088
273273chat_outContactIcon=-1
274−chat_inFileInfoText=-9013641
274+chat_inFileInfoText=-7763573
275275avatar_subtitleInProfileBlue=2029911551
276276key_chat_messagePanelVoiceLockShadow=0
277277chat_inPreviewLine=-8796932
@@ -311,7 +311,7 @@ chat_outFileNameText=-1
311311picker_enabledButton=-9781249
312312inappPlayerBackground=-15066597
313313avatar_nameInMessagePink=-1543014
314−windowBackgroundWhiteGrayText=1862270975
314+windowBackgroundWhiteGrayText=1962934271
315315statisticChartSignature=-1214008894
316316actionBarDefaultSubmenuItemIcon=-1
317317chat_attachPollBackground=-2183099
@@ -382,7 +382,7 @@ chat_textSelectBackground=1516415459
382382chat_messagePanelVoiceDelete=-1
383383chat_inAudioProgress=-1
384384chat_inReactionButtonTextSelected=-1
385−chats_date=-8882056
385+chats_date=-8289920
386386chat_messagePanelText=-1
387387actionBarTabSelector=827876202
388388key_sheet_scrollUp=637534207
@@ -412,7 +412,7 @@ dialogSearchIcon=-8882056
412412chat_inPreviewInstantText=-8796932
413413chats_archiveBackground=-12219694
414414dialog_liveLocationProgress=-9919529
415−chat_inViews=-8881024
415+chat_inViews=-8025458
416416chat_outLoaderSelected=-1
417417dialogButtonSelector=436207615
418418chats_archivePinBackground=-13619152
@@ -420,14 +420,14 @@ player_actionBarItems=-1
420420chat_sentError=-633010
421421player_progressBackground=-12829636
422422chat_inAudioDurationSelectedText=-7490861
423−windowBackgroundWhiteGrayText2=1862139391
423+windowBackgroundWhiteGrayText2=1962802687
424424chat_outAudioSeekbarSelected=-562182145
425425windowBackgroundWhiteGrayText8=-8553091
426426windowBackgroundWhiteGrayText5=-8549479
427427windowBackgroundWhiteGrayText6=-8553091
428428windowBackgroundWhiteGrayText3=-7960954
429−windowBackgroundWhiteGrayText4=-9803158
430−chat_inTimeText=-8552575
429+windowBackgroundWhiteGrayText4=-9079692
430+chat_inTimeText=-7894388
431431dialogRadioBackground=-10855846
432432statisticChartRipple=748994002
433433chat_BlurAlpha=-1308622848
TMessagesProj/src/main/java/org/telegram/messenger/multigram/PaletteFix.javanew file+362
@@ -0,0 +1,362 @@
1+package org.telegram.messenger.multigram;
2+
3+import android.content.Context;
4+import android.util.SparseIntArray;
5+
6+import org.telegram.messenger.ApplicationLoader;
7+import org.telegram.ui.ActionBar.Theme;
8+
9+import java.io.File;
10+import java.util.HashMap;
11+import java.util.HashSet;
12+import java.util.Set;
13+
14+/**
15+ * Palette fix for generated accents ("overlay A", see multigram/palette-fix/README.md).
16+ *
17+ * Stock Telegram colours already fail WCAG contrast on many text/background pairs, so a randomly
18+ * generated accent is only readable on top of this fix. It has three parts:
19+ * <ul>
20+ * <li>static colour changes (ThemeColors defaults, bundled .attheme lines) and extra
21+ * accent-exclusion keys ({@link #addAccentExclusions}), which apply to every accent;</li>
22+ * <li>the on-accent rule: marks drawn on accent-coloured fills (compose pencil, unread digits,
23+ * filled-button text, checkmarks, send and record glyphs...) become white or
24+ * {@link #ON_ACCENT_DARK}, whichever has the higher contrast with the fill, and the muted unread
25+ * badge gets a neutral fill that passes with the chosen digit colour. It runs only for runtime
26+ * accents ({@link #isRuntimeAccent}: generated by MultiGram or made on this device), never for
27+ * stock presets, Telegram's server themes, other people's cloud themes or Monet themes
28+ * ({@link #isActiveFor}); per-chat themes follow the global state ({@link #applyToColorMap});</li>
29+ * <li>the out-bubble text guard in Theme.ThemeAccent.fillAccentColors (runtime accents only).</li>
30+ * </ul>
31+ */
32+public final class PaletteFix {
33+
34+ /**
35+ * Stock preset accents have ids up to 99 (id 100 is a custom accent migrated from very old app
36+ * versions, treated as stock). Every accent a ThemeInfo creates later gets ++lastAccentId, so 101 and
37+ * up: MultiGram's generated accents, accents the user makes in the theme editor, and also accents of
38+ * Telegram's server themes and of cloud themes, which {@link #isRuntimeAccent} excludes by their info.
39+ */
40+ public static final int LAST_STOCK_ACCENT_ID = 100;
41+
42+ public static final int ON_ACCENT_LIGHT = 0xffffffff;
43+ /** The lightest near-black for which max(contrast(white), contrast(this)) >= 4.5 on any fill (min 4.51). */
44+ public static final int ON_ACCENT_DARK = 0xff050505;
45+
46+ /** Contrast targets with headroom over WCAG 4.5 (text) and 3.0 (icons) for accent solvers. */
47+ public static final double TARGET_TEXT_CONTRAST = 4.6;
48+ public static final double TARGET_ICON_CONTRAST = 3.1;
49+
50+ private static final double[] SRGB_LIN = new double[256];
51+ /** Theme key -> {muted badge fill with white digits, muted badge fill with near-black digits}. */
52+ private static final HashMap<String, int[]> MUTED_FILL = new HashMap<>();
53+ /** Bundled assets whose cached copy was already checked in this process ({@link #isStaleAssetCopy}). */
54+ private static final HashSet<String> CHECKED_ASSETS = new HashSet<>();
55+ private static long appUpdateTime = -1;
56+
57+ static {
58+ for (int i = 0; i < 256; i++) {
59+ double c = i / 255.0;
60+ SRGB_LIN[i] = c <= 0.04045 ? c / 12.92 : StrictMath.pow((c + 0.055) / 1.055, 2.4);
61+ }
62+ int[] light = {0xff73777b, 0xffbec3c7};
63+ MUTED_FILL.put("Blue", light);
64+ MUTED_FILL.put("Arctic Blue", light);
65+ MUTED_FILL.put("Day", light);
66+ MUTED_FILL.put("Dark Blue", new int[]{0xff3e5263, 0xff63798b});
67+ MUTED_FILL.put("Night", new int[]{0xff454545, 0xff777676});
68+ }
69+
70+ /**
71+ * Theme.key_* are assigned with colorsCount++ in Theme's static initializer, so they are not
72+ * compile-time constants. This holder is initialised on first use, after those fields are set.
73+ */
74+ private static final class Keys {
75+ /** (content, fill) pairs. */
76+ static final int[] ON_ACCENT = {
77+ Theme.key_chats_actionIcon, Theme.key_chats_actionBackground,
78+ Theme.key_chats_unreadCounterText, Theme.key_chats_unreadCounter,
79+ Theme.key_featuredStickers_buttonText, Theme.key_featuredStickers_addButton,
80+ Theme.key_checkboxSquareCheck, Theme.key_checkboxSquareBackground,
81+ Theme.key_dialogRoundCheckBoxCheck, Theme.key_dialogRoundCheckBox,
82+ Theme.key_chat_goDownButtonCounter, Theme.key_chat_goDownButtonCounterBackground,
83+ Theme.key_dialogCheckboxSquareCheck, Theme.key_dialogCheckboxSquareBackground,
84+ Theme.key_chat_attachCheckBoxCheck, Theme.key_chat_attachCheckBoxBackground,
85+ Theme.key_chat_inMediaIcon, Theme.key_chat_inLoader,
86+ Theme.key_chat_inContactIcon, Theme.key_chat_inContactBackground,
87+ Theme.key_picker_badgeText, Theme.key_picker_badge,
88+ Theme.key_dialogFloatingIcon, Theme.key_dialogFloatingButton,
89+ // not in overlay A: mic/camera/send glyphs of the voice-record circle and the
90+ // send-as menu button, drawn on chat_messagePanelVoiceBackground
91+ Theme.key_chat_messagePanelVoicePressed, Theme.key_chat_messagePanelVoiceBackground
92+ };
93+ /** Keys that fall back to chats_unreadCounterMuted; they keep their colour when it changes. */
94+ static final int[] MUTED_PINS = {
95+ Theme.key_avatar_backgroundArchived,
96+ Theme.key_chats_archivePinBackground,
97+ Theme.key_chats_archivePullDownBackground,
98+ Theme.key_checkboxDisabled
99+ };
100+ /** content key -> fill key. */
101+ static final SparseIntArray FILL_OF = new SparseIntArray();
102+
103+ static {
104+ for (int i = 0; i < ON_ACCENT.length; i += 2) {
105+ FILL_OF.put(ON_ACCENT[i], ON_ACCENT[i + 1]);
106+ }
107+ }
108+ }
109+
110+ private static volatile boolean globalActive;
111+
112+ private PaletteFix() {
113+ }
114+
115+ /**
116+ * Keys that must not follow the accent re-tint (neutral greys and the grey settings background).
117+ * Called once from Theme's static initializer with Theme.themeAccentExclusionKeys.
118+ */
119+ public static void addAccentExclusions(Set<Integer> exclusions) {
120+ exclusions.add(Theme.key_windowBackgroundGray);
121+ exclusions.add(Theme.key_windowBackgroundWhiteGrayText);
122+ exclusions.add(Theme.key_windowBackgroundWhiteGrayText2);
123+ exclusions.add(Theme.key_windowBackgroundWhiteGrayText4);
124+ exclusions.add(Theme.key_chats_message);
125+ exclusions.add(Theme.key_chats_date);
126+ exclusions.add(Theme.key_actionBarDefaultSubtitle);
127+ exclusions.add(Theme.key_chat_inTimeText);
128+ exclusions.add(Theme.key_chat_inFileInfoText);
129+ exclusions.add(Theme.key_chat_inViews);
130+ exclusions.add(Theme.key_chat_messagePanelHint);
131+ exclusions.add(Theme.key_chat_messagePanelIcons);
132+ exclusions.add(Theme.key_chat_topPanelMessage);
133+ }
134+
135+ /**
136+ * Whether the accent was generated by MultiGram or made by the user on this device: id above
137+ * {@link #LAST_STOCK_ACCENT_ID} and no server theme behind it, or a server theme this user created
138+ * (a custom accent keeps the rule after the user shares it). Accents of Telegram's own server themes
139+ * (Chat Settings carousel, account.getThemes) and of cloud themes installed from someone else's
140+ * link also get ids above 100 but carry their TL_theme in accent.info, so they stay stock.
141+ * Also the guard of the out-bubble black/white text block in Theme.ThemeAccent.fillAccentColors.
142+ */
143+ public static boolean isRuntimeAccent(Theme.ThemeAccent accent) {
144+ return accent != null && accent.id > LAST_STOCK_ACCENT_ID && (accent.info == null || accent.info.creator);
145+ }
146+
147+ /**
148+ * Whether the on-accent rule applies to colours built from this accent of this theme: the accent is
149+ * a runtime one ({@link #isRuntimeAccent}), the theme is not a Monet theme, and Theme.getColor would
150+ * not serve the stock defaults for it anyway (on Blue, an accent with the default accent colour, see
151+ * ThemeInfo.isDefaultMainAccent). Stock presets, server themes and chat-theme accents give false.
152+ *
153+ * @param theme the theme the colours are built for, or null to use accent.parentTheme
154+ */
155+ public static boolean isActiveFor(Theme.ThemeInfo theme, Theme.ThemeAccent accent) {
156+ if (!isRuntimeAccent(accent)) {
157+ return false;
158+ }
159+ if (theme == null) {
160+ theme = accent.parentTheme;
161+ }
162+ if (theme == null) {
163+ return true;
164+ }
165+ if (theme.isMonet()) {
166+ return false;
167+ }
168+ if (theme.firstAccentIsDefault && theme.themeAccentsMap != null) {
169+ Theme.ThemeAccent defaultAccent = theme.themeAccentsMap.get(Theme.DEFALT_THEME_ACCENT_ID);
170+ if (defaultAccent != null && defaultAccent.accentColor == accent.accentColor) {
171+ return false;
172+ }
173+ }
174+ return true;
175+ }
176+
177+ /** Whether the global colours (Theme.getColor) currently carry the on-accent rule. */
178+ public static boolean isGlobalActive() {
179+ return globalActive;
180+ }
181+
182+ /**
183+ * Global hook. Called at the end of Theme.refreshThemeColors, after fillAccentColors and the
184+ * calculated colours, with the new currentColors. Fills are read through
185+ * Theme.getColor(key, null, true), so theme-switch animation values are ignored.
186+ */
187+ public static void applyToCurrentColors(Theme.ThemeInfo theme, Theme.ThemeAccent accent, SparseIntArray currentColors) {
188+ boolean active = isActiveFor(theme, accent);
189+ globalActive = active;
190+ if (active) {
191+ apply(theme, currentColors, true);
192+ }
193+ }
194+
195+ /**
196+ * Map-based hook. Call right after accent.fillAccentColors(noAccent, colors) wherever a
197+ * standalone colour map is built for a theme (chat themes, previews, exported theme files).
198+ * Missing keys resolve like Theme.getColor on that map (fallback key, then default colour).
199+ * <ul>
200+ * <li>A map of an installed theme's accent gets the rule iff {@link #isActiveFor}(theme, accent),
201+ * i.e. exactly when that accent gets it globally, so maps of stock accents stay stock.</li>
202+ * <li>A per-chat theme (an accent in theme.chatAccentsByThemeId, built by EmojiThemes.createColors)
203+ * gets the rule iff the global rule is active and the map is for the current light/dark mode,
204+ * so a themed chat's marks follow the user's style: stock when the user's accent is stock,
205+ * readable on the chat theme's own fills when the style is generated.</li>
206+ * </ul>
207+ */
208+ public static void applyToColorMap(Theme.ThemeInfo theme, Theme.ThemeAccent accent, SparseIntArray colors) {
209+ if (colors == null || accent == null) {
210+ return;
211+ }
212+ if (theme == null) {
213+ theme = accent.parentTheme;
214+ }
215+ if (isActiveFor(theme, accent) || isChatThemeAccent(theme, accent) && isGlobalActiveFor(theme)) {
216+ apply(theme, colors, false);
217+ }
218+ }
219+
220+ private static boolean isChatThemeAccent(Theme.ThemeInfo theme, Theme.ThemeAccent accent) {
221+ return theme != null && theme.chatAccentsByThemeId != null && theme.chatAccentsByThemeId.indexOfValue(accent) >= 0;
222+ }
223+
224+ private static boolean isGlobalActiveFor(Theme.ThemeInfo theme) {
225+ if (!globalActive || theme.isMonet()) {
226+ return false;
227+ }
228+ Theme.ThemeInfo active = Theme.getActiveTheme();
229+ return active != null && active.isDark() == theme.isDark();
230+ }
231+
232+ /**
233+ * For upstream code that draws a glyph or text in a fixed colour (usually white) on an accent fill,
234+ * such as the send buttons (ChatActivityEnterView.SendButton). Returns 0, meaning "keep the stock
235+ * colour", unless the global on-accent rule is active; then white or {@link #ON_ACCENT_DARK},
236+ * whichever contrasts more with the fill.
237+ *
238+ * @param fill the colour the glyph is drawn on, or 0 when it is not drawn on a fill
239+ */
240+ public static int getGlyphColorOnFill(int fill) {
241+ if (!globalActive || fill == 0) {
242+ return 0;
243+ }
244+ return onAccentContent(fill, Theme.getColor(Theme.key_windowBackgroundWhite) | 0xff000000);
245+ }
246+
247+ /**
248+ * Theme.getAssetFile hook: whether the copy of a bundled asset cached in filesDir is older than the
249+ * installed app. getAssetFile re-copies an asset only when its size changes, and the palette fix
250+ * changes some .attheme values without changing the file size (arctic, bluebubbles), so an update
251+ * over an older build would keep the old values. Answers true at most once per file and process,
252+ * so a wrong device clock costs one extra copy per start at worst.
253+ */
254+ public static boolean isStaleAssetCopy(File file) {
255+ synchronized (CHECKED_ASSETS) {
256+ if (!CHECKED_ASSETS.add(file.getName())) {
257+ return false;
258+ }
259+ if (appUpdateTime < 0) {
260+ try {
261+ Context context = ApplicationLoader.applicationContext;
262+ appUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
263+ } catch (Throwable e) {
264+ appUpdateTime = 0;
265+ }
266+ }
267+ return file.exists() && file.lastModified() < appUpdateTime;
268+ }
269+ }
270+
271+ /**
272+ * For resources providers that override some colours on top of the global theme: returns the
273+ * colour for a key the override map does not contain. When the global on-accent rule is active
274+ * and the map overrides the fill under this content key, the content is recomputed for that fill
275+ * instead of taking the global value (which was chosen for a different fill).
276+ */
277+ public static int getOverrideFallbackColor(SparseIntArray overrides, int key) {
278+ if (globalActive && overrides != null) {
279+ int fill = Keys.FILL_OF.get(key, -1);
280+ if (fill != -1) {
281+ int index = overrides.indexOfKey(fill);
282+ if (index >= 0) {
283+ int pageIndex = overrides.indexOfKey(Theme.key_windowBackgroundWhite);
284+ int page = pageIndex >= 0 ? overrides.valueAt(pageIndex) : Theme.getColor(Theme.key_windowBackgroundWhite);
285+ return onAccentContent(overrides.valueAt(index), page | 0xff000000);
286+ }
287+ }
288+ }
289+ return Theme.getColor(key);
290+ }
291+
292+ private static void apply(Theme.ThemeInfo theme, SparseIntArray colors, boolean global) {
293+ int[] pairs = Keys.ON_ACCENT;
294+ int page = resolve(colors, Theme.key_windowBackgroundWhite, global) | 0xff000000;
295+ int[] content = new int[pairs.length / 2];
296+ for (int i = 0; i < pairs.length; i += 2) {
297+ content[i / 2] = onAccentContent(resolve(colors, pairs[i + 1], global), page);
298+ }
299+ for (int i = 0; i < pairs.length; i += 2) {
300+ colors.put(pairs[i], content[i / 2]);
301+ }
302+ int[] muted = theme != null ? MUTED_FILL.get(theme.getKey()) : null;
303+ if (muted != null) {
304+ for (int key : Keys.MUTED_PINS) {
305+ if (colors.indexOfKey(key) < 0) {
306+ colors.put(key, resolve(colors, key, global));
307+ }
308+ }
309+ boolean whiteDigits = colors.get(Theme.key_chats_unreadCounterText) == ON_ACCENT_LIGHT;
310+ colors.put(Theme.key_chats_unreadCounterMuted, whiteDigits ? muted[0] : muted[1]);
311+ }
312+ }
313+
314+ private static int resolve(SparseIntArray colors, int key, boolean global) {
315+ if (global) {
316+ return Theme.getColor(key, null, true);
317+ }
318+ int index = colors.indexOfKey(key);
319+ if (index >= 0) {
320+ return colors.valueAt(index);
321+ }
322+ int fallbackKey = Theme.getFallbackKeys().get(key, -1);
323+ if (fallbackKey != -1) {
324+ index = colors.indexOfKey(fallbackKey);
325+ if (index >= 0) {
326+ return colors.valueAt(index);
327+ }
328+ }
329+ return Theme.getDefaultColor(key);
330+ }
331+
332+ /** White or {@link #ON_ACCENT_DARK}, whichever contrasts more with fill composited over page; ties go to white. */
333+ public static int onAccentContent(int fill, int page) {
334+ int f = compositeOver(fill, page);
335+ return contrast(ON_ACCENT_LIGHT, f) >= contrast(ON_ACCENT_DARK, f) ? ON_ACCENT_LIGHT : ON_ACCENT_DARK;
336+ }
337+
338+ /** Integer src-over of fg on an opaque bg, (a*x + (255-a)*y + 127) / 255 per channel; result is opaque. */
339+ public static int compositeOver(int fg, int bg) {
340+ int a = fg >>> 24;
341+ if (a == 255) {
342+ return fg;
343+ }
344+ int ia = 255 - a;
345+ int r = (((fg >> 16) & 255) * a + ((bg >> 16) & 255) * ia + 127) / 255;
346+ int g = (((fg >> 8) & 255) * a + ((bg >> 8) & 255) * ia + 127) / 255;
347+ int b = ((fg & 255) * a + (bg & 255) * ia + 127) / 255;
348+ return 0xff000000 | (r << 16) | (g << 8) | b;
349+ }
350+
351+ /** WCAG 2.2 relative luminance of the RGB part of color (alpha ignored). */
352+ public static double luminance(int color) {
353+ return 0.2126 * SRGB_LIN[(color >> 16) & 255] + 0.7152 * SRGB_LIN[(color >> 8) & 255] + 0.0722 * SRGB_LIN[color & 255];
354+ }
355+
356+ /** WCAG 2.2 contrast ratio of two opaque colours, 1.0 to 21.0. */
357+ public static double contrast(int color1, int color2) {
358+ double l1 = luminance(color1);
359+ double l2 = luminance(color2);
360+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
361+ }
362+}
TMessagesProj/src/main/java/org/telegram/ui/ActionBar/EmojiThemes.java+1
@@ -472,6 +472,7 @@ public class EmojiThemes {
472472 if (isGiftTheme() && accent.parentTheme != null && accent.parentTheme.isLight()) {
473473 accent.resetAccentColorsForMyMessagesGiftThemeLight(currentColors);
474474 }
475+ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
475476 } else {
476477 currentColors = currentColorsNoAccent;
477478 }
TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java+5−2
@@ -676,7 +676,7 @@ public class Theme {
676676 }
677677 }
678678 }
679− if (!isMyMessagesGradientColorsNear) {
679+ if (!isMyMessagesGradientColorsNear || org.telegram.messenger.multigram.PaletteFix.isRuntimeAccent(this)) { // MultiGram: runtime accents (generated/custom) always get readable black/white out-bubble texts
680680 if (myMessagesGradientAccentColor1 != 0) {
681681 int textColor;
682682 int subTextColor;
@@ -1041,6 +1041,7 @@ public class Theme {
10411041 SparseIntArray currentColorsNoAccent = getThemeFileValues(null, parentTheme.assetName, null);
10421042 SparseIntArray currentColors = currentColorsNoAccent.clone();
10431043 fillAccentColors(currentColorsNoAccent, currentColors);
1044+ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(parentTheme, this, currentColors); // MultiGram: on-accent contrast rule
10441045
10451046 String wallpaperLink = null;
10461047
@@ -3884,6 +3885,7 @@ public class Theme {
38843885 themeAccentExclusionKeys.add(key_stories_circle_dialog2);
38853886 themeAccentExclusionKeys.add(key_stories_circle_closeFriends1);
38863887 themeAccentExclusionKeys.add(key_stories_circle_closeFriends2);
3888+ org.telegram.messenger.multigram.PaletteFix.addAccentExclusions(themeAccentExclusionKeys); // MultiGram: neutral greys stay readable under any accent
38873889
38883890
38893891 themes = new ArrayList<>();
@@ -6105,6 +6107,7 @@ public class Theme {
61056107 }
61066108 applyCalculatedTableColors(currentColorsNoAccent, currentColors, currentTheme.isDark());
61076109 applyCalculatedArticleCodeColors(currentColorsNoAccent, currentColors, currentTheme.isDark());
6110+ org.telegram.messenger.multigram.PaletteFix.applyToCurrentColors(currentTheme, accent, currentColors); // MultiGram: on-accent contrast rule
61086111 if (!messages) {
61096112 boolean async = !(LaunchActivity.getLastFragment() instanceof ChatActivity);
61106113 reloadWallpaper(async);
@@ -7248,7 +7251,7 @@ public class Theme {
72487251 size = 0;
72497252 FileLog.e(e);
72507253 }
7251− if (!file.exists() || size != 0 && file.length() != size) {
7254+ 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)
72527255 try (InputStream in = ApplicationLoader.applicationContext.getAssets().open(assetName)) {
72537256 AndroidUtilities.copyFile(in, file);
72547257 } catch (Exception e) {
TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java+11−11
@@ -96,10 +96,10 @@ public class ThemeColors {
9696 defaultColors[key_text_RedRegular] = 0xffcc2929;
9797 defaultColors[key_text_RedBold] = 0xffcc4747;
9898 defaultColors[key_fill_RedNormal] = 0xffeb5e5e;
99− defaultColors[key_windowBackgroundWhiteGrayText] = 0xff808384;
100− defaultColors[key_windowBackgroundWhiteGrayText2] = 0xff82868a;
99+ defaultColors[key_windowBackgroundWhiteGrayText] = 0xff757778; // MultiGram: contrast fix, was 0xff808384
100+ defaultColors[key_windowBackgroundWhiteGrayText2] = 0xff73777b; // MultiGram: contrast fix, was 0xff82868a
101101 defaultColors[key_windowBackgroundWhiteGrayText3] = 0xff999999;
102− defaultColors[key_windowBackgroundWhiteGrayText4] = 0xff808080;
102+ defaultColors[key_windowBackgroundWhiteGrayText4] = 0xff6f6e6e; // MultiGram: contrast fix, was 0xff808080
103103 defaultColors[key_windowBackgroundWhiteGrayText5] = 0xffa3a3a3;
104104 defaultColors[key_windowBackgroundWhiteGrayText6] = 0xff757575;
105105 defaultColors[key_windowBackgroundWhiteGrayText7] = 0xffc6c6c6;
@@ -192,7 +192,7 @@ public class ThemeColors {
192192 defaultColors[key_actionBarActionModeDefaultTop] = 0x10000000;
193193 defaultColors[key_actionBarActionModeDefaultIcon] = DEFAULT_BLACK_TEXT; // key_windowBackgroundWhiteBlackText
194194 defaultColors[key_actionBarDefaultTitle] = DEFAULT_BLACK_TEXT;
195− defaultColors[key_actionBarDefaultSubtitle] = 0xff79817e; // key_windowBackgroundWhiteGrayText
195+ defaultColors[key_actionBarDefaultSubtitle] = 0xff717876; // key_windowBackgroundWhiteGrayText; MultiGram: contrast fix, was 0xff79817e
196196 defaultColors[key_actionBarDefaultSelector] = 0x121a1d21;
197197 defaultColors[key_actionBarWhiteSelector] = 0x121a1d21;
198198 defaultColors[key_actionBarDefaultSearch] = DEFAULT_BLACK_TEXT;
@@ -236,7 +236,7 @@ public class ThemeColors {
236236 defaultColors[key_chats_secretName] = 0xff00a60e;
237237 defaultColors[key_chats_secretIcon] = 0xff19b126;
238238 defaultColors[key_chats_pinnedIcon] = 0xff919294;
239− defaultColors[key_chats_message] = 0xff75787A;
239+ defaultColors[key_chats_message] = 0xff747779; // MultiGram: contrast fix, was 0xff75787A
240240 defaultColors[key_chats_messageArchived] = 0xff919191;
241241 defaultColors[key_chats_message_threeLines] = 0xff8e9091;
242242 defaultColors[key_chats_draft] = 0xffdd4b39;
@@ -246,7 +246,7 @@ public class ThemeColors {
246246 defaultColors[key_chats_nameMessageArchived_threeLines] = 0xff5e5e5e;
247247 defaultColors[key_chats_attachMessage] = TELEGRAM_COLOR_TEXT;
248248 defaultColors[key_chats_actionMessage] = TELEGRAM_COLOR_TEXT;
249− defaultColors[key_chats_date] = 0xff848688;
249+ defaultColors[key_chats_date] = 0xff757778; // MultiGram: contrast fix, was 0xff848688
250250 defaultColors[key_chats_date_bold] = 0xff919395;
251251 defaultColors[key_chats_pinnedOverlay] = 0x08000000;
252252 defaultColors[key_chats_tabletSelectedOverlay] = 0x0f000000;
@@ -330,7 +330,7 @@ public class ThemeColors {
330330 defaultColors[key_chat_inSentClockSelected] = 0xff93bdca;
331331 defaultColors[key_chat_mediaSentCheck] = 0xffffffff;
332332 defaultColors[key_chat_mediaSentClock] = 0xffffffff;
333− defaultColors[key_chat_inViews] = 0xffa1aab3;
333+ defaultColors[key_chat_inViews] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
334334 defaultColors[key_chat_inViewsSelected] = 0xff93bdca;
335335 defaultColors[key_chat_outViews] = 0xff6eb257;
336336 defaultColors[key_chat_outViewsSelected] = 0xff6eb257;
@@ -395,7 +395,7 @@ public class ThemeColors {
395395 defaultColors[key_chat_inAdminSelectedText] = 0xff89b4c1;
396396 defaultColors[key_chat_outAdminText] = 0xff70b15c;
397397 defaultColors[key_chat_outAdminSelectedText] = 0xff70b15c;
398− defaultColors[key_chat_inTimeText] = 0xffa1aab3;
398+ defaultColors[key_chat_inTimeText] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
399399 defaultColors[key_chat_inTimeSelectedText] = 0xff89b4c1;
400400 defaultColors[key_chat_outTimeText] = 0xff70b15c;
401401 defaultColors[key_chat_outTimeSelectedText] = 0xff70b15c;
@@ -429,7 +429,7 @@ public class ThemeColors {
429429 defaultColors[key_chat_outFileProgressSelected] = 0xffc5eca7;
430430 defaultColors[key_chat_inFileNameText] = TELEGRAM_COLOR_TEXT;
431431 defaultColors[key_chat_outFileNameText] = 0xff55ab4f;
432− defaultColors[key_chat_inFileInfoText] = 0xffa1aab3;
432+ defaultColors[key_chat_inFileInfoText] = 0xff6f7780; // MultiGram: contrast fix, was 0xffa1aab3
433433 defaultColors[key_chat_outFileInfoText] = 0xff65b05b;
434434 defaultColors[key_chat_inFileInfoSelectedText] = 0xff89b4c1;
435435 defaultColors[key_chat_outFileInfoSelectedText] = 0xff65b05b;
@@ -485,7 +485,7 @@ public class ThemeColors {
485485 defaultColors[key_chat_replyPanelLine] = 0xffe8e8e8;
486486 defaultColors[key_chat_messagePanelBackground] = 0xffffffff;
487487 defaultColors[key_chat_messagePanelText] = 0xff000000;
488− defaultColors[key_chat_messagePanelHint] = 0xff858a84;
488+ defaultColors[key_chat_messagePanelHint] = 0xff737873; // MultiGram: contrast fix, was 0xff858a84
489489 defaultColors[key_chat_messagePanelCursor] = 0xff54a1db;
490490 defaultColors[key_chat_messagePanelShadow] = 0xff000000;
491491 defaultColors[key_chat_messagePanelIcons] = 0xff8e959b;
@@ -517,7 +517,7 @@ public class ThemeColors {
517517 defaultColors[key_chat_topPanelClose] = 0xff818786;
518518 defaultColors[key_chat_topPanelLine] = 0xff3fa8ef;
519519 defaultColors[key_chat_topPanelTitle] = TELEGRAM_COLOR_TEXT;
520− defaultColors[key_chat_topPanelMessage] = 0xff767e7c;
520+ defaultColors[key_chat_topPanelMessage] = 0xff717877; // MultiGram: contrast fix, was 0xff767e7c
521521 defaultColors[key_chat_addContact] = TELEGRAM_COLOR_TEXT;
522522 defaultColors[key_chat_inLoader] = TELEGRAM_COLOR;
523523 defaultColors[key_chat_inLoaderSelected] = 0xff65abe0;
TMessagesProj/src/main/java/org/telegram/ui/ChannelColorActivity.java+1
@@ -2580,6 +2580,7 @@ public class ChannelColorActivity extends BaseFragment implements NotificationCe
25802580 Theme.ThemeAccent accent = themeInfo.getAccent(false);
25812581 if (accent != null) {
25822582 accent.fillAccentColors(themeColors, currentColors);
2583+ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
25832584 }
25842585 }
25852586 dividerPaint.setColor(Theme.getColor(Theme.key_divider, resourceProvider));
TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java+9−4
@@ -15509,7 +15509,7 @@ public class ChatActivityEnterView extends FrameLayout implements
1550915509 }
1551015510 canvas.clipPath(path);
1551115511 if (loadingShown > 0) {
15512− loadingPaint.setColor(0xFFFFFFFF);
15512+ loadingPaint.setColor(multigramGlyphColor != 0 ? multigramGlyphColor : 0xFFFFFFFF); // MultiGram: was 0xFFFFFFFF
1551315513 loadingPaint.setAlpha((int) (0xFF * loadingShown));
1551415514 final float R = dp(8.66f);
1551515515 AndroidUtilities.rectTmp.set(right - w / 2.0f - R, cy - R, right - w / 2.0f + R, cy + R);
@@ -15672,15 +15672,20 @@ public class ChatActivityEnterView extends FrameLayout implements
1567215672 }
1567315673
1567415674 private int drawableColor;
15675+ private int multigramGlyphColor; // MultiGram: glyph colour on the fill for generated styles, 0 = stock
1567515676
1567615677 public void updateColors() {
15677− int color = isNewDesignSendButton ? Color.WHITE : Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider);
15678− if (color != drawableColor) {
15678+ 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
15679+ int color = isNewDesignSendButton ? (glyph != 0 ? glyph : Color.WHITE) : Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider); // MultiGram: was Color.WHITE for the new design
15680+ if (color != drawableColor || glyph != multigramGlyphColor) { // MultiGram: was color != drawableColor
1567915681 drawableColor = color;
15682+ multigramGlyphColor = glyph; // MultiGram
1568015683 drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
1568115684 int c = Theme.getColor(Theme.key_glass_defaultIcon, resourcesProvider);
1568215685 inactiveDrawable.setColorFilter(new PorterDuffColorFilter(Color.argb(0xb4, Color.red(c), Color.green(c), Color.blue(c)), PorterDuff.Mode.SRC_IN));
15683− drawableInverse.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoicePressed, resourcesProvider), PorterDuff.Mode.SRC_IN));
15686+ drawableInverse.setColorFilter(new PorterDuffColorFilter(glyph != 0 ? glyph : Theme.getColor(Theme.key_chat_messagePanelVoicePressed, resourcesProvider), PorterDuff.Mode.SRC_IN)); // MultiGram: was the voicePressed colour only
15687+ count.setTextColor(glyph != 0 && !isNewDesignSendButton ? glyph : 0xFFFFFFFF); // MultiGram: counter digits on the fill (old design)
15688+ priceText.setTextColor(glyph != 0 ? glyph : 0xFFFFFFFF); // MultiGram: star price on the fill
1568415689 }
1568515690 if (isNewDesignSendButton) {
1568615691 backgroundPaint.setColor(Theme.getColor(Theme.key_chat_messagePanelSend, resourcesProvider));
TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/MessageEntityView.java+1
@@ -1397,6 +1397,7 @@ public class MessageEntityView extends EntityView {
13971397 Theme.ThemeAccent accent = themeInfo.getAccent(false);
13981398 if (accent != null) {
13991399 accent.fillAccentColors(themeColors, currentColors);
1400+ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
14001401 }
14011402 }
14021403
TMessagesProj/src/main/java/org/telegram/ui/PeerColorActivity.java+1
@@ -1494,6 +1494,7 @@ public class PeerColorActivity extends BaseFragment implements NotificationCente
14941494 Theme.ThemeAccent accent = themeInfo.getAccent(false);
14951495 if (accent != null) {
14961496 accent.fillAccentColors(themeColors, currentColors);
1497+ org.telegram.messenger.multigram.PaletteFix.applyToColorMap(themeInfo, accent, currentColors); // MultiGram: on-accent contrast rule
14971498 }
14981499
14991500 if (namePage != null && namePage.messagesCellPreview != null) {
TMessagesProj/src/main/java/org/telegram/ui/Stories/DarkThemeResourceProvider.java+1−1
@@ -202,7 +202,7 @@ public class DarkThemeResourceProvider implements Theme.ResourcesProvider {
202202 if (!debugUnknownKeys.contains(key)) {
203203 debugUnknownKeys.add(key);
204204 }
205− return Theme.getColor(key);
205+ return org.telegram.messenger.multigram.PaletteFix.getOverrideFallbackColor(sparseIntArray, key); // MultiGram: was Theme.getColor(key); marks on fills overridden here stay readable
206206 }
207207
208208 Drawable msgOutMedia;
multigram/palette-fix/README.mdnew file+70
@@ -0,0 +1,70 @@
1+# Palette fix (overlay A)
2+
3+Stock Telegram colours already fail WCAG contrast on many text/background pairs. MultiGram gives every
4+install a generated accent, and those accents are only readable on top of this fix. The values come from
5+the colour-engine simulator (research folder `random-style/palette-fix`, overlay A); `overlay_A.json` here
6+is a copy of that spec.
7+
8+## What changes
9+
10+| Part | Where | Applies to |
11+|---|---|---|
12+| 11 `ThemeColors` defaults (grey secondary texts) | `ThemeColors.createDefaultColors()`, each line marked `MultiGram: contrast fix, was ...` | every theme without its own value, including Blue preset 99 |
13+| 36 lines in the five bundled `.attheme` files | `assets/{bluebubbles,darkblue,arctic,day,night}.attheme` | those themes, every accent |
14+| 13 accent-exclusion keys (neutral greys, `windowBackgroundGray`) | `PaletteFix.addAccentExclusions`, called from Theme's static initializer | every non-default accent of the five themes |
15+| On-accent rule: 12 marks on accent fills become white or `#050505`, whichever contrasts more; muted unread badge gets a per-theme neutral fill | `PaletteFix.applyToCurrentColors` / `applyToColorMap` | runtime accents only (see below); per-chat themes while a runtime accent is active |
16+| Not in overlay A: a 13th pair, the voice-record glyphs (`chat_messagePanelVoicePressed` on `chat_messagePanelVoiceBackground`), and the send-button glyphs, which upstream draws in hard-coded white | `PaletteFix` `ON_ACCENT`; `ChatActivityEnterView.SendButton.updateColors` via `getGlyphColorOnFill` | while a runtime accent is active |
17+| Out-bubble black/white text block also for runtime accents | `Theme.ThemeAccent.fillAccentColors`: `if (!isMyMessagesGradientColorsNear \|\| PaletteFix.isRuntimeAccent(this))` | runtime accents only |
18+| Bundled `.attheme` copies are refreshed after an app update | `Theme.getAssetFile` via `PaletteFix.isStaleAssetCopy` | every bundled theme |
19+
20+**Runtime accents** (`PaletteFix.isRuntimeAccent`): id > 100 and no server theme behind the accent
21+(`accent.info == null`), or a server theme this user created (`info.creator`, a custom accent the user
22+shared). That is MultiGram's generated accents and accents made in the theme editor. Telegram's own
23+server themes (the Chat Settings carousel, `account.getThemes`) and cloud themes installed from someone
24+else's link also get ids above 100, but they carry their `TL_theme` in `accent.info` and stay stock.
25+Stock presets have ids up to 99; id 100 is a custom accent migrated from very old app versions and
26+counts as stock. On top of that, `isActiveFor` is false for Monet themes and, on Blue, for an accent
27+with the default accent colour (`Theme.getColor` serves the stock defaults for it anyway).
28+
29+**Per-chat themes** (`EmojiThemes.createColors`, accents in `ThemeInfo.chatAccentsByThemeId`) get the
30+rule when the global rule is active and the map is for the current light/dark mode. So a themed chat
31+looks exactly as in Forkgram for a user on a stock accent, and gets readable marks on the chat theme's
32+own fills for a user on a generated style. The map `createColors` returns is complete (it fills every
33+key from `Theme.getDefaultColors()`), so a themed chat never falls back to the global colours.
34+
35+The 12 on-accent `ThemeColors` defaults keep their stock value (white): the rule sets them at run time, so
36+stock accents look exactly as in Forkgram. Monet themes define all 11 changed keys themselves and have no
37+accents, so they are unaffected.
38+
39+## Hooks in upstream files (small, every changed line marked `MultiGram:`)
40+
41+- `Theme.java`: the `isRuntimeAccent(this)` guard in `fillAccentColors`; `addAccentExclusions` after
42+ the last `themeAccentExclusionKeys.add`; `applyToCurrentColors` at the end of `refreshThemeColors`
43+ (after the calculated table/article colours); `applyToColorMap` in `ThemeAccent.saveToFile`
44+ (exported themes); `isStaleAssetCopy` in `getAssetFile`, which otherwise re-copies a bundled
45+ `.attheme` only when its size changes (the arctic and bluebubbles edits keep the size).
46+- `EmojiThemes.createColors`, `PeerColorActivity`, `ChannelColorActivity`, `MessageEntityView`:
47+ `applyToColorMap` right after `fillAccentColors`, so chat-theme and preview colour maps follow the
48+ rule as described above.
49+- `Components/ChatActivityEnterView.SendButton.updateColors` (a field, five lines in `updateColors`
50+ and the loading-arc colour in `onDraw`): the glyph, star price, counter digits and loading arc drawn
51+ on the send button's fill use `PaletteFix.getGlyphColorOnFill(fill)` while the global rule is
52+ active. This covers the chat composer (new design, `chat_messagePanelSend`) and the old-design send
53+ buttons of the attach, share, forward, gift and photo-viewer sheets (`getFillColor()`).
54+- `Stories/DarkThemeResourceProvider.getColor`: keys the provider does not override go through
55+ `PaletteFix.getOverrideFallbackColor`, so a mark whose fill the provider overrides (for example the
56+ group-call checkbox) is recomputed for that fill instead of taking the global one.
57+
58+Not hooked, on purpose: `EmojiThemes.getPreviewColors` (only bubble, button and wallpaper colours),
59+static `EmojiThemes.getPreviewColors(ThemeInfo)` (no callers), `Stories/recorder/PreviewView`
60+(wallpaper only), `ThemePreviewDrawable` (no on-accent marks drawn).
61+
62+## Replaying onto a new Forkgram release
63+
64+Locate every hook by content, not line number. Then run:
65+
66+ python3 multigram/tools/palette_fix_check.py --base <new forkgram snapshot>
67+
68+It checks the values against `overlay_A.json`, the key tables in `PaletteFix.java`, that each hook is
69+present once, and that nothing else in `ThemeColors.java` or the `.attheme` files changed. If upstream
70+changed a stock value that the overlay replaces, re-run the simulator before accepting it.
multigram/palette-fix/overlay_A.jsonnew file+178
@@ -0,0 +1,178 @@
1+{
2+ "attheme": {
3+ "arctic.attheme": {
4+ "key_actionBarDefaultSubtitle": 4285626239,
5+ "key_chat_inFileInfoText": 4285495168,
6+ "key_chat_topPanelMessage": 4285691770,
7+ "key_windowBackgroundWhiteGrayText": 4285495170
8+ },
9+ "bluebubbles.attheme": {
10+ "key_actionBarDefaultSubtitle": 4285495169,
11+ "key_windowBackgroundWhiteGrayText": 4285822841,
12+ "key_windowBackgroundWhiteGrayText2": 4285822841,
13+ "key_windowBackgroundWhiteGrayText4": 4285296240
14+ },
15+ "darkblue.attheme": {
16+ "key_actionBarDefaultSubtitle": 2463888127,
17+ "key_chat_inFileInfoText": 4287931827,
18+ "key_chat_inTimeText": 4287931572,
19+ "key_chat_inViews": 4287931827,
20+ "key_chat_messagePanelHint": 2530996223,
21+ "key_chat_topPanelMessage": 2480731391,
22+ "key_chats_attachMessage": 4288256409,
23+ "key_chats_date": 4287535782,
24+ "key_chats_message": 4287404969,
25+ "key_windowBackgroundWhiteGrayText": 4287404969,
26+ "key_windowBackgroundWhiteGrayText2": 2213674495,
27+ "key_windowBackgroundWhiteGrayText4": 4085091225
28+ },
29+ "day.attheme": {
30+ "key_actionBarDefaultSubtitle": 4285626486,
31+ "key_chat_inFileInfoText": 4285295984,
32+ "key_chat_inTimeText": 4285295985,
33+ "key_chat_inViews": 4285230705,
34+ "key_chat_topPanelMessage": 4285626487,
35+ "key_windowBackgroundWhiteGrayText": 4285888376
36+ },
37+ "night.attheme": {
38+ "key_actionBarDefaultSubtitle": 2163405554,
39+ "key_chat_inFileInfoText": 4287203723,
40+ "key_chat_inTimeText": 4287072908,
41+ "key_chat_inViews": 4286941838,
42+ "key_chat_messagePanelHint": 1979711487,
43+ "key_chat_topPanelMessage": 1962934271,
44+ "key_chats_date": 4286677376,
45+ "key_windowBackgroundWhiteGrayText": 1962934271,
46+ "key_windowBackgroundWhiteGrayText2": 1962802687,
47+ "key_windowBackgroundWhiteGrayText4": 4285887604
48+ }
49+ },
50+ "defaults": {
51+ "key_actionBarDefaultSubtitle": 4285626486,
52+ "key_chat_attachCheckBoxCheck": 4278519045,
53+ "key_chat_goDownButtonCounter": 4278519045,
54+ "key_chat_inContactIcon": 4278519045,
55+ "key_chat_inFileInfoText": 4285495168,
56+ "key_chat_inMediaIcon": 4278519045,
57+ "key_chat_inTimeText": 4285495168,
58+ "key_chat_inViews": 4285495168,
59+ "key_chat_messagePanelHint": 4285757555,
60+ "key_chat_topPanelMessage": 4285626487,
61+ "key_chats_actionIcon": 4278519045,
62+ "key_chats_date": 4285888376,
63+ "key_chats_message": 4285822841,
64+ "key_chats_unreadCounterText": 4278519045,
65+ "key_checkboxSquareCheck": 4278519045,
66+ "key_dialogCheckboxSquareCheck": 4278519045,
67+ "key_dialogFloatingIcon": 4278519045,
68+ "key_dialogRoundCheckBoxCheck": 4278519045,
69+ "key_featuredStickers_buttonText": 4278519045,
70+ "key_picker_badgeText": 4278519045,
71+ "key_windowBackgroundWhiteGrayText": 4285888376,
72+ "key_windowBackgroundWhiteGrayText2": 4285757307,
73+ "key_windowBackgroundWhiteGrayText4": 4285492846
74+ },
75+ "exclusions": [
76+ "key_windowBackgroundGray",
77+ "key_windowBackgroundWhiteGrayText",
78+ "key_windowBackgroundWhiteGrayText2",
79+ "key_chats_message",
80+ "key_chats_date",
81+ "key_actionBarDefaultSubtitle",
82+ "key_chat_inTimeText",
83+ "key_chat_inFileInfoText",
84+ "key_chat_messagePanelHint",
85+ "key_windowBackgroundWhiteGrayText4",
86+ "key_chat_topPanelMessage",
87+ "key_chat_inViews",
88+ "key_chat_messagePanelIcons"
89+ ],
90+ "hook": {
91+ "muted": {
92+ "key": "key_chats_unreadCounterMuted",
93+ "pins": [
94+ "key_avatar_backgroundArchived",
95+ "key_chats_archivePinBackground",
96+ "key_chats_archivePullDownBackground",
97+ "key_checkboxDisabled"
98+ ],
99+ "table": {
100+ "Arctic Blue": [
101+ 4285757307,
102+ 4290692039
103+ ],
104+ "Blue": [
105+ 4285757307,
106+ 4290692039
107+ ],
108+ "Dark Blue": [
109+ 4282274403,
110+ 4284709259
111+ ],
112+ "Day": [
113+ 4285757307,
114+ 4290692039
115+ ],
116+ "Night": [
117+ 4282729797,
118+ 4286019190
119+ ]
120+ },
121+ "text": "key_chats_unreadCounterText"
122+ },
123+ "near_black": 4278519045,
124+ "out_block_custom": true,
125+ "pairs": [
126+ [
127+ "key_chats_actionIcon",
128+ "key_chats_actionBackground"
129+ ],
130+ [
131+ "key_chats_unreadCounterText",
132+ "key_chats_unreadCounter"
133+ ],
134+ [
135+ "key_featuredStickers_buttonText",
136+ "key_featuredStickers_addButton"
137+ ],
138+ [
139+ "key_checkboxSquareCheck",
140+ "key_checkboxSquareBackground"
141+ ],
142+ [
143+ "key_dialogRoundCheckBoxCheck",
144+ "key_dialogRoundCheckBox"
145+ ],
146+ [
147+ "key_chat_goDownButtonCounter",
148+ "key_chat_goDownButtonCounterBackground"
149+ ],
150+ [
151+ "key_dialogCheckboxSquareCheck",
152+ "key_dialogCheckboxSquareBackground"
153+ ],
154+ [
155+ "key_chat_attachCheckBoxCheck",
156+ "key_chat_attachCheckBoxBackground"
157+ ],
158+ [
159+ "key_chat_inMediaIcon",
160+ "key_chat_inLoader"
161+ ],
162+ [
163+ "key_chat_inContactIcon",
164+ "key_chat_inContactBackground"
165+ ],
166+ [
167+ "key_picker_badgeText",
168+ "key_picker_badge"
169+ ],
170+ [
171+ "key_dialogFloatingIcon",
172+ "key_dialogFloatingButton"
173+ ]
174+ ]
175+ },
176+ "tag": "canvas-A",
177+ "variant": "A"
178+}
multigram/tools/palette_fix_check.pynew file+190
@@ -0,0 +1,190 @@
1+#!/usr/bin/env python3
2+"""Check that the MultiGram palette fix (overlay A) is fully applied to this source tree.
3+
4+Run from the repository root:
5+
6+ python3 multigram/tools/palette_fix_check.py # values, key tables and hooks
7+ python3 multigram/tools/palette_fix_check.py --base REV # also: nothing else changed vs REV
8+
9+What it checks, against multigram/palette-fix/overlay_A.json:
10+ * ThemeColors.createDefaultColors(): the overlay's defaults, except the 12 on-accent content keys,
11+ which keep their stock value (the on-accent rule sets them at run time for runtime accents).
12+ * The five bundled .attheme files: the overlay's values.
13+ * PaletteFix.java: accent exclusions, on-accent pairs (the overlay's 12 plus EXTRA_PAIRS), muted
14+ pins and muted fill table.
15+ * The one-line hooks in upstream files.
16+ * With --base: the diff of ThemeColors.java and the .attheme files against REV touches only the
17+ lines above.
18+
19+Standard library only. Exit status 0 when everything matches, 1 otherwise.
20+"""
21+import argparse
22+import json
23+import os
24+import re
25+import subprocess
26+import sys
27+
28+ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
29+SPEC = os.path.join(ROOT, "multigram", "palette-fix", "overlay_A.json")
30+JAVA = "TMessagesProj/src/main/java/org/telegram"
31+THEME = JAVA + "/ui/ActionBar/Theme.java"
32+THEME_COLORS = JAVA + "/ui/ActionBar/ThemeColors.java"
33+PALETTE_FIX = JAVA + "/messenger/multigram/PaletteFix.java"
34+ASSETS = "TMessagesProj/src/main/assets/"
35+
36+# On-accent pairs MultiGram adds after the overlay's 12 (content drawn on the voice-record fill).
37+EXTRA_PAIRS = [["key_chat_messagePanelVoicePressed", "key_chat_messagePanelVoiceBackground"]]
38+
39+# (file, regex that must match exactly once)
40+HOOKS = [
41+ (THEME, r"if \(!isMyMessagesGradientColorsNear \|\| org\.telegram\.messenger\.multigram\.PaletteFix\.isRuntimeAccent\(this\)\) \{ // MultiGram:"),
42+ (THEME, r"file\.length\(\) != size \|\| org\.telegram\.messenger\.multigram\.PaletteFix\.isStaleAssetCopy\(file\)\) \{ // MultiGram:"),
43+ (THEME, r"PaletteFix\.addAccentExclusions\(themeAccentExclusionKeys\); // MultiGram:"),
44+ (THEME, r"PaletteFix\.applyToCurrentColors\(currentTheme, accent, currentColors\); // MultiGram:"),
45+ (THEME, r"PaletteFix\.applyToColorMap\(parentTheme, this, currentColors\); // MultiGram:"),
46+ (JAVA + "/ui/ActionBar/EmojiThemes.java", r"PaletteFix\.applyToColorMap\(themeInfo, accent, currentColors\); // MultiGram:"),
47+ (JAVA + "/ui/PeerColorActivity.java", r"PaletteFix\.applyToColorMap\(themeInfo, accent, currentColors\); // MultiGram:"),
48+ (JAVA + "/ui/ChannelColorActivity.java", r"PaletteFix\.applyToColorMap\(themeInfo, accent, currentColors\); // MultiGram:"),
49+ (JAVA + "/ui/Components/Paint/Views/MessageEntityView.java", r"PaletteFix\.applyToColorMap\(themeInfo, accent, currentColors\); // MultiGram:"),
50+ (JAVA + "/ui/Stories/DarkThemeResourceProvider.java", r"PaletteFix\.getOverrideFallbackColor\(sparseIntArray, key\); // MultiGram:"),
51+ (JAVA + "/ui/Components/ChatActivityEnterView.java", r"int glyph = org\.telegram\.messenger\.multigram\.PaletteFix\.getGlyphColorOnFill\(.*\); // MultiGram:"),
52+ (JAVA + "/ui/Components/ChatActivityEnterView.java", r"new PorterDuffColorFilter\(glyph != 0 \? glyph : Theme\.getColor\(Theme\.key_chat_messagePanelVoicePressed, resourcesProvider\), PorterDuff\.Mode\.SRC_IN\)\); // MultiGram:"),
53+]
54+
55+errors = []
56+
57+
58+def fail(msg):
59+ errors.append(msg)
60+
61+
62+def read(rel):
63+ with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
64+ return f.read()
65+
66+
67+def u32(v):
68+ return v & 0xFFFFFFFF
69+
70+
71+def s32(v):
72+ v = u32(v)
73+ return v - (1 << 32) if v & 0x80000000 else v
74+
75+
76+def java_int(tok):
77+ tok = tok.strip().rstrip("Ll")
78+ return u32(int(tok, 16) if tok.lower().startswith("0x") else int(tok))
79+
80+
81+def themecolors_defaults(src):
82+ out = {}
83+ for m in re.finditer(r"defaultColors\[(key_\w+)\]\s*=\s*(0x[0-9a-fA-F]+|-?\d+)\s*;", src):
84+ out.setdefault(m.group(1), []).append(java_int(m.group(2)))
85+ return out
86+
87+
88+def attheme_values(text):
89+ out = {}
90+ for line in text.split("\n"):
91+ if line.startswith("WPS"):
92+ break
93+ k, sep, v = line.partition("=")
94+ if sep and re.fullmatch(r"-?\d+", v.strip()):
95+ out.setdefault("key_" + k, []).append(u32(int(v)))
96+ return out
97+
98+
99+def check_values(spec):
100+ content_keys = {a for a, _ in spec["hook"]["pairs"]}
101+ tc = themecolors_defaults(read(THEME_COLORS))
102+ for key, val in sorted(spec["defaults"].items()):
103+ got = tc.get(key, [])
104+ want = 0xFFFFFFFF if key in content_keys else u32(val) # on-accent defaults stay stock white
105+ if got != [want]:
106+ fail("ThemeColors %s = %s, expected 0x%08x" % (key, ["0x%08x" % g for g in got], want))
107+ for asset, kv in sorted(spec["attheme"].items()):
108+ vals = attheme_values(read(ASSETS + asset))
109+ for key, val in sorted(kv.items()):
110+ got = vals.get(key, [])
111+ if got != [u32(val)]:
112+ fail("%s %s = %s, expected %d" % (asset, key[4:], [s32(g) for g in got], s32(val)))
113+
114+
115+def check_palette_fix(spec):
116+ src = read(PALETTE_FIX)
117+ m = re.search(r"addAccentExclusions\([^)]*\)\s*\{(.*?)\n \}", src, re.S)
118+ excl = re.findall(r"exclusions\.add\(Theme\.(key_\w+)\)", m.group(1)) if m else []
119+ if sorted(excl) != sorted(spec["exclusions"]) or len(excl) != len(set(excl)):
120+ fail("PaletteFix exclusions %s != overlay %s" % (sorted(excl), sorted(spec["exclusions"])))
121+ m = re.search(r"ON_ACCENT\s*=\s*\{(.*?)\};", src, re.S)
122+ keys = re.findall(r"Theme\.(key_\w+)", m.group(1)) if m else []
123+ pairs = [list(p) for p in zip(keys[0::2], keys[1::2])]
124+ if pairs != spec["hook"]["pairs"] + EXTRA_PAIRS:
125+ fail("PaletteFix ON_ACCENT pairs differ from the overlay's plus EXTRA_PAIRS")
126+ m = re.search(r"MUTED_PINS\s*=\s*\{(.*?)\};", src, re.S)
127+ pins = re.findall(r"Theme\.(key_\w+)", m.group(1)) if m else []
128+ if pins != spec["hook"]["muted"]["pins"]:
129+ fail("PaletteFix MUTED_PINS %s != overlay %s" % (pins, spec["hook"]["muted"]["pins"]))
130+ m = re.search(r"ON_ACCENT_DARK\s*=\s*(0x[0-9a-fA-F]+)", src)
131+ if not m or java_int(m.group(1)) != u32(spec["hook"]["near_black"]):
132+ fail("PaletteFix ON_ACCENT_DARK != overlay near_black")
133+ arrays = {n: [java_int(x) for x in re.findall(r"0x[0-9a-fA-F]+", body)]
134+ for n, body in re.findall(r"int\[\]\s+(\w+)\s*=\s*\{([^}]*)\}", src)}
135+ table = {}
136+ for theme, expr in re.findall(r'MUTED_FILL\.put\("([^"]+)",\s*([^;]+)\);', src):
137+ vals = [java_int(x) for x in re.findall(r"0x[0-9a-fA-F]+", expr)] or arrays.get(expr.strip(), [])
138+ table[theme] = vals
139+ want = {t: [u32(v) for v in vs] for t, vs in spec["hook"]["muted"]["table"].items()}
140+ if table != want:
141+ fail("PaletteFix MUTED_FILL %s != overlay %s" % (table, want))
142+
143+
144+def check_hooks():
145+ for rel, pat in HOOKS:
146+ n = len(re.findall(pat, read(rel)))
147+ if n != 1:
148+ fail("hook %r found %d times in %s" % (pat, n, rel))
149+
150+
151+def changed_lines(base, rel):
152+ diff = subprocess.run(["git", "-C", ROOT, "diff", "-U0", base, "--", rel],
153+ check=True, capture_output=True, text=True).stdout
154+ return [l[1:] for l in diff.split("\n") if l.startswith("+") and not l.startswith("+++")]
155+
156+
157+def check_diff(spec, base):
158+ content_keys = {a for a, _ in spec["hook"]["pairs"]}
159+ allowed = {k for k in spec["defaults"] if k not in content_keys}
160+ for line in changed_lines(base, THEME_COLORS):
161+ m = re.match(r"\s*defaultColors\[(key_\w+)\]", line)
162+ if not m or m.group(1) not in allowed:
163+ fail("unexpected ThemeColors change: %s" % line.strip())
164+ for asset in ["arctic.attheme", "bluebubbles.attheme", "darkblue.attheme", "day.attheme", "night.attheme",
165+ "monet_light.attheme", "monet_dark.attheme"]:
166+ allowed = {k[4:] for k in spec["attheme"].get(asset, {})}
167+ for line in changed_lines(base, ASSETS + asset):
168+ if line.partition("=")[0] not in allowed:
169+ fail("unexpected %s change: %s" % (asset, line.strip()))
170+
171+
172+def main():
173+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
174+ ap.add_argument("--base", help="git revision of the unpatched Forkgram snapshot")
175+ args = ap.parse_args()
176+ with open(SPEC) as f:
177+ spec = json.load(f)
178+ check_values(spec)
179+ check_palette_fix(spec)
180+ check_hooks()
181+ if args.base:
182+ check_diff(spec, args.base)
183+ for e in errors:
184+ print("palette-fix: " + e)
185+ print("palette-fix: %s" % ("FAILED, %d problem(s)" % len(errors) if errors else "OK"))
186+ return 1 if errors else 0
187+
188+
189+if __name__ == "__main__":
190+ sys.exit(main())