From 9029a823e1cd080d971a160b1d77e75cbe8e8369 Mon Sep 17 00:00:00 2001 From: MiMoHo <37556964+MiMoHo@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:51:33 +0200 Subject: [PATCH 1/5] Restore standalone regional indicators at the end of the flags category They were removed from the symbols group in 1680 with the agreement to restore them if requested (2627), preferably in the flags category: two of them in a row compose a country flag, so they sit next to what they build. Injected by the generator after parsing, since Unicode's emoji-test.txt only lists the composed flags; support entries restored in the API 19 section, so minApi.txt is unaffected. Verified by running make-emoji-keys into a scratch dir: output is byte-identical to the checked-in assets. Co-Authored-By: Claude Fable 5 --- app/src/main/assets/emoji/FLAGS.txt | 28 ++++++++++++++++++- .../inputmethod/tools/emoji/MakeEmojiKeys.kt | 1 + .../tools/emoji/model/EmojiData.kt | 14 ++++++++++ .../resources/emoji/android-emoji-support.txt | 26 +++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/emoji/FLAGS.txt b/app/src/main/assets/emoji/FLAGS.txt index c4bd834609..a58892d413 100644 --- a/app/src/main/assets/emoji/FLAGS.txt +++ b/app/src/main/assets/emoji/FLAGS.txt @@ -267,4 +267,30 @@ 🇿🇼 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 -🏴󠁧󠁢󠁷󠁬󠁳󠁿 \ No newline at end of file +🏴󠁧󠁢󠁷󠁬󠁳󠁿 +🇦 +🇧 +🇨 +🇩 +🇪 +🇫 +🇬 +🇭 +🇮 +🇯 +🇰 +🇱 +🇲 +🇳 +🇴 +🇵 +🇶 +🇷 +🇸 +🇹 +🇺 +🇻 +🇼 +🇽 +🇾 +🇿 diff --git a/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/MakeEmojiKeys.kt b/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/MakeEmojiKeys.kt index ce11e9c505..0ae333b7f0 100644 --- a/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/MakeEmojiKeys.kt +++ b/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/MakeEmojiKeys.kt @@ -49,6 +49,7 @@ class MakeEmojiKeys { val parser = EmojiUCDTestFileParser() parser.parse(JarUtils.getLatestEmojiTestResource(jar)) val emojis = parser.getParsedData() + emojis.appendRegionalIndicators() val parser2 = AndroidEmojiSupportFileParser() parser2.parse(JarUtils.getEmojiSupportResource(jar)) diff --git a/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/model/EmojiData.kt b/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/model/EmojiData.kt index d4f73733a4..5919cc38df 100644 --- a/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/model/EmojiData.kt +++ b/tools/make-emoji-keys/src/main/kotlin/com/majeur/inputmethod/tools/emoji/model/EmojiData.kt @@ -21,6 +21,17 @@ class EmojiData { fun emojiGroupCount(group: EmojiGroup) = emojiGroups[group]?.size ?: 0 + /** Unicode's emoji-test.txt only lists the composed country flags, but Android also supports + * the standalone regional indicator letters (two of them in a row combine into a flag). + * They were removed from the symbols group in #1680 with the promise to restore them if + * requested (#2627), so append them to the end of the flags group as suggested there. */ + fun appendRegionalIndicators() { + (CP_REGIONAL_INDICATOR_SYMBOL_LETTER_A..CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Z).forEach { cp -> + val letter = 'a' + (cp - CP_REGIONAL_INDICATOR_SYMBOL_LETTER_A) + insertEmoji(EmojiGroup.FLAGS, intArrayOf(cp), 2.0f, "regional indicator symbol letter $letter") + } + } + fun insertEmoji(group: EmojiGroup, codes: IntArray, unicodeVer: Float, name: String): EmojiSpec { return EmojiSpec(codes, unicodeVer, name).also { emoji -> val baseEmoji = findBaseEmoji(group, emoji) @@ -103,5 +114,8 @@ class EmojiData { private const val CP_WHITE_HAIR = 0x1F9B3 private const val CP_BARLD = 0x1F9B2 private const val CP_VARIANT_SELECTOR = 0xFE0F + + private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_A = 0x1F1E6 + private const val CP_REGIONAL_INDICATOR_SYMBOL_LETTER_Z = 0x1F1FF } } diff --git a/tools/make-emoji-keys/src/main/resources/emoji/android-emoji-support.txt b/tools/make-emoji-keys/src/main/resources/emoji/android-emoji-support.txt index 01fee14143..2538cf0f07 100644 --- a/tools/make-emoji-keys/src/main/resources/emoji/android-emoji-support.txt +++ b/tools/make-emoji-keys/src/main/resources/emoji/android-emoji-support.txt @@ -3912,5 +3912,31 @@ U+1F532 # black square button U+1F3C1 # chequered flag U+1F6A9 # triangular flag U+1F38C # crossed flags +U+1F1E6 # regional indicator symbol letter a +U+1F1E7 # regional indicator symbol letter b +U+1F1E8 # regional indicator symbol letter c +U+1F1E9 # regional indicator symbol letter d +U+1F1EA # regional indicator symbol letter e +U+1F1EB # regional indicator symbol letter f +U+1F1EC # regional indicator symbol letter g +U+1F1ED # regional indicator symbol letter h +U+1F1EE # regional indicator symbol letter i +U+1F1EF # regional indicator symbol letter j +U+1F1F0 # regional indicator symbol letter k +U+1F1F1 # regional indicator symbol letter l +U+1F1F2 # regional indicator symbol letter m +U+1F1F3 # regional indicator symbol letter n +U+1F1F4 # regional indicator symbol letter o +U+1F1F5 # regional indicator symbol letter p +U+1F1F6 # regional indicator symbol letter q +U+1F1F7 # regional indicator symbol letter r +U+1F1F8 # regional indicator symbol letter s +U+1F1F9 # regional indicator symbol letter t +U+1F1FA # regional indicator symbol letter u +U+1F1FB # regional indicator symbol letter v +U+1F1FC # regional indicator symbol letter w +U+1F1FD # regional indicator symbol letter x +U+1F1FE # regional indicator symbol letter y +U+1F1FF # regional indicator symbol letter z # Above emojis are supported from Android 4.4 (API level 19) From bf6e18448c43c42520ac1cfdb9423652ac9ec0c0 Mon Sep 17 00:00:00 2001 From: MiMoHo <37556964+MiMoHo@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:58:57 +0200 Subject: [PATCH 2/5] Accept standalone regional indicators in isEmoji The RGI-based singleEmojiRegex only matches indicator pairs (country flags), and isSingleGrapheme falls back to that regex for strings containing 0xD83C, so a single indicator letter failed emoji detection (caught by StringUtilsTest.isEmojiDetectsAllAvailableEmojis once the indicators were back in the flags category). Co-Authored-By: Claude Fable 5 --- .../java/helium314/keyboard/latin/common/StringUtils.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt index 8268c4b961..937902c5e4 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -246,7 +246,14 @@ fun mightBeEmoji(text: CharSequence): Boolean { fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c)) /** returns whether the text is a single emoji */ -fun isEmoji(text: CharSequence): Boolean = text.toString().isSingleGrapheme && mightBeEmoji(text) && text.matches(singleEmojiRegex) +// the standalone regional indicator check comes first because the RGI-based singleEmojiRegex only +// matches indicator pairs (country flags), and isSingleGrapheme falls back to that regex too, +// but the flags category also offers the standalone indicator letters +fun isEmoji(text: CharSequence): Boolean = text.isSingleRegionalIndicator + || (text.toString().isSingleGrapheme && mightBeEmoji(text) && text.matches(singleEmojiRegex)) + +private val CharSequence.isSingleRegionalIndicator: Boolean + get() = length == 2 && Character.codePointAt(this, 0) in 0x1F1E6..0x1F1FF // from https://github.com/chattymin/Pebble/blob/main/pebble/src/main/java/com/chattymin/pebble/LocalBreakIterator.kt, Apache-2.0 license // there is more potentially useful code like String.graphemeLength (should be graphemeCount though) From 26c24ff9ad926d643721ed26e296987c5bfb9d72 Mon Sep 17 00:00:00 2001 From: MiMoHo <37556964+MiMoHo@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:14:51 +0200 Subject: [PATCH 3/5] Drop the stray trailing newline from generated FLAGS.txt make-emoji-keys writes each category file with joinToString("\n") and emits no trailing newline (see the other category files and the flags list on main, which end without one). The regional-indicator commit added the 26 letters correctly but left a trailing newline behind, so the checked-in file no longer matched the generator output byte-for-byte. Regenerated so `makeEmoji` now leaves FLAGS.txt unchanged. Co-Authored-By: Claude Opus 4.8 --- app/src/main/assets/emoji/FLAGS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/emoji/FLAGS.txt b/app/src/main/assets/emoji/FLAGS.txt index a58892d413..c95f7c77cc 100644 --- a/app/src/main/assets/emoji/FLAGS.txt +++ b/app/src/main/assets/emoji/FLAGS.txt @@ -293,4 +293,4 @@ 🇼 🇽 🇾 -🇿 +🇿 \ No newline at end of file From e6a51b4b2377897adf3ee4f443b50ef246cbcedc Mon Sep 17 00:00:00 2001 From: MiMoHo <37556964+MiMoHo@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:15:37 +0200 Subject: [PATCH 4/5] Make the makeEmoji task rebuild the generator jar `dependsOn['jar']` is a subscript on the dependsOn set, not a task dependency, so `makeEmoji` never rebuilt the jar. With no jar present it fails outright ("Unable to access jarfile ..."); with a stale one it silently regenerates the assets from old code. Use `dependsOn jar` so the task always runs against freshly compiled sources. Co-Authored-By: Claude Opus 4.8 --- tools/make-emoji-keys/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/make-emoji-keys/build.gradle b/tools/make-emoji-keys/build.gradle index f4ec920c81..c2e8c4689c 100644 --- a/tools/make-emoji-keys/build.gradle +++ b/tools/make-emoji-keys/build.gradle @@ -17,7 +17,7 @@ jar { } tasks.register('makeEmoji', JavaExec) { - dependsOn['jar'] + dependsOn jar main = '-jar' args jar.archiveFile.get() args '-assets' From c31e4b490eeae3a69a53d77920c3815ce44bd0a7 Mon Sep 17 00:00:00 2001 From: MiMoHo Date: Wed, 22 Jul 2026 23:04:16 +0200 Subject: [PATCH 5/5] Let a standalone regional indicator count as a single grapheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isSingleGrapheme returned false for a lone regional indicator (🇦–🇿) only because the cheap '\uD83C' skin-tone guard also catches the indicator's high surrogate, so it fell through to singleEmojiRegex, which matches indicator pairs (flags) only. Short-circuit the standalone case with the existing isSingleRegionalIndicator helper and cover it in the singleGrapheme test. Co-Authored-By: Claude Opus 4.8 --- .../java/helium314/keyboard/latin/common/StringUtils.kt | 7 ++++--- .../test/java/helium314/keyboard/latin/StringUtilsTest.kt | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt index 937902c5e4..48442d2c27 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -247,8 +247,8 @@ fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointStri /** returns whether the text is a single emoji */ // the standalone regional indicator check comes first because the RGI-based singleEmojiRegex only -// matches indicator pairs (country flags), and isSingleGrapheme falls back to that regex too, -// but the flags category also offers the standalone indicator letters +// matches indicator pairs (country flags), so the second clause can't recognise the standalone +// indicator letters that the flags category also offers fun isEmoji(text: CharSequence): Boolean = text.isSingleRegionalIndicator || (text.toString().isSingleGrapheme && mightBeEmoji(text) && text.matches(singleEmojiRegex)) @@ -279,7 +279,8 @@ val String.isSingleGrapheme: Boolean get() { iterator.next() if (iterator.next() != BreakIterator.DONE) return false // we have a single grapheme, but " 🏼" is detected as single grapheme which we don't want - return if ('\uD83C' !in this) true // does not contain skin tone + return if (isSingleRegionalIndicator) true // standalone regional indicator (🇦–🇿): one code point + else if ('\uD83C' !in this) true // does not contain skin tone else singleEmojiRegex.matches(this) // single grapheme only if it's a single emoji } // got IllegalArgumentException: Invalid index on iterator.next() diff --git a/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt b/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt index 672c7e739d..7cb7614fb1 100644 --- a/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt @@ -133,6 +133,9 @@ class StringUtilsTest { assert(!"a🏼".isSingleGrapheme) assert(!"🏼🏼".isSingleGrapheme) assert("🏼".isSingleGrapheme) + assert("🇦".isSingleGrapheme) // 🇦 standalone regional indicator is a single grapheme + assert("🇩🇪".isSingleGrapheme) // 🇩🇪 flag (indicator pair) is still a single grapheme + assert(!"🇦🇧🇨".isSingleGrapheme) // 🇦🇧🇨 three indicators are not one grapheme } @Test fun detectEmojisAtEnd() {