Bug 1023451 - Part 2: Apply basic heuristics for locale usability. r=mcomella, a=sledru
authorRichard Newman <rnewman@mozilla.com>
Fri, 20 Jun 2014 14:31:53 -0700
changeset 208573 c4beb9f4379fdc6b6cbee53845eef3d0e1f3ed82
parent 208572 a79aeb6214b572d65f1dd91113e354c616147088
child 208574 1d16403e76a50b425f1e5ec6db5dca92c5eb49a8
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella, sledru
bugs1023451
milestone32.0a2
Bug 1023451 - Part 2: Apply basic heuristics for locale usability. r=mcomella, a=sledru
mobile/android/base/preferences/LocaleListPreference.java
--- a/mobile/android/base/preferences/LocaleListPreference.java
+++ b/mobile/android/base/preferences/LocaleListPreference.java
@@ -1,41 +1,93 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.preferences;
 
+import java.nio.ByteBuffer;
 import java.text.Collator;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
 
 import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.R;
 
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
 import android.preference.ListPreference;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 
 public class LocaleListPreference extends ListPreference {
     private static final String LOG_TAG = "GeckoLocaleList";
 
+    /**
+     * With thanks to <http://stackoverflow.com/a/22679283/22003> for the
+     * initial solution.
+     *
+     * This class encapsulates an approach to checking whether a script
+     * is usable on a device. We attempt to draw a character from the
+     * script (e.g., ব). If the fonts on the device don't have the correct
+     * glyph, Android typically renders whitespace (rather than .notdef).
+     *
+     * Pass in part of the name of the locale in its local representation,
+     * and a whitespace character; this class performs the graphical comparison.
+     *
+     * See Bug 1023451 Comment 24 for extensive explanation.
+     */
+    private static class CharacterValidator {
+        private static final int BITMAP_WIDTH = 32;
+        private static final int BITMAP_HEIGHT = 48;
+
+        private final Paint paint = new Paint();
+        private final byte[] missingCharacter;
+
+        public CharacterValidator(String missing) {
+            this.missingCharacter = getPixels(drawBitmap(missing));
+        }
+
+        private Bitmap drawBitmap(String text){
+            Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8);
+            Canvas c = new Canvas(b);
+            c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
+            return b;
+        }
+        private static byte[] getPixels(Bitmap b) {
+            ByteBuffer buffer = ByteBuffer.allocate(b.getByteCount());
+            b.copyPixelsToBuffer(buffer);
+            return buffer.array();
+        }
+
+        public boolean characterIsMissingInFont(String ch) {
+            byte[] rendered = getPixels(drawBitmap(ch));
+            return Arrays.equals(rendered, missingCharacter);
+        }
+    }
+
     private volatile Locale entriesLocale;
+    private final CharacterValidator characterValidator;
 
     public LocaleListPreference(Context context) {
         this(context, null);
     }
 
     public LocaleListPreference(Context context, AttributeSet attributes) {
         super(context, attributes);
+
+        // Thus far, missing glyphs are replaced by whitespace, not a box
+        // or other Unicode codepoint.
+        this.characterValidator = new CharacterValidator(" ");
         buildList();
     }
 
     private static final class LocaleDescriptor implements Comparable<LocaleDescriptor> {
         // We use Locale.US here to ensure a stable ordering of entries.
         private static final Collator COLLATOR = Collator.getInstance(Locale.US);
 
         public final String tag;
@@ -85,20 +137,51 @@ public class LocaleListPreference extend
 
         @Override
         public int compareTo(LocaleDescriptor another) {
             // We sort by name, so we use Collator.
             return COLLATOR.compare(this.nativeName, another.nativeName);
         }
 
         /**
+         * See Bug 1023451 Comment 10 for the research that led to
+         * this method.
+         *
          * @return true if this locale can be used for displaying UI
          *         on this device without known issues.
          */
-        public boolean isUsable() {
+        public boolean isUsable(CharacterValidator validator) {
+            // Oh, for Java 7 switch statements.
+            if (this.tag.equals("bn-IN")) {
+                // Bengali sometimes has an English label if the Bengali script
+                // is missing. This prevents us from simply checking character
+                // rendering for bn-IN; we'll get a false positive for "B", not "ব".
+                //
+                // This doesn't seem to affect other Bengali-script locales
+                // (below), which always have a label in native script.
+                if (!this.nativeName.startsWith("বাংলা")) {
+                    // We're on an Android version that doesn't even have
+                    // characters to say বাংলা. Definite failure.
+                    return false;
+                }
+            }
+
+            // These locales use a script that is often unavailable
+            // on common Android devices. Make sure we can show them.
+            // See documentation for CharacterValidator.
+            // Note that bn-IN is checked here even if it passed above.
+            if (this.tag.equals("or") ||
+                this.tag.equals("pa-IN") ||
+                this.tag.equals("gu-IN") ||
+                this.tag.equals("bn-IN")) {
+                if (validator.characterIsMissingInFont(this.nativeName.substring(0, 1))) {
+                    return false;
+                }
+            }
+
             return true;
         }
     }
 
     /**
      * Not every locale we ship can be used on every device, due to
      * font or rendering constraints.
      *
@@ -113,17 +196,17 @@ public class LocaleListPreference extend
             return new LocaleDescriptor[] { new LocaleDescriptor(fallbackTag) };
         }
 
         final int initialCount = shippingLocales.size();
         final Set<LocaleDescriptor> locales = new HashSet<LocaleDescriptor>(initialCount);
         for (String tag : shippingLocales) {
             final LocaleDescriptor descriptor = new LocaleDescriptor(tag);
 
-            if (!descriptor.isUsable()) {
+            if (!descriptor.isUsable(this.characterValidator)) {
                 Log.w(LOG_TAG, "Skipping locale " + tag + " on this device.");
                 continue;
             }
 
             locales.add(descriptor);
         }
 
         final int usableCount = locales.size();