+ * Currently this file is aligned to zh.txt in ICU 4.6 + */ +@SuppressWarnings("SizeReplaceableByIsEmpty") +public record HanziToPinyin(boolean mHasChinaCollator) { + private static final String TAG = "HanziToPinyin"; + + // Turn on this flag when we want to check internal data structure. + private static final boolean DEBUG = false; + + /** + * Unihans array. + *
+ * Each unihans is the first one within same pinyin when collator is zh_CN. + */ + public static final char[] UNIHANS = { + '阿', '哎', '安', '肮', '凹', '八', + '挀', '扳', '邦', '勹', '陂', '奔', + '伻', '屄', '边', '灬', '憋', '汃', + '冫', '癶', '峬', '嚓', '偲', '参', + '仓', '撡', '冊', '嵾', '曽', '曾', + '層', '叉', '芆', '辿', '伥', '抄', + '车', '抻', '沈', '沉', '阷', '吃', + '充', '抽', '出', '欻', '揣', '巛', + '刅', '吹', '旾', '逴', '呲', '匆', + '凑', '粗', '汆', '崔', '邨', '搓', + '咑', '呆', '丹', '当', '刀', '嘚', + '扥', '灯', '氐', '嗲', '甸', '刁', + '爹', '丁', '丟', '东', '吺', '厾', + '耑', '襨', '吨', '多', '妸', '诶', + '奀', '鞥', '儿', '发', '帆', '匚', + '飞', '分', '丰', '覅', '仏', '紑', + '伕', '旮', '侅', '甘', '冈', '皋', + '戈', '给', '根', '刯', '工', '勾', + '估', '瓜', '乖', '关', '光', '归', + '丨', '呙', '哈', '咍', '佄', '夯', + '茠', '诃', '黒', '拫', '亨', '噷', + '叿', '齁', '乯', '花', '怀', '犿', + '巟', '灰', '昏', '吙', '丌', '加', + '戋', '江', '艽', '阶', '巾', '坕', + '冂', '丩', '凥', '姢', '噘', '军', + '咔', '开', '刊', '忼', '尻', '匼', + '肎', '劥', '空', '抠', '扝', '夸', + '蒯', '宽', '匡', '亏', '坤', '扩', + '垃', '来', '兰', '啷', '捞', '肋', + '勒', '崚', '刕', '俩', '奁', '良', + '撩', '列', '拎', '刢', '溜', '囖', + '龙', '瞜', '噜', '娈', '畧', '抡', + '罗', '呣', '妈', '埋', '嫚', '牤', + '猫', '么', '呅', '门', '甿', '咪', + '宀', '喵', '乜', '民', '名', '谬', + '摸', '哞', '毪', '嗯', '拏', '腉', + '囡', '囔', '孬', '疒', '娞', '恁', + '能', '妮', '拈', '嬢', '鸟', '捏', + '囜', '宁', '妞', '农', '羺', '奴', + '奻', '疟', '黁', '郍', '喔', '讴', + '妑', '拍', '眅', '乓', '抛', '呸', + '喷', '匉', '丕', '囨', '剽', '氕', + '姘', '乒', '钋', '剖', '仆', '七', + '掐', '千', '呛', '悄', '癿', '亲', + '狅', '芎', '丘', '区', '峑', '缺', + '夋', '呥', '穣', '娆', '惹', '人', + '扔', '日', '茸', '厹', '邚', '挼', + '堧', '婑', '瞤', '捼', '仨', '毢', + '三', '桒', '掻', '閪', '森', '僧', + '杀', '筛', '山', '伤', '弰', '奢', + '申', '莘', '敒', '升', '尸', '収', + '书', '刷', '衰', '闩', '双', '谁', + '吮', '说', '厶', '忪', '捜', '苏', + '狻', '夊', '孙', '唆', '他', '囼', + '坍', '汤', '夲', '忑', '熥', '剔', + '天', '旫', '帖', '厅', '囲', '偷', + '凸', '湍', '推', '吞', '乇', '穵', + '歪', '弯', '尣', '危', '昷', '翁', + '挝', '乌', '夕', '虲', '仚', '乡', + '灱', '些', '心', '星', '凶', '休', + '吁', '吅', '削', '坃', '丫', '恹', + '央', '幺', '倻', '一', '囙', '应', + '哟', '佣', '优', '扜', '囦', '曰', + '晕', '筠', '筼', '帀', '災', '兂', + '匨', '傮', '则', '贼', '怎', '増', + '扎', '捚', '沾', '张', '长', '長', + '佋', '蜇', '贞', '争', '之', '峙', + '庢', '中', '州', '朱', '抓', '拽', + '专', '妆', '隹', '宒', '卓', '乲', + '宗', '邹', '租', '钻', '厜', '尊', + '昨', '兙', '鿃', '鿄'}; + + /** + * Pinyin array. + *
+ * Each pinyin is corresponding to unihans of same
+ * offset in the unihans array.
+ */
+ public static final byte[][] PINYINS = {
+ {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0},
+ {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0},
+ {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0},
+ {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0},
+ {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0},
+ {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0},
+ {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0},
+ {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0},
+ {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0},
+ {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0},
+ {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0},
+ {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0},
+ {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0},
+ {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0},
+ {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0},
+ {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0},
+ {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0},
+ {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0},
+ {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0},
+ {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0},
+ {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0},
+ {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0},
+ {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0},
+ {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0},
+ {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0},
+ {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0},
+ {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0},
+ {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0},
+ {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0},
+ {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0},
+ {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0},
+ {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0},
+ {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0},
+ {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0},
+ {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0},
+ {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0},
+ {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0},
+ {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0},
+ {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0},
+ {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0},
+ {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0},
+ {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0},
+ {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0},
+ {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0},
+ {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0},
+ {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0},
+ {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0},
+ {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0},
+ {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0},
+ {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0},
+ {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0},
+ {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0},
+ {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0},
+ {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0},
+ {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0},
+ {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0},
+ {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0},
+ {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0},
+ {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0},
+ {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0},
+ {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0},
+ {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0},
+ {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0},
+ {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0},
+ {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0},
+ {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0},
+ {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0},
+ {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0},
+ {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0},
+ {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0},
+ {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0},
+ {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0},
+ {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0},
+ {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0},
+ {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
+ {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0},
+ {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0},
+ {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0},
+ {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0},
+ {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0},
+ {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0},
+ {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0},
+ {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0},
+ {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0},
+ {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0},
+ {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0},
+ {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0},
+ {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0},
+ {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0},
+ {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0},
+ {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0},
+ {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0},
+ {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0},
+ {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0},
+ {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0},
+ {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0},
+ {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0},
+ {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0},
+ {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0},
+ {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0},
+ {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0},
+ {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0},
+ {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0},
+ {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0},
+ {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0},
+ {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0},
+ {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0},
+ {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0},
+ {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0},
+ {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0},
+ {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0},
+ {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0},
+ {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0},
+ {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0},
+ {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0},
+ {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0},
+ {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0},
+ {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0},
+ {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0},
+ {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0},
+ {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0},
+ {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0},
+ {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0},
+ {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0},
+ {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0},
+ {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0},
+ {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0},
+ {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0},
+ {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0},
+ {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0},
+ {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0},
+ {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0},
+ {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0},
+ {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0},
+ {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0},
+ {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0},
+ {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0},
+ {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0},
+ {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0},
+ {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0},
+ {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0},
+ {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0},
+ {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0},
+ {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0},
+ {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0},
+ {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0},
+ {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0},
+ {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0},
+ {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0},
+ {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0},
+ {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0},
+ {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0},
+ {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0},
+ {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0},
+ {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0},
+ {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0},
+ {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0},
+ {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0},
+ {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0},
+ {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0},
+ {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0},
+ {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0},
+ {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0},
+ {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0},
+ {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0},
+ {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0},
+ {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0},
+ {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0},
+ {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0},
+ {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0},
+ {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0},
+ {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0},
+ {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0},
+ {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0},
+ {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0},
+ {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0},
+ {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0},
+ {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0},
+ {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0},
+ {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0},
+ {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0},
+ {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0},
+ {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0},
+ {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0},
+ {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0},
+ {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0},
+ {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0},
+ {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0},
+ {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0},
+ {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
+ {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0},
+ {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0},
+ {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0},
+ {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0},
+ {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0},
+ {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0},
+ {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0},
+ {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0},
+ {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0},
+ {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0},
+ {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0},
+ {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0},
+ {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0},
+ {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0},
+ {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71},
+ {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0},
+ {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0},
+ {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0},
+ {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0},
+ {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0},
+ {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0},
+ {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0}};
+
+ /**
+ * First and last Chinese character with known Pinyin according to zh collation
+ */
+ private static final String FIRST_PINYIN_UNIHAN = "阿";
+ private static final String LAST_PINYIN_UNIHAN = "鿿";
+
+ private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);
+
+ private static HanziToPinyin sInstance;
+
+ public static class Token {
+ /**
+ * Separator between target string for each source char
+ */
+ public static final String SEPARATOR = " ";
+
+ public static final int LATIN = 1;
+ public static final int PINYIN = 2;
+ public static final int UNKNOWN = 3;
+
+ public Token() {
+ }
+
+ public Token(int type, String source, String target) {
+ this.type = type;
+ this.source = source;
+ this.target = target;
+ }
+
+ /**
+ * Type of this token, ASCII, PINYIN or UNKNOWN.
+ */
+ public int type;
+ /**
+ * Original string before translation.
+ */
+ public String source;
+ /**
+ * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is
+ * original string in source.
+ */
+ public String target;
+ }
+
+ public static HanziToPinyin getInstance() {
+ synchronized (HanziToPinyin.class) {
+ if (sInstance != null) {
+ return sInstance;
+ }
+ // Check if zh_CN collation data is available
+ final Locale[] locale = Collator.getAvailableLocales();
+ for (Locale value : locale) {
+ if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) {
+ // Do self validation just once.
+ if (DEBUG) {
+ Log.d(TAG, "Self validation. Result: " + doSelfValidation());
+ }
+ sInstance = new HanziToPinyin(true);
+ return sInstance;
+ }
+ }
+ if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题
+ if (Locale.CHINA.equals(Locale.getDefault())) {
+ sInstance = new HanziToPinyin(true);
+ return sInstance;
+ }
+ }
+ Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
+ sInstance = new HanziToPinyin(false);
+ return sInstance;
+ }
+ }
+
+ /**
+ * Validate if our internal table has some wrong value.
+ *
+ * @return true when the table looks correct.
+ */
+ private static boolean doSelfValidation() {
+ char lastChar = UNIHANS[0];
+ String lastString = Character.toString(lastChar);
+ for (char c : UNIHANS) {
+ if (lastChar == c) {
+ continue;
+ }
+ final String curString = Character.toString(c);
+ int cmp = COLLATOR.compare(lastString, curString);
+ if (cmp >= 0) {
+ Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString
+ + "\" is greater than current string \"" + curString + "\".");
+ return false;
+ }
+ lastString = curString;
+ }
+ return true;
+ }
+
+ private Token getToken(char character) {
+ Token token = new Token();
+ final String letter = Character.toString(character);
+ token.source = letter;
+ int offset = -1;
+ int cmp;
+ if (character < 256) {
+ token.type = Token.LATIN;
+ token.target = letter;
+ return token;
+ } else {
+ cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN);
+ if (cmp < 0) {
+ token.type = Token.UNKNOWN;
+ token.target = letter;
+ return token;
+ } else if (cmp == 0) {
+ token.type = Token.PINYIN;
+ offset = 0;
+ } else {
+ cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN);
+ if (cmp > 0) {
+ token.type = Token.UNKNOWN;
+ token.target = letter;
+ return token;
+ } else if (cmp == 0) {
+ token.type = Token.PINYIN;
+ offset = UNIHANS.length - 1;
+ }
+ }
+ }
+
+ token.type = Token.PINYIN;
+ if (offset < 0) {
+ int begin = 0;
+ int end = UNIHANS.length - 1;
+ while (begin <= end) {
+ offset = (begin + end) / 2;
+ final String unihan = Character.toString(UNIHANS[offset]);
+ cmp = COLLATOR.compare(letter, unihan);
+ if (cmp == 0) {
+ break;
+ } else if (cmp > 0) {
+ begin = offset + 1;
+ } else {
+ end = offset - 1;
+ }
+ }
+ }
+ if (cmp < 0) {
+ offset--;
+ }
+ StringBuilder pinyin = new StringBuilder();
+ for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) {
+ pinyin.append((char) PINYINS[offset][j]);
+ }
+ token.target = pinyin.toString();
+ if (TextUtils.isEmpty(token.target)) {
+ token.type = Token.UNKNOWN;
+ token.target = token.source;
+ }
+ return token;
+ }
+
+ /**
+ * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without
+ * space will be put into a Token, One Hanzi character which has pinyin will be treated as a
+ * Token. If these is no China collator, the empty token array is returned.
+ */
+ public ArrayList
+ * To avoid leaking user or app data to the web, make sure to choose {@code directory}
+ * carefully, and assume any file under this directory could be accessed by any web page subject
+ * to same-origin rules.
+ *
+ * A typical usage would be like:
+ *
+ * Note: Any future addition to this list will be considered breaking changes to the API.
+ */
+ private static final String[] FORBIDDEN_DATA_DIRS =
+ new String[] {"/data/data", "/data/system"};
+
+ @NonNull
+ private final File mDirectory;
+
+ private final Shell mShell;
+
+ /**
+ * Creates PathHandler for app's internal storage.
+ * The directory to be exposed must be inside either the application's internal data
+ * directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}.
+ * External storage is not supported for security reasons, as other apps with
+ * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the
+ * files.
+ *
+ * Exposing the entire data or cache directory is not permitted, to avoid accidentally
+ * exposing sensitive application files to the web. Certain existing subdirectories of
+ * {@link Context#getDataDir} are also not permitted as they are often sensitive.
+ * These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"},
+ * {@code "shared_prefs/"} and {@code "code_cache/"}).
+ *
+ * The application should typically use a dedicated subdirectory for the files it intends to
+ * expose and keep them separate from other files.
+ *
+ * @param directory the absolute path of the exposed app internal storage directory from
+ * which files can be loaded.
+ * @throws IllegalArgumentException if the directory is not allowed.
+ */
+ public SuFilePathHandler(@NonNull File directory, Shell rootShell) {
+ try {
+ mDirectory = new File(getCanonicalDirPath(directory));
+ if (!isAllowedInternalStorageDir()) {
+ throw new IllegalArgumentException("The given directory \"" + directory
+ + "\" doesn't exist under an allowed app internal storage directory");
+ }
+ mShell = rootShell;
+ } catch (IOException e) {
+ throw new IllegalArgumentException(
+ "Failed to resolve the canonical path for the given directory: "
+ + directory.getPath(), e);
+ }
+ }
+
+ private boolean isAllowedInternalStorageDir() throws IOException {
+ String dir = getCanonicalDirPath(mDirectory);
+
+ for (String forbiddenPath : FORBIDDEN_DATA_DIRS) {
+ if (dir.startsWith(forbiddenPath)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Opens the requested file from the exposed data directory.
+ *
+ * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the
+ * requested file cannot be found or is outside the mounted directory a
+ * {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be
+ * returned instead of {@code null}. This saves the time of falling back to network and
+ * trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with
+ * {@code null} {@link InputStream} will be received as an HTTP response with status code
+ * {@code 404} and no body.
+ *
+ * The MIME type for the file will be determined from the file's extension using
+ * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that
+ * files are named using standard file extensions. If the file does not have a
+ * recognised extension, {@code "text/plain"} will be used by default.
+ *
+ * @param path the suffix path to be handled.
+ * @return {@link WebResourceResponse} for the requested file.
+ */
+ @Override
+ @WorkerThread
+ @NonNull
+ public WebResourceResponse handle(@NonNull String path) {
+ try {
+ File file = getCanonicalFileIfChild(mDirectory, path);
+ if (file != null) {
+ InputStream is = openFile(file, mShell);
+ String mimeType = guessMimeType(path);
+ return new WebResourceResponse(mimeType, null, is);
+ } else {
+ Log.e(TAG, String.format(
+ "The requested file: %s is outside the mounted directory: %s", path,
+ mDirectory));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error opening the requested path: " + path, e);
+ }
+ return new WebResourceResponse(null, null, null);
+ }
+
+ public static String getCanonicalDirPath(@NonNull File file) throws IOException {
+ String canonicalPath = file.getCanonicalPath();
+ if (!canonicalPath.endsWith("/")) canonicalPath += "/";
+ return canonicalPath;
+ }
+
+ public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child)
+ throws IOException {
+ String parentCanonicalPath = getCanonicalDirPath(parent);
+ String childCanonicalPath = new File(parent, child).getCanonicalPath();
+ if (childCanonicalPath.startsWith(parentCanonicalPath)) {
+ return new File(childCanonicalPath);
+ }
+ return null;
+ }
+
+ @NonNull
+ private static InputStream handleSvgzStream(@NonNull String path,
+ @NonNull InputStream stream) throws IOException {
+ return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
+ }
+
+ public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException {
+ SuFile suFile = new SuFile(file.getAbsolutePath());
+ suFile.setShell(shell);
+ InputStream fis = SuFileInputStream.open(suFile);
+ return handleSvgzStream(file.getPath(), fis);
+ }
+
+ /**
+ * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the
+ * {@link #DEFAULT_MIME_TYPE} if it can't guess.
+ *
+ * @param filePath path of the file to guess its MIME type.
+ * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}.
+ */
+ @NonNull
+ public static String guessMimeType(@NonNull String filePath) {
+ String mimeType = MimeUtil.getMimeFromFileName(filePath);
+ return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
+ }
+}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt
deleted file mode 100644
index c0f7930..0000000
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package com.sukisu.ultra.ui.webui
-
-import android.content.Context
-import android.util.Log
-import android.webkit.WebResourceResponse
-import androidx.annotation.WorkerThread
-import androidx.webkit.WebViewAssetLoader
-import com.topjohnwu.superuser.Shell
-import com.topjohnwu.superuser.io.SuFile
-import com.topjohnwu.superuser.io.SuFileInputStream
-import java.io.ByteArrayInputStream
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.nio.charset.StandardCharsets
-import java.util.zip.GZIPInputStream
-
-/**
- * Handler class to open files from file system by root access
- * For more information about android storage please refer to
- * [Android Developers Docs: Data and file storage overview](https://developer.android.com/guide/topics/data/data-storage).
- *
- * To avoid leaking user or app data to the web, make sure to choose [directory]
- * carefully, and assume any file under this directory could be accessed by any web page subject
- * to same-origin rules.
- *
- * A typical usage would be like:
- * ```
- * val publicDir = File(context.filesDir, "public")
- * // Host "files/public/" in app's data directory under:
- * // http://appassets.androidplatform.net/public/...
- * val assetLoader = WebViewAssetLoader.Builder()
- * .addPathHandler("/public/", SuFilePathHandler(context, publicDir, shell, insetsSupplier))
- * .build()
- * ```
- */
-class SuFilePathHandler(
- directory: File,
- private val shell: Shell,
- private val insetsSupplier: InsetsSupplier
-) : WebViewAssetLoader.PathHandler {
-
- private val directory: File
-
- init {
- try {
- this.directory = File(getCanonicalDirPath(directory))
- if (!isAllowedInternalStorageDir()) {
- throw IllegalArgumentException(
- "The given directory \"$directory\" doesn't exist under an allowed app internal storage directory"
- )
- }
- } catch (e: IOException) {
- throw IllegalArgumentException(
- "Failed to resolve the canonical path for the given directory: ${directory.path}",
- e
- )
- }
- }
-
- fun interface InsetsSupplier {
- fun get(): Insets
- }
-
- private fun isAllowedInternalStorageDir(): Boolean {
- return try {
- val dir = getCanonicalDirPath(directory)
- FORBIDDEN_DATA_DIRS.none { dir.startsWith(it) }
- } catch (_: IOException) {
- false
- }
- }
-
- /**
- * Opens the requested file from the exposed data directory.
- *
- * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the
- * requested file cannot be found or is outside the mounted directory a
- * [WebResourceResponse] object with a `null` [InputStream] will be
- * returned instead of `null`. This saves the time of falling back to network and
- * trying to resolve a path that doesn't exist. A [WebResourceResponse] with
- * `null` [InputStream] will be received as an HTTP response with status code
- * `404` and no body.
- *
- * The MIME type for the file will be determined from the file's extension using
- * [java.net.URLConnection.guessContentTypeFromName]. Developers should ensure that
- * files are named using standard file extensions. If the file does not have a
- * recognised extension, `"text/plain"` will be used by default.
- *
- * @param path the suffix path to be handled.
- * @return [WebResourceResponse] for the requested file.
- */
- @WorkerThread
- override fun handle(path: String): WebResourceResponse {
- if (path == "internal/insets.css") {
- val css = insetsSupplier.get().css
- return WebResourceResponse(
- "text/css",
- "utf-8",
- ByteArrayInputStream(css.toByteArray(StandardCharsets.UTF_8))
- )
- }
-
- try {
- val file = getCanonicalFileIfChild(directory, path)
- if (file != null) {
- val inputStream = openFile(file, shell)
- val mimeType = guessMimeType(path)
- return WebResourceResponse(mimeType, null, inputStream)
- } else {
- Log.e(
- TAG,
- "The requested file: $path is outside the mounted directory: $directory"
- )
- }
- } catch (e: IOException) {
- Log.e(TAG, "Error opening the requested path: $path", e)
- }
-
- return WebResourceResponse(null, null, null)
- }
-
- companion object {
- private const val TAG = "SuFilePathHandler"
-
- /**
- * Default value to be used as MIME type if guessing MIME type failed.
- */
- const val DEFAULT_MIME_TYPE = "text/plain"
-
- /**
- * Forbidden subdirectories of [Context.getDataDir] that cannot be exposed by this
- * handler. They are forbidden as they often contain sensitive information.
- *
- * Note: Any future addition to this list will be considered breaking changes to the API.
- */
- private val FORBIDDEN_DATA_DIRS = arrayOf("/data/data", "/data/system")
-
- @JvmStatic
- @Throws(IOException::class)
- fun getCanonicalDirPath(file: File): String {
- var canonicalPath = file.canonicalPath
- if (!canonicalPath.endsWith("/")) {
- canonicalPath += "/"
- }
- return canonicalPath
- }
-
- @JvmStatic
- @Throws(IOException::class)
- fun getCanonicalFileIfChild(parent: File, child: String): File? {
- val parentCanonicalPath = getCanonicalDirPath(parent)
- val childCanonicalPath = File(parent, child).canonicalPath
- return if (childCanonicalPath.startsWith(parentCanonicalPath)) {
- File(childCanonicalPath)
- } else {
- null
- }
- }
-
- @Throws(IOException::class)
- private fun handleSvgzStream(path: String, stream: InputStream): InputStream {
- return if (path.endsWith(".svgz")) {
- GZIPInputStream(stream)
- } else {
- stream
- }
- }
-
- @JvmStatic
- @Throws(IOException::class)
- fun openFile(file: File, shell: Shell): InputStream {
- val suFile = SuFile(file.absolutePath).apply {
- setShell(shell)
- }
- val fis = SuFileInputStream.open(suFile)
- return handleSvgzStream(file.path, fis)
- }
-
- /**
- * Use [MimeUtil.getMimeFromFileName] to guess MIME type or return the
- * [DEFAULT_MIME_TYPE] if it can't guess.
- *
- * @param filePath path of the file to guess its MIME type.
- * @return MIME type guessed from file extension or [DEFAULT_MIME_TYPE].
- */
- @JvmStatic
- fun guessMimeType(filePath: String): String {
- return MimeUtil.getMimeFromFileName(filePath) ?: DEFAULT_MIME_TYPE
- }
- }
-}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt
index 91ecd6c..8a924df 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt
@@ -2,38 +2,32 @@ package com.sukisu.ultra.ui.webui
import android.annotation.SuppressLint
import android.app.ActivityManager
-import android.graphics.Color
import android.os.Build
import android.os.Bundle
+import android.view.ViewGroup.MarginLayoutParams
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
+import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.sukisu.ultra.ui.util.createRootShell
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.io.File
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
private val rootShell by lazy { createRootShell(true) }
-
- private lateinit var insets: Insets
+ private val superUserViewModel: SuperUserViewModel by viewModels()
private var webView = null as WebView?
override fun onCreate(savedInstanceState: Bundle?) {
@@ -46,21 +40,10 @@ class WebUIActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
- setContent {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator()
- }
+ lifecycleScope.launch {
+ superUserViewModel.fetchAppList()
}
- lifecycleScope.launch {
- SuperUserViewModel.isAppListLoaded.first { it }
- setupWebView()
- }
- }
- private fun setupWebView() {
val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return }
val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@@ -77,12 +60,11 @@ class WebUIActivity : ComponentActivity() {
val moduleDir = "/data/adb/modules/${moduleId}"
val webRoot = File("${moduleDir}/webroot")
- insets = Insets(0, 0, 0, 0)
val webViewAssetLoader = WebViewAssetLoader.Builder()
.setDomain("mui.kernelsu.org")
.addPathHandler(
"/",
- SuFilePathHandler(webRoot, rootShell) { insets }
+ SuFilePathHandler(webRoot, rootShell)
)
.build()
@@ -112,18 +94,15 @@ class WebUIActivity : ComponentActivity() {
val webView = WebView(this).apply {
webView = this
- setBackgroundColor(Color.TRANSPARENT)
- val density = resources.displayMetrics.density
-
- ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsets ->
- val inset = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
- insets = Insets(
- top = (inset.top / density).toInt(),
- bottom = (inset.bottom / density).toInt(),
- left = (inset.left / density).toInt(),
- right = (inset.right / density).toInt()
- )
- WindowInsetsCompat.CONSUMED
+ ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
+ val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ view.updateLayoutParams (&mut self, module_dir: P) -> Result>(emptyList())
- private val _isAppListLoaded = MutableStateFlow(false)
- val isAppListLoaded = _isAppListLoaded.asStateFlow()
+ var appGroups by mutableStateOf
>(emptyList())
@JvmStatic
fun getAppIconDrawable(context: Context, packageName: String): Drawable? {
@@ -71,8 +67,6 @@ class SuperUserViewModel : ViewModel() {
?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
}
- var appGroups by mutableStateOf
>(emptyList())
-
private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category"
@@ -83,36 +77,31 @@ class SuperUserViewModel : ViewModel() {
private const val BATCH_SIZE = 20
}
- @Immutable
@Parcelize
data class AppInfo(
val label: String,
val packageInfo: PackageInfo,
val profile: Natives.Profile?,
) : Parcelable {
- @IgnoredOnParcel
- val packageName: String = packageInfo.packageName
- @IgnoredOnParcel
- val uid: Int = packageInfo.applicationInfo!!.uid
+ val packageName: String get() = packageInfo.packageName
+ val uid: Int get() = packageInfo.applicationInfo!!.uid
}
- @Immutable
@Parcelize
data class AppGroup(
val uid: Int,
val apps: List
+ * File publicDir = new File(context.getFilesDir(), "public");
+ * // Host "files/public/" in app's data directory under:
+ * // http://appassets.androidplatform.net/public/...
+ * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
+ * .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
+ * .build();
+ *
+ */
+public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
+ private static final String TAG = "SuFilePathHandler";
+
+ /**
+ * Default value to be used as MIME type if guessing MIME type failed.
+ */
+ public static final String DEFAULT_MIME_TYPE = "text/plain";
+
+ /**
+ * Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this
+ * handler. They are forbidden as they often contain sensitive information.
+ *