1 package org.robolectric.res.android; 2 3 import static org.robolectric.res.android.LocaleDataTables.LIKELY_SCRIPTS; 4 import static org.robolectric.res.android.LocaleDataTables.MAX_PARENT_DEPTH; 5 import static org.robolectric.res.android.LocaleDataTables.REPRESENTATIVE_LOCALES; 6 import static org.robolectric.res.android.LocaleDataTables.SCRIPT_CODES; 7 import static org.robolectric.res.android.LocaleDataTables.SCRIPT_PARENTS; 8 9 import java.util.Arrays; 10 import java.util.Map; 11 12 // transliterated from https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/LocaleData.cpp 13 public class LocaleData { 14 packLocale(final byte[] language, final byte[] region)15 private static int packLocale(final byte[] language, final byte[] region) { 16 return ((language[0] & 0xff) << 24) | ((language[1] & 0xff) << 16) | 17 ((region[0] & 0xff) << 8) | (region[1] & 0xff); 18 } 19 dropRegion(int packed_locale)20 private static int dropRegion(int packed_locale) { 21 return packed_locale & 0xFFFF0000; 22 } 23 hasRegion(int packed_locale)24 private static boolean hasRegion(int packed_locale) { 25 return (packed_locale & 0x0000FFFF) != 0; 26 } 27 28 static final int SCRIPT_LENGTH = 4; 29 private static final int PACKED_ROOT = 0; // to represent the root locale 30 findParent(int packed_locale, final String script)31 private static int findParent(int packed_locale, final String script) { 32 if (hasRegion(packed_locale)) { 33 for (Map.Entry<String, Map<Integer, Integer>> entry : SCRIPT_PARENTS.entrySet()) { 34 if (script.equals(entry.getKey())) { 35 Map<Integer, Integer> map = entry.getValue(); 36 Integer lookup_result = map.get(packed_locale); 37 if (lookup_result != null) { 38 return lookup_result; 39 } 40 break; 41 } 42 } 43 return dropRegion(packed_locale); 44 } 45 return PACKED_ROOT; 46 } 47 48 // Find the ancestors of a locale, and fill 'out' with it (assumes out has enough 49 // space). If any of the members of stop_list was seen, write it in the 50 // output but stop afterwards. 51 // 52 // This also outputs the index of the last written ancestor in the stop_list 53 // to stop_list_index, which will be -1 if it is not found in the stop_list. 54 // 55 // Returns the number of ancestors written in the output, which is always 56 // at least one. 57 // 58 // (If 'out' is null, we do everything the same way but we simply don't write 59 // any results in 'out'.) findAncestors(int[] out, Ref<Long> stop_list_index, int packed_locale, final String script, final int[] stop_list, int stop_set_length)60 static int findAncestors(int[] out, Ref<Long> stop_list_index, 61 int packed_locale, final String script, 62 final int[] stop_list, int stop_set_length) { 63 int ancestor = packed_locale; 64 int count = 0; 65 do { 66 if (out != null) { 67 out[count] = ancestor; 68 } 69 count++; 70 for (int i = 0; i < stop_set_length; i++) { 71 if (stop_list[i] == ancestor) { 72 stop_list_index.set((long) i); 73 return count; 74 } 75 } 76 ancestor = findParent(ancestor, script); 77 } while (ancestor != PACKED_ROOT); 78 stop_list_index.set((long) -1); 79 return count; 80 } 81 findDistance(int supported, final String script, final int[] request_ancestors, int request_ancestors_count)82 static int findDistance(int supported, 83 final String script, 84 final int[] request_ancestors, 85 int request_ancestors_count) { 86 final Ref<Long> request_ancestors_indexRef = new Ref<>(null); 87 final int supported_ancestor_count = findAncestors( 88 null, request_ancestors_indexRef, 89 supported, script, 90 request_ancestors, request_ancestors_count); 91 // Since both locales share the same root, there will always be a shared 92 // ancestor, so the distance in the parent tree is the sum of the distance 93 // of 'supported' to the lowest common ancestor (number of ancestors 94 // written for 'supported' minus 1) plus the distance of 'request' to the 95 // lowest common ancestor (the index of the ancestor in request_ancestors). 96 return (int) (supported_ancestor_count + request_ancestors_indexRef.get() - 1); 97 } 98 isRepresentative(int language_and_region, final String script)99 static boolean isRepresentative(int language_and_region, final String script) { 100 final long packed_locale = ( 101 (((long) language_and_region) << 32) | 102 (((long) script.charAt(0) & 0xff) << 24) | 103 (((long) script.charAt(1) & 0xff) << 16) | 104 (((long) script.charAt(2) & 0xff) << 8) | 105 ((long) script.charAt(3) & 0xff)); 106 return (REPRESENTATIVE_LOCALES.contains(packed_locale)); 107 } 108 109 private static final int US_SPANISH = 0x65735553; // es-US 110 private static final int MEXICAN_SPANISH = 0x65734D58; // es-MX 111 private static final int LATIN_AMERICAN_SPANISH = 0x6573A424; // es-419 112 113 // The two locales es-US and es-MX are treated as special fallbacks for es-419. 114 // If there is no es-419, they are considered its equivalent. isSpecialSpanish(int language_and_region)115 private static boolean isSpecialSpanish(int language_and_region) { 116 return (language_and_region == US_SPANISH || language_and_region == MEXICAN_SPANISH); 117 } 118 localeDataCompareRegions( final byte[] left_region, final byte[] right_region, final byte[] requested_language, final String requested_script, final byte[] requested_region)119 static int localeDataCompareRegions( 120 final byte[] left_region, final byte[] right_region, 121 final byte[] requested_language, final String requested_script, 122 final byte[] requested_region) { 123 if (left_region[0] == right_region[0] && left_region[1] == right_region[1]) { 124 return 0; 125 } 126 int left = packLocale(requested_language, left_region); 127 int right = packLocale(requested_language, right_region); 128 final int request = packLocale(requested_language, requested_region); 129 130 // If one and only one of the two locales is a special Spanish locale, we 131 // replace it with es-419. We don't do the replacement if the other locale 132 // is already es-419, or both locales are special Spanish locales (when 133 // es-US is being compared to es-MX). 134 final boolean leftIsSpecialSpanish = isSpecialSpanish(left); 135 final boolean rightIsSpecialSpanish = isSpecialSpanish(right); 136 if (leftIsSpecialSpanish && !rightIsSpecialSpanish && right != LATIN_AMERICAN_SPANISH) { 137 left = LATIN_AMERICAN_SPANISH; 138 } else if (rightIsSpecialSpanish && !leftIsSpecialSpanish && left != LATIN_AMERICAN_SPANISH) { 139 right = LATIN_AMERICAN_SPANISH; 140 } 141 142 int[] request_ancestors = new int[MAX_PARENT_DEPTH + 1]; 143 final Ref<Long> left_right_indexRef = new Ref<Long>(null); 144 // Find the parents of the request, but stop as soon as we saw left or right 145 final int left_and_right[] = {left, right}; 146 final int ancestor_count = findAncestors( 147 request_ancestors, left_right_indexRef, 148 request, requested_script, 149 left_and_right, sizeof(left_and_right)); 150 if (left_right_indexRef.get() == 0) { // We saw left earlier 151 return 1; 152 } 153 if (left_right_indexRef.get() == 1) { // We saw right earlier 154 return -1; 155 } 156 // If we are here, neither left nor right are an ancestor of the 157 // request. This means that all the ancestors have been computed and 158 // the last ancestor is just the language by itself. We will use the 159 // distance in the parent tree for determining the better match. 160 final int left_distance = findDistance( 161 left, requested_script, request_ancestors, ancestor_count); 162 final int right_distance = findDistance( 163 right, requested_script, request_ancestors, ancestor_count); 164 if (left_distance != right_distance) { 165 return (int) right_distance - (int) left_distance; // smaller distance is better 166 } 167 // If we are here, left and right are equidistant from the request. We will 168 // try and see if any of them is a representative locale. 169 final boolean left_is_representative = isRepresentative(left, requested_script); 170 final boolean right_is_representative = isRepresentative(right, requested_script); 171 if (left_is_representative != right_is_representative) { 172 return (left_is_representative ? 1 : 0) - (right_is_representative ? 1 : 0); 173 } 174 // We have no way of figuring out which locale is a better match. For 175 // the sake of stability, we consider the locale with the lower region 176 // code (in dictionary order) better, with two-letter codes before 177 // three-digit codes (since two-letter codes are more specific). 178 return right - left; 179 } 180 localeDataComputeScript(byte[] out, final byte[] language, final byte[] region)181 static void localeDataComputeScript(byte[] out, final byte[] language, final byte[] region) { 182 if (language[0] == '\0') { 183 // memset(out, '\0', SCRIPT_LENGTH); 184 Arrays.fill(out, (byte) 0); 185 return; 186 } 187 int lookup_key = packLocale(language, region); 188 Byte lookup_result = LIKELY_SCRIPTS.get(lookup_key); 189 if (lookup_result == null) { 190 // We couldn't find the locale. Let's try without the region 191 if (region[0] != '\0') { 192 lookup_key = dropRegion(lookup_key); 193 lookup_result = LIKELY_SCRIPTS.get(lookup_key); 194 if (lookup_result != null) { 195 // memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH); 196 System.arraycopy(SCRIPT_CODES[lookup_result], 0, out, 0, SCRIPT_LENGTH); 197 return; 198 } 199 } 200 // We don't know anything about the locale 201 // memset(out, '\0', SCRIPT_LENGTH); 202 Arrays.fill(out, (byte) 0); 203 return; 204 } else { 205 // We found the locale. 206 // memcpy(out, SCRIPT_CODES[lookup_result.second], SCRIPT_LENGTH); 207 System.arraycopy(SCRIPT_CODES[lookup_result], 0, out, 0, SCRIPT_LENGTH); 208 } 209 } 210 211 static final int[] ENGLISH_STOP_LIST = { 212 0x656E0000, // en 213 0x656E8400, // en-001 214 }; 215 216 static final byte[] ENGLISH_CHARS = {'e', 'n'}; 217 218 static final String LATIN_CHARS = "Latn"; 219 localeDataIsCloseToUsEnglish(final byte[] region)220 static boolean localeDataIsCloseToUsEnglish(final byte[] region) { 221 final int locale = packLocale(ENGLISH_CHARS, region); 222 final Ref<Long> stop_list_indexRef = new Ref<>(null); 223 findAncestors(null, stop_list_indexRef, locale, LATIN_CHARS, ENGLISH_STOP_LIST, 2); 224 // A locale is like US English if we see "en" before "en-001" in its ancestor list. 225 return stop_list_indexRef.get() == 0; // 'en' is first in ENGLISH_STOP_LIST 226 } 227 228 sizeof(int[] array)229 private static int sizeof(int[] array) { 230 return array.length; 231 } 232 233 } 234