Merge "switch to using android-base/file.h instead of android-base/test_utils.h"
diff --git a/minadbd/Android.bp b/minadbd/Android.bp
index 370232b..7e33261 100644
--- a/minadbd/Android.bp
+++ b/minadbd/Android.bp
@@ -15,6 +15,7 @@
 cc_defaults {
     name: "minadbd_defaults",
 
+    cpp_std: "gnu++17",
     cflags: [
         "-DADB_HOST=0",
         "-Wall",
diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp
index 54329db..d7fdb8f 100644
--- a/tests/component/resources_test.cpp
+++ b/tests/component/resources_test.cpp
@@ -101,7 +101,7 @@
     EXPECT_LT(0, len) << "Locale string should be non-empty.";
     EXPECT_NE(0, row[5]) << "Locale string is missing.";
 
-    ASSERT_GT(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
+    ASSERT_GE(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
     char* loc = reinterpret_cast<char*>(&row[5]);
     if (matches_locale(loc, kLocale.c_str())) {
       EXPECT_TRUE(android::base::StartsWith(loc, kLocale));
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
index a011f10..8730945 100644
--- a/tools/image_generator/ImageGenerator.java
+++ b/tools/image_generator/ImageGenerator.java
@@ -60,8 +60,9 @@
     // This is the canvas we used to draw texts.
     private BufferedImage mBufferedImage;
 
-    // The width in pixels of our image. Once set, its value won't change.
-    private final int mImageWidth;
+    // The width in pixels of our image. The value will be adjusted once when we calculate the
+    // maximum width to fit the wrapped text strings.
+    private int mImageWidth;
 
     // The current height in pixels of our image. We will adjust the value when drawing more texts.
     private int mImageHeight;
@@ -79,6 +80,9 @@
     // The directory that contains all the needed font files (e.g. ttf, otf, ttc files).
     private final String mFontDirPath;
 
+    // Align the text in the center of the image.
+    private final boolean mCenterAlignment;
+
     // An explicit map from language to the font name to use.
     // The map is extracted from frameworks/base/data/fonts/fonts.xml.
     // And the language-subtag-registry is found in:
@@ -130,7 +134,9 @@
             };
 
     // Languages that breaks on arbitrary characters.
-    // TODO(xunchang) switch to icu library if possible.
+    // TODO(xunchang) switch to icu library if possible. For example, for Thai and Khmer, there is
+    // no space between words; and word breaking is based on grammatical analysis and on word
+    // matching in dictionaries.
     private static final Set<String> LOGOGRAM_LANGUAGE =
             new HashSet<String>() {
                 {
@@ -138,6 +144,7 @@
                     add("km"); // Khmer
                     add("ko"); // Korean
                     add("lo"); // Lao
+                    add("th"); // Thai
                     add("zh"); // Chinese
                 }
             };
@@ -154,8 +161,13 @@
     }
 
     /** Initailizes the fields of the image image. */
-    public ImageGenerator(int imageWidth, String textName, float fontSize, String fontDirPath) {
-        mImageWidth = imageWidth;
+    public ImageGenerator(
+            int initialImageWidth,
+            String textName,
+            float fontSize,
+            String fontDirPath,
+            boolean centerAlignment) {
+        mImageWidth = initialImageWidth;
         mImageHeight = INITIAL_HEIGHT;
         mVerticalOffset = 0;
 
@@ -165,6 +177,8 @@
         mTextName = textName;
         mFontSize = fontSize;
         mFontDirPath = fontDirPath;
+
+        mCenterAlignment = centerAlignment;
     }
 
     /**
@@ -299,8 +313,6 @@
         List<String> wrappedText = new ArrayList<>();
         StringTokenizer st = new StringTokenizer(text, " \n");
 
-        // TODO(xunchang). We assume that all words can fit on the screen. Raise an
-        // IllegalStateException if the word is wider than the image width.
         StringBuilder line = new StringBuilder();
         while (st.hasMoreTokens()) {
             String token = st.nextToken();
@@ -373,6 +385,43 @@
         return info;
     }
 
+    /** Returns Graphics2D object that uses the given locale. */
+    private Graphics2D createGraphics(Locale locale) throws IOException, FontFormatException {
+        Graphics2D graphics = mBufferedImage.createGraphics();
+        graphics.setColor(Color.WHITE);
+        graphics.setRenderingHint(
+                RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
+        graphics.setFont(loadFontsByLocale(locale.getLanguage()));
+
+        return graphics;
+    }
+
+    /** Returns the maximum screen width needed to fit the given text after wrapping. */
+    private int measureTextWidth(String text, Locale locale)
+            throws IOException, FontFormatException {
+        Graphics2D graphics = createGraphics(locale);
+        FontMetrics fontMetrics = graphics.getFontMetrics();
+        List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+
+        int textWidth = 0;
+        for (String line : wrappedText) {
+            textWidth = Math.max(textWidth, fontMetrics.stringWidth(line));
+        }
+
+        // This may happen if one single word is larger than the image width.
+        if (textWidth > mImageWidth) {
+            throw new IllegalStateException(
+                    "Wrapped text width "
+                            + textWidth
+                            + " is larger than image width "
+                            + mImageWidth
+                            + " for locale: "
+                            + locale);
+        }
+
+        return textWidth;
+    }
+
     /**
      * Draws the text string on the canvas for given locale.
      *
@@ -381,16 +430,11 @@
      * @throws IOException if we cannot find the corresponding font file for the given locale.
      * @throws FontFormatException if we failed to load the font file for the given locale.
      */
-    private void drawText(String text, Locale locale, String languageTag, boolean centralAlignment)
+    private void drawText(String text, Locale locale, String languageTag)
             throws IOException, FontFormatException {
-        Graphics2D graphics = mBufferedImage.createGraphics();
-        graphics.setColor(Color.WHITE);
-        graphics.setRenderingHint(
-                RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
-        graphics.setFont(loadFontsByLocale(locale.getLanguage()));
-
         System.out.println("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
 
+        Graphics2D graphics = createGraphics(locale);
         FontMetrics fontMetrics = graphics.getFontMetrics();
         List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
 
@@ -402,7 +446,7 @@
             int lineHeight = fontMetrics.getHeight();
             // Doubles the height of the image if we are short of space.
             if (mVerticalOffset + lineHeight >= mImageHeight) {
-                resizeHeight(mImageHeight * 2);
+                resize(mImageWidth, mImageHeight * 2);
             }
 
             // Draws the text at mVerticalOffset and increments the offset with line space.
@@ -410,7 +454,7 @@
 
             // Draws from right if it's an RTL language.
             int x =
-                    centralAlignment
+                    mCenterAlignment
                             ? (mImageWidth - fontMetrics.stringWidth(line)) / 2
                             : RTL_LANGUAGE.contains(languageTag)
                                     ? mImageWidth - fontMetrics.stringWidth(line)
@@ -431,18 +475,19 @@
     }
 
     /**
-     * Redraws the image with the new height.
+     * Redraws the image with the new width and new height.
      *
+     * @param width the new width of the image in pixels.
      * @param height the new height of the image in pixels.
      */
-    private void resizeHeight(int height) {
-        BufferedImage resizedImage =
-                new BufferedImage(mImageWidth, height, BufferedImage.TYPE_BYTE_GRAY);
+    private void resize(int width, int height) {
+        BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
         Graphics2D graphic = resizedImage.createGraphics();
         graphic.drawImage(mBufferedImage, 0, 0, null);
         graphic.dispose();
 
         mBufferedImage = resizedImage;
+        mImageWidth = width;
         mImageHeight = height;
     }
 
@@ -458,11 +503,16 @@
     public void generateImage(Map<Locale, String> localizedTextMap, String outputPath)
             throws FontFormatException, IOException {
         Map<String, Integer> languageCount = new TreeMap<>();
+        int textWidth = 0;
         for (Locale locale : localizedTextMap.keySet()) {
             String language = locale.getLanguage();
             languageCount.put(language, languageCount.getOrDefault(language, 0) + 1);
+            textWidth = Math.max(textWidth, measureTextWidth(localizedTextMap.get(locale), locale));
         }
 
+        // Removes the black margins to reduce the size of the image.
+        resize(textWidth, mImageHeight);
+
         for (Locale locale : localizedTextMap.keySet()) {
             Integer count = languageCount.get(locale.getLanguage());
             // Recovery expects en-US instead of en_US.
@@ -475,12 +525,10 @@
                 languageCount.put(locale.getLanguage(), count - 1);
             }
 
-            drawText(localizedTextMap.get(locale), locale, languageTag, false);
+            drawText(localizedTextMap.get(locale), locale, languageTag);
         }
 
-        // TODO(xunchang) adjust the width to save some space if all texts are smaller than
-        // imageWidth.
-        resizeHeight(mVerticalOffset);
+        resize(mImageWidth, mVerticalOffset);
         ImageIO.write(mBufferedImage, "png", new File(outputPath));
     }
 
@@ -528,11 +576,17 @@
 
         options.addOption(
                 OptionBuilder.withLongOpt("output_file")
-                        .withDescription("Path to the generated image")
+                        .withDescription("Path to the generated image.")
                         .hasArgs(1)
                         .isRequired()
                         .create());
 
+        options.addOption(
+                OptionBuilder.withLongOpt("center_alignment")
+                        .withDescription("Align the text in the center of the screen.")
+                        .hasArg(false)
+                        .create());
+
         return options;
     }
 
@@ -557,7 +611,8 @@
                         imageWidth,
                         cmd.getOptionValue("text_name"),
                         DEFAULT_FONT_SIZE,
-                        cmd.getOptionValue("font_dir"));
+                        cmd.getOptionValue("font_dir"),
+                        cmd.hasOption("center_alignment"));
 
         Map<Locale, String> localizedStringMap =
                 imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"));