Snap for 5112637 from 562440a8c9cb5592a284c832d4b18fec93773d6c to qt-release

Change-Id: Ia85e5f890c6789256d7aa99f40df54f555a1c852
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index 24c63e7..c611c22 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -664,6 +664,48 @@
   ASSERT_EQ(tgt_content, updated_content);
 }
 
+TEST_F(UpdaterTest, block_image_update_patch_underrun) {
+  std::string src_content = std::string(4096, 'a') + std::string(4096, 'c');
+  std::string tgt_content = std::string(4096, 'b') + std::string(4096, 'd');
+
+  // Generate the patch data. We intentionally provide one-byte short target to trigger the underrun
+  // path.
+  TemporaryFile patch_file;
+  ASSERT_EQ(0,
+            bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(),
+                           reinterpret_cast<const uint8_t*>(tgt_content.data()),
+                           tgt_content.size() - 1, patch_file.path, nullptr));
+  std::string patch_content;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content));
+
+  // Create the transfer list that contains a bsdiff.
+  std::string src_hash = get_sha1(src_content);
+  std::string tgt_hash = get_sha1(tgt_content);
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + src_hash + " 2,0,2",
+    android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,2 2 - %s:2,0,2", patch_content.size(),
+                                src_hash.c_str(), tgt_hash.c_str(), src_hash.c_str()),
+    "free " + src_hash,
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", patch_content },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
+
+  // The update should fail due to underrun.
+  RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure);
+}
+
 TEST_F(UpdaterTest, block_image_update_fail) {
   std::string src_content(4096 * 2, 'e');
   std::string src_hash = get_sha1(src_content);
diff --git a/tests/testdata/jarsigned.zip b/tests/testdata/jarsigned.zip
deleted file mode 100644
index 8b1ef8b..0000000
--- a/tests/testdata/jarsigned.zip
+++ /dev/null
Binary files differ
diff --git a/tests/testdata/patch.bsdiff b/tests/testdata/patch.bsdiff
deleted file mode 100644
index b78d385..0000000
--- a/tests/testdata/patch.bsdiff
+++ /dev/null
Binary files differ
diff --git a/tests/testdata/unsigned.zip b/tests/testdata/unsigned.zip
deleted file mode 100644
index 24e3ead..0000000
--- a/tests/testdata/unsigned.zip
+++ /dev/null
Binary files differ
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
index b910337..8ed696a 100644
--- a/tools/image_generator/ImageGenerator.java
+++ b/tools/image_generator/ImageGenerator.java
@@ -26,10 +26,12 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.StringTokenizer;
 
@@ -115,6 +117,25 @@
     put("zh", "NotoSansCJK-Regular");
   }};
 
+  // Languages that write from right to left.
+  private static final Set<String> RTL_LANGUAGE = new HashSet<String>() {{
+    add("ar"); // Arabic
+    add("fa"); // Persian
+    add("he"); // Hebrew
+    add("iw"); // Hebrew
+    add("ur"); // Urdu
+  }};
+
+  // Languages that breaks on arbitrary characters.
+  // TODO(xunchang) switch to icu library if possible.
+  private static final Set<String> LOGOGRAM_LANGUAGE = new HashSet<String>() {{
+    add("ja"); // Japanese
+    add("km"); // Khmer
+    add("ko"); // Korean
+    add("lo"); // Lao
+    add("zh"); // Chinese
+  }};
+
   /**
    * Exception to indicate the failure to find the translated text strings.
    */
@@ -220,7 +241,20 @@
     }
 
     Map<Locale, String> result =
-        new TreeMap<Locale, String>(Comparator.comparing(Locale::toLanguageTag));
+        // Overrides the string comparator so that sr is sorted behind sr-Latn. And thus recovery
+        // can find the most relevant locale when going down the list.
+        new TreeMap<>((Locale l1, Locale l2) -> {
+          if (l1.toLanguageTag().equals(l2.toLanguageTag())) {
+            return 0;
+          }
+          if (l1.getLanguage().equals(l2.toLanguageTag())) {
+            return -1;
+          }
+          if (l2.getLanguage().equals(l1.toLanguageTag())) {
+            return 1;
+          }
+          return l1.toLanguageTag().compareTo(l2.toLanguageTag());
+        });
 
     // Find all the localized resource subdirectories in the format of values-$LOCALE
     String[] nameList = resourceDir.list(
@@ -269,6 +303,8 @@
     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();
@@ -284,6 +320,25 @@
   }
 
   /**
+   * One character is a word for CJK.
+   */
+  private List<String> wrapTextByCharacters(String text, FontMetrics metrics) {
+    List<String> wrappedText = new ArrayList<>();
+
+    StringBuilder line = new StringBuilder();
+    for (char token : text.toCharArray()) {
+      if (metrics.stringWidth(line + Character.toString(token)) > mImageWidth) {
+        wrappedText.add(line.toString());
+        line = new StringBuilder();
+      }
+      line.append(token);
+    }
+    wrappedText.add(line.toString());
+
+    return wrappedText;
+  }
+
+  /**
    * Wraps the text with a maximum of mImageWidth pixels per line.
    *
    * @param text the string representation of text to wrap
@@ -292,15 +347,36 @@
    *
    * @return a list of strings with their width smaller than mImageWidth pixels
    */
-  private List<String> wrapText(String text, FontMetrics metrics) {
-    // TODO(xunchang) handle other cases of text wrapping
-    // 1. RTL languages: "ar"(Arabic), "fa"(Persian), "he"(Hebrew), "iw"(Hebrew), "ur"(Urdu)
-    // 2. Language uses characters: CJK, "lo"(lao), "km"(khmer)
+  private List<String> wrapText(String text, FontMetrics metrics, String language) {
+    if (LOGOGRAM_LANGUAGE.contains(language)) {
+      return wrapTextByCharacters(text, metrics);
+    }
 
     return wrapTextByWords(text, metrics);
   }
 
   /**
+   * Encodes the information of the text image for |locale|.
+   * According to minui/resources.cpp, the width, height and locale of the image is decoded as:
+   *   int w = (row[1] << 8) | row[0];
+   *   int h = (row[3] << 8) | row[2];
+   *   __unused int len = row[4];
+   *   char* loc = reinterpret_cast<char*>(&row[5]);
+  */
+  private List<Integer> encodeTextInfo(int width, int height, String locale) {
+    List<Integer> info = new ArrayList<>(Arrays.asList(width & 0xff, width >> 8,
+        height & 0xff, height >> 8, locale.length()));
+
+    byte[] localeBytes = locale.getBytes();
+    for (byte b: localeBytes) {
+      info.add((int)b);
+    }
+    info.add(0);
+
+    return info;
+  }
+
+  /**
    * Draws the text string on the canvas for given locale.
    *
    * @param text the string to draw on canvas
@@ -309,17 +385,23 @@
    * @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) throws IOException, FontFormatException  {
+  private void drawText(String text, Locale locale, String languageTag, boolean centralAlignment)
+      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("Drawing text for locale " + locale + " text " + text);
+    System.out.println("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
 
     FontMetrics fontMetrics = graphics.getFontMetrics();
-    List<String> wrappedText = wrapTextByWords(text, fontMetrics);
+    List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+
+    // Marks the start y offset for the text image of current locale; and reserves one line to
+    // encode the image metadata.
+    int currentImageStart = mVerticalOffset;
+    mVerticalOffset += 1;
     for (String line : wrappedText) {
       int lineHeight = fontMetrics.getHeight();
       // Doubles the height of the image if we are short of space.
@@ -329,9 +411,23 @@
 
       // Draws the text at mVerticalOffset and increments the offset with line space.
       int baseLine = mVerticalOffset + lineHeight - fontMetrics.getDescent();
-      graphics.drawString(line, 0, baseLine);
+
+      // Draws from right if it's an RTL language.
+      int x = centralAlignment ? (mImageWidth - fontMetrics.stringWidth(line)) / 2 :
+          RTL_LANGUAGE.contains(languageTag) ? mImageWidth - fontMetrics.stringWidth(line) : 0;
+
+      graphics.drawString(line, x, baseLine);
+
       mVerticalOffset += lineHeight;
     }
+
+    // Encodes the metadata of the current localized image as pixels.
+    int currentImageHeight = mVerticalOffset - currentImageStart - 1;
+    List<Integer> info = encodeTextInfo(mImageWidth, currentImageHeight, languageTag);
+    for (int i = 0; i < info.size(); i++) {
+      int pixel[] =  { info.get(i) };
+      mBufferedImage.getRaster().setPixel(i, currentImageStart, pixel);
+    }
   }
 
   /**
@@ -362,11 +458,24 @@
    */
   public void generateImage(Map<Locale, String> localizedTextMap, String outputPath) throws
       FontFormatException, IOException {
+    Map<String, Integer> languageCount = new TreeMap<>();
     for (Locale locale : localizedTextMap.keySet()) {
-      // TODO(xunchang) reprocess the locales for the same language and make the last locale the
-      // catch-all type. e.g. "zh-CN, zh-HK, zh-TW" will become "zh-CN, zh-HK, zh"
-      // Or maybe we don't need to support these variants?
-      drawText(localizedTextMap.get(locale), locale);
+      String language = locale.getLanguage();
+      languageCount.put(language, languageCount.getOrDefault(language, 0) + 1 );
+    }
+
+    for (Locale locale : localizedTextMap.keySet()) {
+      Integer count = languageCount.get(locale.getLanguage());
+      // Recovery expects en-US instead of en_US.
+      String languageTag = locale.toLanguageTag();
+      if (count == 1) {
+        // Make the last country variant for a given language be the catch-all for that language.
+        languageTag = locale.getLanguage();
+      } else {
+        languageCount.put(locale.getLanguage(), count - 1);
+      }
+
+      drawText(localizedTextMap.get(locale), locale, languageTag, false);
     }
 
     // TODO(xunchang) adjust the width to save some space if all texts are smaller than imageWidth.
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index 47849a1..c4c0909 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -1399,7 +1399,10 @@
 
       // We expect the output of the patcher to fill the tgt ranges exactly.
       if (!writer.Finished()) {
-        LOG(ERROR) << "range sink underrun?";
+        LOG(ERROR) << "Failed to fully write target blocks (range sink underrun): Missing "
+                   << writer.AvailableSpace() << " bytes";
+        failure_type = kPatchApplicationFailure;
+        return -1;
       }
     } else {
       LOG(INFO) << "skipping " << blocks << " blocks already patched to " << tgt.blocks() << " ["