Home | History | Annotate | Download | only in recovery_l10n
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.recovery_l10n;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.res.AssetManager;
     23 import android.content.res.Configuration;
     24 import android.content.res.Resources;
     25 import android.graphics.Bitmap;
     26 import android.os.Bundle;
     27 import android.os.RemoteException;
     28 import android.util.DisplayMetrics;
     29 import android.util.Log;
     30 import android.view.View;
     31 import android.widget.Button;
     32 import android.widget.TextView;
     33 import android.widget.Spinner;
     34 import android.widget.ArrayAdapter;
     35 import android.widget.AdapterView;
     36 
     37 import java.io.FileOutputStream;
     38 import java.io.IOException;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.Comparator;
     42 import java.util.HashMap;
     43 import java.util.Locale;
     44 
     45 /**
     46  * This activity assists in generating the specially-formatted bitmaps
     47  * of text needed for recovery's localized text display.  Each image
     48  * contains all the translations of a single string; above each
     49  * translation is a "header row" that encodes that subimage's width,
     50  * height, and locale using pixel values.
     51  *
     52  * To use this app to generate new translations:
     53  *
     54  *   - Update the string resources in res/values-*
     55  *
     56  *   - Build and run the app.  Select the string you want to
     57  *     translate, and press the "Go" button.
     58  *
     59  *   - Wait for it to finish cycling through all the strings, then
     60  *     pull /data/data/com.android.recovery_l10n/files/text-out.png
     61  *     from the device.
     62  *
     63  *   - "pngcrush -c 0 text-out.png output.png"
     64  *
     65  *   - Put output.png in bootable/recovery/res/images/ (renamed
     66  *     appropriately).
     67  *
     68  * Recovery expects 8-bit 1-channel images (white text on black
     69  * background).  pngcrush -c 0 will convert the output of this program
     70  * to such an image.  If you use any other image handling tools,
     71  * remember that they must be lossless to preserve the exact values of
     72  * pixels in the header rows; don't convert them to jpeg or anything.
     73  */
     74 
     75 public class Main extends Activity {
     76     private static final String TAG = "RecoveryL10N";
     77 
     78     HashMap<Locale, Bitmap> savedBitmaps;
     79     TextView mText;
     80     int mStringId = R.string.recovery_installing;
     81 
     82     public class TextCapture implements Runnable {
     83         private Locale nextLocale;
     84         private Locale thisLocale;
     85         private Runnable next;
     86 
     87         TextCapture(Locale thisLocale, Locale nextLocale, Runnable next) {
     88             this.nextLocale = nextLocale;
     89             this.thisLocale = thisLocale;
     90             this.next = next;
     91         }
     92 
     93         public void run() {
     94             Bitmap b = mText.getDrawingCache();
     95             savedBitmaps.put(thisLocale, b.copy(Bitmap.Config.ARGB_8888, false));
     96 
     97             if (nextLocale != null) {
     98                 switchTo(nextLocale);
     99             }
    100 
    101             if (next != null) {
    102                 mText.postDelayed(next, 200);
    103             }
    104         }
    105     }
    106 
    107     private void switchTo(Locale locale) {
    108         Resources standardResources = getResources();
    109         AssetManager assets = standardResources.getAssets();
    110         DisplayMetrics metrics = standardResources.getDisplayMetrics();
    111         Configuration config = new Configuration(standardResources.getConfiguration());
    112         config.locale = locale;
    113         Resources defaultResources = new Resources(assets, metrics, config);
    114 
    115         mText.setText(mStringId);
    116 
    117         mText.setDrawingCacheEnabled(false);
    118         mText.setDrawingCacheEnabled(true);
    119         mText.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    120     }
    121 
    122     @Override
    123     public void onCreate(Bundle savedInstance) {
    124         super.onCreate(savedInstance);
    125         setContentView(R.layout.main);
    126 
    127         savedBitmaps = new HashMap<Locale, Bitmap>();
    128 
    129         Spinner spinner = (Spinner) findViewById(R.id.which);
    130         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
    131             this, R.array.string_options, android.R.layout.simple_spinner_item);
    132         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    133         spinner.setAdapter(adapter);
    134         spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    135             @Override
    136             public void onItemSelected(AdapterView parent, View view,
    137                                        int pos, long id) {
    138                 switch (pos) {
    139                     case 0: mStringId = R.string.recovery_installing; break;
    140                     case 1: mStringId = R.string.recovery_erasing; break;
    141                     case 2: mStringId = R.string.recovery_no_command; break;
    142                     case 3: mStringId = R.string.recovery_error; break;
    143                     case 4: mStringId = R.string.recovery_installing_security; break;
    144                 }
    145             }
    146             @Override public void onNothingSelected(AdapterView parent) { }
    147             });
    148 
    149         mText = (TextView) findViewById(R.id.text);
    150 
    151         String[] localeNames = getAssets().getLocales();
    152         Arrays.sort(localeNames, new Comparator<String>() {
    153         // Override the string comparator so that en is sorted behind en_US.
    154         // As a result, en_US will be matched first in recovery.
    155             @Override
    156             public int compare(String s1, String s2) {
    157                 if (s1.equals(s2)) {
    158                     return 0;
    159                 } else if (s1.startsWith(s2)) {
    160                     return -1;
    161                 } else if (s2.startsWith(s1)) {
    162                     return 1;
    163                 }
    164                 return s1.compareTo(s2);
    165             }
    166         });
    167 
    168         ArrayList<Locale> locales = new ArrayList<Locale>();
    169         for (String localeName : localeNames) {
    170             Log.i(TAG, "locale = " + localeName);
    171             if (!localeName.isEmpty()) {
    172                 locales.add(Locale.forLanguageTag(localeName));
    173             }
    174         }
    175 
    176         final Runnable seq = buildSequence(locales.toArray(new Locale[0]));
    177 
    178         Button b = (Button) findViewById(R.id.go);
    179         b.setOnClickListener(new View.OnClickListener() {
    180             @Override
    181             public void onClick(View ignore) {
    182                 mText.post(seq);
    183             }
    184             });
    185     }
    186 
    187     private Runnable buildSequence(final Locale[] locales) {
    188         Runnable head = new Runnable() { public void run() { mergeBitmaps(locales); } };
    189         Locale prev = null;
    190         for (Locale loc : locales) {
    191             head = new TextCapture(loc, prev, head);
    192             prev = loc;
    193         }
    194         final Runnable fhead = head;
    195         final Locale floc = prev;
    196         return new Runnable() { public void run() { startSequence(fhead, floc); } };
    197     }
    198 
    199     private void startSequence(Runnable firstRun, Locale firstLocale) {
    200         savedBitmaps.clear();
    201         switchTo(firstLocale);
    202         mText.postDelayed(firstRun, 200);
    203     }
    204 
    205     private void saveBitmap(Bitmap b, String filename) {
    206         try {
    207             FileOutputStream fos = openFileOutput(filename, 0);
    208             b.compress(Bitmap.CompressFormat.PNG, 100, fos);
    209             fos.close();
    210         } catch (IOException e) {
    211             Log.i(TAG, "failed to write PNG", e);
    212         }
    213     }
    214 
    215     private int colorFor(byte b) {
    216         return 0xff000000 | (b<<16) | (b<<8) | b;
    217     }
    218 
    219     private int colorFor(int b) {
    220         return 0xff000000 | (b<<16) | (b<<8) | b;
    221     }
    222 
    223     private void mergeBitmaps(final Locale[] locales) {
    224         HashMap<String, Integer> countByLanguage = new HashMap<String, Integer>();
    225 
    226         int height = 2;
    227         int width = 10;
    228         int maxHeight = 0;
    229         for (Locale loc : locales) {
    230             Bitmap b = savedBitmaps.get(loc);
    231             int h = b.getHeight();
    232             int w = b.getWidth();
    233             height += h+1;
    234             if (h > maxHeight) maxHeight = h;
    235             if (w > width) width = w;
    236 
    237             String lang = loc.getLanguage();
    238             if (countByLanguage.containsKey(lang)) {
    239                 countByLanguage.put(lang, countByLanguage.get(lang)+1);
    240             } else {
    241                 countByLanguage.put(lang, 1);
    242             }
    243         }
    244 
    245         Log.i(TAG, "output bitmap is " + width + " x " + height);
    246         Bitmap out = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    247         out.eraseColor(0xff000000);
    248         int[] pixels = new int[maxHeight * width];
    249 
    250         int p = 0;
    251         for (Locale loc : locales) {
    252             Bitmap bm = savedBitmaps.get(loc);
    253             int h = bm.getHeight();
    254             int w = bm.getWidth();
    255 
    256             bm.getPixels(pixels, 0, w, 0, 0, w, h);
    257 
    258             // Find the rightmost and leftmost columns with any
    259             // nonblack pixels; we'll copy just that region to the
    260             // output image.
    261 
    262             int right = w;
    263             while (right > 1) {
    264                 boolean all_black = true;
    265                 for (int j = 0; j < h; ++j) {
    266                     if (pixels[j*w+right-1] != 0xff000000) {
    267                         all_black = false;
    268                         break;
    269                     }
    270                 }
    271                 if (all_black) {
    272                     --right;
    273                 } else {
    274                     break;
    275                 }
    276             }
    277 
    278             int left = 0;
    279             while (left < right-1) {
    280                 boolean all_black = true;
    281                 for (int j = 0; j < h; ++j) {
    282                     if (pixels[j*w+left] != 0xff000000) {
    283                         all_black = false;
    284                         break;
    285                     }
    286                 }
    287                 if (all_black) {
    288                     ++left;
    289                 } else {
    290                     break;
    291                 }
    292             }
    293 
    294             // Make the last country variant for a given language be
    295             // the catch-all for that language (because recovery will
    296             // take the first one that matches).
    297             String lang = loc.getLanguage();
    298             if (countByLanguage.get(lang) > 1) {
    299                 countByLanguage.put(lang, countByLanguage.get(lang)-1);
    300                 lang = loc.toString();
    301             }
    302             int tw = right - left;
    303             Log.i(TAG, "encoding \"" + loc + "\" as \"" + lang + "\": " + tw + " x " + h);
    304             byte[] langBytes = lang.getBytes();
    305             out.setPixel(0, p, colorFor(tw & 0xff));
    306             out.setPixel(1, p, colorFor(tw >>> 8));
    307             out.setPixel(2, p, colorFor(h & 0xff));
    308             out.setPixel(3, p, colorFor(h >>> 8));
    309             out.setPixel(4, p, colorFor(langBytes.length));
    310             int x = 5;
    311             for (byte b : langBytes) {
    312                 out.setPixel(x, p, colorFor(b));
    313                 x++;
    314             }
    315             out.setPixel(x, p, colorFor(0));
    316 
    317             p++;
    318 
    319             out.setPixels(pixels, left, w, 0, p, tw, h);
    320             p += h;
    321         }
    322 
    323         // if no languages match, suppress text display by using a
    324         // single black pixel as the image.
    325         out.setPixel(0, p, colorFor(1));
    326         out.setPixel(1, p, colorFor(0));
    327         out.setPixel(2, p, colorFor(1));
    328         out.setPixel(3, p, colorFor(0));
    329         out.setPixel(4, p, colorFor(0));
    330         p++;
    331 
    332         saveBitmap(out, "text-out.png");
    333         Log.i(TAG, "wrote text-out.png");
    334     }
    335 }
    336