| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.text; |
| |
| import android.annotation.Nullable; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.RandomAccessFile; |
| import java.nio.ByteBuffer; |
| import java.nio.MappedByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.util.HashMap; |
| import java.util.Locale; |
| |
| /** |
| * Hyphenator is a wrapper class for a native implementation of automatic hyphenation, |
| * in essence finding valid hyphenation opportunities in a word. |
| * |
| * @hide |
| */ |
| public class Hyphenator { |
| // This class has deliberately simple lifetime management (no finalizer) because in |
| // the common case a process will use a very small number of locales. |
| |
| private static String TAG = "Hyphenator"; |
| |
| private final static Object sLock = new Object(); |
| |
| @GuardedBy("sLock") |
| final static HashMap<Locale, Hyphenator> sMap = new HashMap<Locale, Hyphenator>(); |
| |
| final static Hyphenator sEmptyHyphenator = |
| new Hyphenator(StaticLayout.nLoadHyphenator(null, 0), null); |
| |
| final private long mNativePtr; |
| |
| // We retain a reference to the buffer to keep the memory mapping valid |
| @SuppressWarnings("unused") |
| final private ByteBuffer mBuffer; |
| |
| private Hyphenator(long nativePtr, ByteBuffer b) { |
| mNativePtr = nativePtr; |
| mBuffer = b; |
| } |
| |
| public long getNativePtr() { |
| return mNativePtr; |
| } |
| |
| public static Hyphenator get(@Nullable Locale locale) { |
| synchronized (sLock) { |
| Hyphenator result = sMap.get(locale); |
| if (result != null) { |
| return result; |
| } |
| |
| // If there's a variant, fall back to language+variant only, if available |
| final String variant = locale.getVariant(); |
| if (!variant.isEmpty()) { |
| final Locale languageAndVariantOnlyLocale = |
| new Locale(locale.getLanguage(), "", variant); |
| result = sMap.get(languageAndVariantOnlyLocale); |
| if (result != null) { |
| sMap.put(locale, result); |
| return result; |
| } |
| } |
| |
| // Fall back to language-only, if available |
| final Locale languageOnlyLocale = new Locale(locale.getLanguage()); |
| result = sMap.get(languageOnlyLocale); |
| if (result != null) { |
| sMap.put(locale, result); |
| return result; |
| } |
| |
| // Fall back to script-only, if available |
| final String script = locale.getScript(); |
| if (!script.equals("")) { |
| final Locale scriptOnlyLocale = new Locale.Builder() |
| .setLanguage("und") |
| .setScript(script) |
| .build(); |
| result = sMap.get(scriptOnlyLocale); |
| if (result != null) { |
| sMap.put(locale, result); |
| return result; |
| } |
| } |
| |
| sMap.put(locale, sEmptyHyphenator); // To remember we found nothing. |
| } |
| return sEmptyHyphenator; |
| } |
| |
| private static Hyphenator loadHyphenator(String languageTag) { |
| String patternFilename = "hyph-" + languageTag.toLowerCase(Locale.US) + ".hyb"; |
| File patternFile = new File(getSystemHyphenatorLocation(), patternFilename); |
| try { |
| RandomAccessFile f = new RandomAccessFile(patternFile, "r"); |
| try { |
| FileChannel fc = f.getChannel(); |
| MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); |
| long nativePtr = StaticLayout.nLoadHyphenator(buf, 0); |
| return new Hyphenator(nativePtr, buf); |
| } finally { |
| f.close(); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "error loading hyphenation " + patternFile, e); |
| return null; |
| } |
| } |
| |
| private static File getSystemHyphenatorLocation() { |
| return new File("/system/usr/hyphen-data"); |
| } |
| |
| // This array holds pairs of language tags that are used to prefill the map from locale to |
| // hyphenation data: The hyphenation data for the first field will be prefilled from the |
| // hyphenation data for the second field. |
| // |
| // The aliases that are computable by the get() method above are not included. |
| private static final String[][] LOCALE_FALLBACK_DATA = { |
| // English locales that fall back to en-US. The data is |
| // from CLDR. It's all English locales, minus the locales whose |
| // parent is en-001 (from supplementalData.xml, under <parentLocales>). |
| // TODO: Figure out how to get this from ICU. |
| {"en-AS", "en-US"}, // English (American Samoa) |
| {"en-GU", "en-US"}, // English (Guam) |
| {"en-MH", "en-US"}, // English (Marshall Islands) |
| {"en-MP", "en-US"}, // English (Northern Mariana Islands) |
| {"en-PR", "en-US"}, // English (Puerto Rico) |
| {"en-UM", "en-US"}, // English (United States Minor Outlying Islands) |
| {"en-VI", "en-US"}, // English (Virgin Islands) |
| |
| // All English locales other than those falling back to en-US are mapped to en-GB. |
| {"en", "en-GB"}, |
| |
| // For German, we're assuming the 1996 (and later) orthography by default. |
| {"de", "de-1996"}, |
| // Liechtenstein uses the Swiss hyphenation rules for the 1901 orthography. |
| {"de-LI-1901", "de-CH-1901"}, |
| |
| // Norwegian is very probably Norwegian Bokmål. |
| {"no", "nb"}, |
| |
| // Use mn-Cyrl. According to CLDR's likelySubtags.xml, mn is most likely to be mn-Cyrl. |
| {"mn", "mn-Cyrl"}, // Mongolian |
| |
| // Fall back to Ethiopic script for languages likely to be written in Ethiopic. |
| // Data is from CLDR's likelySubtags.xml. |
| // TODO: Convert this to a mechanism using ICU4J's ULocale#addLikelySubtags(). |
| {"am", "und-Ethi"}, // Amharic |
| {"byn", "und-Ethi"}, // Blin |
| {"gez", "und-Ethi"}, // Geʻez |
| {"ti", "und-Ethi"}, // Tigrinya |
| {"wal", "und-Ethi"}, // Wolaytta |
| }; |
| |
| /** |
| * Load hyphenation patterns at initialization time. We want to have patterns |
| * for all locales loaded and ready to use so we don't have to do any file IO |
| * on the UI thread when drawing text in different locales. |
| * |
| * @hide |
| */ |
| public static void init() { |
| sMap.put(null, null); |
| |
| // TODO: replace this with a discovery-based method that looks into /system/usr/hyphen-data |
| String[] availableLanguages = { |
| "as", |
| "bn", |
| "cy", |
| "da", |
| "de-1901", "de-1996", "de-CH-1901", |
| "en-GB", "en-US", |
| "es", |
| "et", |
| "eu", |
| "fr", |
| "ga", |
| "gu", |
| "hi", |
| "hr", |
| "hu", |
| "hy", |
| "kn", |
| "ml", |
| "mn-Cyrl", |
| "mr", |
| "nb", |
| "nn", |
| "or", |
| "pa", |
| "pt", |
| "sl", |
| "ta", |
| "te", |
| "tk", |
| "und-Ethi", |
| }; |
| for (int i = 0; i < availableLanguages.length; i++) { |
| String languageTag = availableLanguages[i]; |
| Hyphenator h = loadHyphenator(languageTag); |
| if (h != null) { |
| sMap.put(Locale.forLanguageTag(languageTag), h); |
| } |
| } |
| |
| for (int i = 0; i < LOCALE_FALLBACK_DATA.length; i++) { |
| String language = LOCALE_FALLBACK_DATA[i][0]; |
| String fallback = LOCALE_FALLBACK_DATA[i][1]; |
| sMap.put(Locale.forLanguageTag(language), sMap.get(Locale.forLanguageTag(fallback))); |
| } |
| } |
| } |