add an app to generate localized text bitmaps needed for recovery

Recovery only knows how to display images, so any text has to be
turned into bitmaps.  This app displays each localized version of the
text in a TextView and then captures a bitmap, combining them and
generating an output PNG.

Change-Id: Ib16858a1d66e9839d1255b962fb1aaf8982bfe10
diff --git a/tools/recovery_l10n/Android.mk b/tools/recovery_l10n/Android.mk
new file mode 100644
index 0000000..937abd1
--- /dev/null
+++ b/tools/recovery_l10n/Android.mk
@@ -0,0 +1,12 @@
+# Copyright 2012 Google Inc. All Rights Reserved.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := RecoveryLocalizer
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_PACKAGE)
diff --git a/tools/recovery_l10n/AndroidManifest.xml b/tools/recovery_l10n/AndroidManifest.xml
new file mode 100644
index 0000000..8c51a4e
--- /dev/null
+++ b/tools/recovery_l10n/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.recovery_l10n">
+
+  <application android:label="Recovery Localizer">
+    <activity android:name="Main"
+              android:label="Recovery Localizer">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+
+</manifest>
+
+
diff --git a/tools/recovery_l10n/res/layout/main.xml b/tools/recovery_l10n/res/layout/main.xml
new file mode 100644
index 0000000..1ac2b24
--- /dev/null
+++ b/tools/recovery_l10n/res/layout/main.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              >
+
+  <Spinner android:id="@+id/which"
+           android:layout_width="wrap_content"
+           android:layout_height="wrap_content"
+           />
+
+  <Button android:id="@+id/go"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:text="@string/go"
+          />
+
+  <TextView android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#ffffffff"
+            android:background="#ff000000"
+            android:maxWidth="480px"
+            />
+
+
+</LinearLayout>
+
+
diff --git a/tools/recovery_l10n/res/values-de/strings.xml b/tools/recovery_l10n/res/values-de/strings.xml
new file mode 100644
index 0000000..a03803d
--- /dev/null
+++ b/tools/recovery_l10n/res/values-de/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <string name="recovery_installing">Systemupdate wird installiert\u2026</string>
+  <string name="recovery_erasing">Löschen\u2026</string>
+  <string name="recovery_no_command">Kein Befehl.</string>
+  <string name="recovery_error">Fehler!</string>
+
+</resources>
diff --git a/tools/recovery_l10n/res/values/strings.xml b/tools/recovery_l10n/res/values/strings.xml
new file mode 100644
index 0000000..3a8aeec
--- /dev/null
+++ b/tools/recovery_l10n/res/values/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <!-- Do not translate. -->
+  <string translatable="false" name="go">Go</string>
+
+  <!-- Do not translate. -->
+  <string-array translatable="false" name="string_options">
+    <item>installing</item>
+    <item>erasing</item>
+    <item>no_command</item>
+    <item>error</item>
+  </string-array>
+
+  <!-- Displayed on the screen beneath the animated android while the
+       system is installing an update. [CHAR LIMIT=60] -->
+  <string name="recovery_installing">Installing system update\u2026</string>
+
+  <!-- Displayed on the screen beneath the animated android while the
+       system is erasing a partition (either a data wipe aka "factory
+       reset", or a cache wipe). [CHAR LIMIT=60] -->
+  <string name="recovery_erasing">Erasing\u2026</string>
+
+  <!-- Displayed on the screen when the user has gotten into recovery
+       mode without a command to run.  Will not normally happen, but
+       users (especially developers) may boot into recovery mode
+       manually via special key combinations.  [CHAR LIMIT=60] -->
+  <string name="recovery_no_command">No command.</string>
+
+  <!-- Displayed on the triangle-! screen when a system update
+       installation or data wipe procedure encounters an error.  [CHAR
+       LIMIT=60] -->
+  <string name="recovery_error">Error!</string>
+
+</resources>
diff --git a/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java b/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java
new file mode 100644
index 0000000..63ae3ea
--- /dev/null
+++ b/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java
@@ -0,0 +1,269 @@
+/*
+ * 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.recovery_l10n;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Spinner;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Locale;
+import java.util.HashMap;
+
+/**
+ * This activity assists in generating the specially-formatted bitmaps
+ * of text needed for recovery's localized text display.  Each image
+ * contains all the translations of a single string; above each
+ * translation is a "header row" that encodes that subimage's width,
+ * height, and locale using pixel values.
+ *
+ * To use this app to generate new translations:
+ *
+ *   - Update the string resources in res/values-*
+ *
+ *   - Update the list of desired locales (mLocales) below.  Locales
+ *     should be ordered by decreasing popularity (ie, the most
+ *     commonly-used one first).
+ *
+ *   - Build and run the app.  Select the string you want to
+ *     translate, and press the "Go" button.
+ *
+ *   - Wait for it to finish cycling through all the strings, then
+ *     pull /data/data/com.android.recovery_l10n/files/text-out.png
+ *     from the device.
+ *
+ *   - "pngcrush -c 0 text-out.png output.png"
+ *
+ *   - Put output.png in bootable/recovery/res/images/ (renamed
+ *     appropriately).
+ *
+ * Recovery expects 8-bit 1-channel images (white text on black
+ * background).  pngcrush -c 0 will convert the output of this program
+ * to such an image.  If you use any other image handling tools,
+ * remember that they must be lossless to preserve the exact values of
+ * pixels in the header rows; don't convert them to jpeg or anything.
+ */
+
+public class Main extends Activity {
+    private static final String TAG = "RecoveryL10N";
+
+    final Locale[] mLocales = new Locale[] { Locale.US, Locale.GERMANY };
+
+    HashMap<Locale, Bitmap> savedBitmaps;
+    TextView mText;
+    int mStringId = R.string.recovery_installing;
+
+    public class TextCapture implements Runnable {
+        private Locale nextLocale;
+        private Locale thisLocale;
+        private Runnable next;
+
+        TextCapture(Locale thisLocale, Locale nextLocale, Runnable next) {
+            this.nextLocale = nextLocale;
+            this.thisLocale = thisLocale;
+            this.next = next;
+        }
+
+        public void run() {
+            Bitmap b = mText.getDrawingCache();
+            savedBitmaps.put(thisLocale, b.copy(Bitmap.Config.ARGB_8888, false));
+
+            if (nextLocale != null) {
+                switchTo(nextLocale);
+            }
+
+            if (next != null) {
+                mText.postDelayed(next, 200);
+            }
+        }
+    }
+
+    private void switchTo(Locale locale) {
+        Resources standardResources = getResources();
+        AssetManager assets = standardResources.getAssets();
+        DisplayMetrics metrics = standardResources.getDisplayMetrics();
+        Configuration config = new Configuration(standardResources.getConfiguration());
+        config.locale = locale;
+        Resources defaultResources = new Resources(assets, metrics, config);
+
+        mText.setText(mStringId);
+
+        mText.setDrawingCacheEnabled(false);
+        mText.setDrawingCacheEnabled(true);
+        mText.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstance) {
+        super.onCreate(savedInstance);
+        setContentView(R.layout.main);
+
+        savedBitmaps = new HashMap<Locale, Bitmap>();
+
+        Spinner spinner = (Spinner) findViewById(R.id.which);
+        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
+            this, R.array.string_options, android.R.layout.simple_spinner_item);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinner.setAdapter(adapter);
+        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView parent, View view,
+                                       int pos, long id) {
+                switch (pos) {
+                    case 0: mStringId = R.string.recovery_installing; break;
+                    case 1: mStringId = R.string.recovery_erasing; break;
+                    case 2: mStringId = R.string.recovery_no_command; break;
+                    case 3: mStringId = R.string.recovery_error; break;
+                }
+            }
+            @Override public void onNothingSelected(AdapterView parent) { }
+            });
+
+        mText = (TextView) findViewById(R.id.text);
+
+        final Runnable seq = buildSequence(mLocales);
+
+        Button b = (Button) findViewById(R.id.go);
+        b.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View ignore) {
+                mText.post(seq);
+            }
+            });
+    }
+
+    private Runnable buildSequence(final Locale[] locales) {
+        Runnable head = new Runnable() { public void run() { mergeBitmaps(locales); } };
+        Locale prev = null;
+        for (Locale loc : locales) {
+            head = new TextCapture(loc, prev, head);
+            prev = loc;
+        }
+        final Runnable fhead = head;
+        final Locale floc = prev;
+        return new Runnable() { public void run() { startSequence(fhead, floc); } };
+    }
+
+    private void startSequence(Runnable firstRun, Locale firstLocale) {
+        savedBitmaps.clear();
+        switchTo(firstLocale);
+        mText.postDelayed(firstRun, 200);
+    }
+
+    private void saveBitmap(Bitmap b, String filename) {
+        try {
+            FileOutputStream fos = openFileOutput(filename, 0);
+            b.compress(Bitmap.CompressFormat.PNG, 100, fos);
+            fos.close();
+        } catch (IOException e) {
+            Log.i(TAG, "failed to write PNG", e);
+        }
+    }
+
+    private int colorFor(byte b) {
+        return 0xff000000 | (b<<16) | (b<<8) | b;
+    }
+
+    private int colorFor(int b) {
+        return 0xff000000 | (b<<16) | (b<<8) | b;
+    }
+
+    private void mergeBitmaps(final Locale[] locales) {
+        HashMap<String, Integer> countByLanguage = new HashMap<String, Integer>();
+
+        int height = 2;
+        int width = 10;
+        int maxHeight = 0;
+        for (Locale loc : locales) {
+            Bitmap b = savedBitmaps.get(loc);
+            int h = b.getHeight();
+            int w = b.getWidth();
+            height += h+1;
+            if (h > maxHeight) maxHeight = h;
+            if (w > width) width = w;
+
+            String lang = loc.getLanguage();
+            if (countByLanguage.containsKey(lang)) {
+                countByLanguage.put(lang, countByLanguage.get(lang)+1);
+            } else {
+                countByLanguage.put(lang, 1);
+            }
+        }
+
+        Log.i(TAG, "output bitmap is " + width + " x " + height);
+        Bitmap out = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        out.eraseColor(0xff000000);
+        int[] pixels = new int[maxHeight * width];
+
+        int p = 0;
+        for (Locale loc : locales) {
+            Bitmap bm = savedBitmaps.get(loc);
+            int h = bm.getHeight();
+            int w = bm.getWidth();
+
+            String lang = loc.getLanguage();
+            if (countByLanguage.get(lang) > 1) {
+                lang = loc.toString();
+            }
+            Log.i(TAG, "encoding \"" + loc + "\" as \"" + lang + "\"");
+            byte[] langBytes = lang.getBytes();
+            out.setPixel(0, p, colorFor(w & 0xff));
+            out.setPixel(1, p, colorFor(w >>> 8));
+            out.setPixel(2, p, colorFor(h & 0xff));
+            out.setPixel(3, p, colorFor(h >>> 8));
+            out.setPixel(4, p, colorFor(langBytes.length));
+            int x = 5;
+            for (byte b : langBytes) {
+                out.setPixel(x, p, colorFor(b));
+                x++;
+            }
+            out.setPixel(x, p, colorFor(0));
+
+            p++;
+
+            bm.getPixels(pixels, 0, w, 0, 0, w, h);
+            out.setPixels(pixels, 0, w, 0, p, w, h);
+            p += h;
+        }
+
+        // if no languages match, suppress text display by using a
+        // single black pixel as the image.
+        out.setPixel(0, p, colorFor(1));
+        out.setPixel(1, p, colorFor(0));
+        out.setPixel(2, p, colorFor(1));
+        out.setPixel(3, p, colorFor(0));
+        out.setPixel(4, p, colorFor(0));
+        p++;
+
+        saveBitmap(out, "text-out.png");
+    }
+}