/* * Copyright (C) 2012 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 com.android.inputmethod.keyboard.tools; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import java.util.TreeMap; import java.util.jar.JarFile; public class MoreKeysResources { private static final String TEXT_RESOURCE_NAME = "donottranslate-more-keys.xml"; private static final String JAVA_TEMPLATE = "KeyboardTextsTable.tmpl"; private static final String MARK_NAMES = "@NAMES@"; private static final String MARK_DEFAULT_TEXTS = "@DEFAULT_TEXTS@"; private static final String MARK_TEXTS = "@TEXTS@"; private static final String TEXTS_ARRAY_NAME_PREFIX = "TEXTS_"; private static final String MARK_LOCALES_AND_TEXTS = "@LOCALES_AND_TEXTS@"; private static final String EMPTY_STRING_VAR = "EMPTY"; private final JarFile mJar; // String resources maps sorted by its language. The language is determined from the jar entry // name by calling {@link JarUtils#getLocaleFromEntryName(String)}. private final TreeMap mResourcesMap = new TreeMap<>(); // Default string resources map. private final StringResourceMap mDefaultResourceMap; // Histogram of string resource names. This is used to sort {@link #mSortedResourceNames}. private final HashMap mNameHistogram = new HashMap<>(); // Sorted string resource names array; Descending order of histogram count. // The string resource name is specified as an attribute "name" in string resource files. // The string resource can be accessed by specifying name "!text/" // via {@link KeyboardTextsSet#getText(String)}. private final String[] mSortedResourceNames; public MoreKeysResources(final JarFile jar) { mJar = jar; final ArrayList resourceEntryNames = JarUtils.getEntryNameListing( jar, TEXT_RESOURCE_NAME); for (final String entryName : resourceEntryNames) { final StringResourceMap resMap = new StringResourceMap(entryName); mResourcesMap.put(LocaleUtils.getLocaleCode(resMap.mLocale), resMap); } mDefaultResourceMap = mResourcesMap.get( LocaleUtils.getLocaleCode(LocaleUtils.DEFAULT_LOCALE)); // Initialize name histogram and names list. final HashMap nameHistogram = mNameHistogram; final ArrayList resourceNamesList = new ArrayList<>(); for (final StringResource res : mDefaultResourceMap.getResources()) { nameHistogram.put(res.mName, 0); // Initialize histogram value. resourceNamesList.add(res.mName); } // Make name histogram. for (final String locale : mResourcesMap.keySet()) { final StringResourceMap resMap = mResourcesMap.get(locale); if (resMap == mDefaultResourceMap) continue; for (final StringResource res : resMap.getResources()) { if (!mDefaultResourceMap.contains(res.mName)) { throw new RuntimeException(res.mName + " in " + locale + " doesn't have default resource"); } final int histogramValue = nameHistogram.get(res.mName); nameHistogram.put(res.mName, histogramValue + 1); } } // Sort names list. Collections.sort(resourceNamesList, new Comparator() { @Override public int compare(final String leftName, final String rightName) { final int leftCount = nameHistogram.get(leftName); final int rightCount = nameHistogram.get(rightName); // Descending order of histogram count. if (leftCount > rightCount) return -1; if (leftCount < rightCount) return 1; // TODO: Add further criteria to order the same histogram value names to be able to // minimize footprints of string resources arrays. return 0; } }); mSortedResourceNames = resourceNamesList.toArray(new String[resourceNamesList.size()]); } public void writeToJava(final String outDir) { final ArrayList list = JarUtils.getEntryNameListing(mJar, JAVA_TEMPLATE); if (list.isEmpty()) { throw new RuntimeException("Can't find java template " + JAVA_TEMPLATE); } if (list.size() > 1) { throw new RuntimeException("Found multiple java template " + JAVA_TEMPLATE); } final String template = list.get(0); final String javaPackage = template.substring(0, template.lastIndexOf('/')); PrintStream ps = null; LineNumberReader lnr = null; try { if (outDir == null) { ps = System.out; } else { final File outPackage = new File(outDir, javaPackage); final File outputFile = new File(outPackage, JAVA_TEMPLATE.replace(".tmpl", ".java")); outPackage.mkdirs(); ps = new PrintStream(outputFile, "UTF-8"); } lnr = new LineNumberReader(new InputStreamReader(JarUtils.openResource(template))); inflateTemplate(lnr, ps); } catch (IOException e) { throw new RuntimeException(e); } finally { JarUtils.close(lnr); JarUtils.close(ps); } } private void inflateTemplate(final LineNumberReader in, final PrintStream out) throws IOException { String line; while ((line = in.readLine()) != null) { if (line.contains(MARK_NAMES)) { dumpNames(out); } else if (line.contains(MARK_DEFAULT_TEXTS)) { dumpDefaultTexts(out); } else if (line.contains(MARK_TEXTS)) { dumpTexts(out); } else if (line.contains(MARK_LOCALES_AND_TEXTS)) { dumpLocalesMap(out); } else { out.println(line); } } } private void dumpNames(final PrintStream out) { final int namesCount = mSortedResourceNames.length; for (int index = 0; index < namesCount; index++) { final String name = mSortedResourceNames[index]; final int histogramValue = mNameHistogram.get(name); out.format(" /* %3d:%2d */ \"%s\",\n", index, histogramValue, name); } } private void dumpDefaultTexts(final PrintStream out) { final int outputArraySize = dumpTextsInternal(out, mDefaultResourceMap); mDefaultResourceMap.setOutputArraySize(outputArraySize); } private static String getArrayNameForLocale(final Locale locale) { return TEXTS_ARRAY_NAME_PREFIX + LocaleUtils.getLocaleCode(locale); } private void dumpTexts(final PrintStream out) { for (final StringResourceMap resMap : mResourcesMap.values()) { final Locale locale = resMap.mLocale; if (resMap == mDefaultResourceMap) continue; out.format(" /* Locale %s: %s */\n", locale, LocaleUtils.getLocaleDisplayName(locale)); out.format(" private static final String[] " + getArrayNameForLocale(locale) + " = {\n"); final int outputArraySize = dumpTextsInternal(out, resMap); resMap.setOutputArraySize(outputArraySize); out.format(" };\n\n"); } } private void dumpLocalesMap(final PrintStream out) { for (final StringResourceMap resMap : mResourcesMap.values()) { final Locale locale = resMap.mLocale; final String localeStr = LocaleUtils.getLocaleCode(locale); final String localeToDump = (locale == LocaleUtils.DEFAULT_LOCALE) ? String.format("\"%s\"", localeStr) : String.format("\"%s\"%s", localeStr, " ".substring(localeStr.length())); out.format(" %s, %-12s /* %3d/%3d %s */\n", localeToDump, getArrayNameForLocale(locale) + ",", resMap.getResources().size(), resMap.getOutputArraySize(), LocaleUtils.getLocaleDisplayName(locale)); } } private int dumpTextsInternal(final PrintStream out, final StringResourceMap resMap) { final ArrayInitializerFormatter formatter = new ArrayInitializerFormatter(out, 100, " ", mSortedResourceNames); int outputArraySize = 0; boolean successiveNull = false; final int namesCount = mSortedResourceNames.length; for (int index = 0; index < namesCount; index++) { final String name = mSortedResourceNames[index]; final StringResource res = resMap.get(name); if (res != null) { // TODO: Check whether the resource value is equal to the default. if (res.mComment != null) { formatter.outCommentLines(addPrefix(" // ", res. mComment)); } final String escaped = escapeNonAscii(res.mValue); if (escaped.length() == 0) { formatter.outElement(EMPTY_STRING_VAR + ","); } else { formatter.outElement(String.format("\"%s\",", escaped)); } successiveNull = false; outputArraySize = formatter.getCurrentIndex(); } else { formatter.outElement("null,"); successiveNull = true; } } if (!successiveNull) { formatter.flush(); } return outputArraySize; } private static String addPrefix(final String prefix, final String lines) { final StringBuilder sb = new StringBuilder(); for (final String line : lines.split("\n")) { sb.append(prefix + line.trim() + "\n"); } return sb.toString(); } private static String escapeNonAscii(final String text) { final StringBuilder sb = new StringBuilder(); final int length = text.length(); for (int i = 0; i < length; i++) { final char c = text.charAt(i); if (c >= ' ' && c < 0x7f) { sb.append(c); } else { sb.append(String.format("\\u%04X", (int)c)); } } return sb.toString(); } }