tenfourfox/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

317 lines
12 KiB
Java

/* 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.AppConstants.Versions;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.Locales;
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(final Bitmap b) {
final int byteCount;
if (Versions.feature19Plus) {
byteCount = b.getAllocationByteCount();
} else {
// Close enough for government work.
// Equivalent to getByteCount, but works on <12.
byteCount = b.getRowBytes() * b.getHeight();
}
final ByteBuffer buffer = ByteBuffer.allocate(byteCount);
try {
b.copyPixelsToBuffer(buffer);
} catch (RuntimeException e) {
// Android throws this if there's not enough space in the buffer.
// This should never occur, but if it does, we don't
// really care -- we probably don't need the entire image.
// This is awful. I apologize.
if ("Buffer not large enough for pixels".equals(e.getMessage())) {
return buffer.array();
}
throw e;
}
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;
private final String nativeName;
public LocaleDescriptor(String tag) {
this(Locales.parseLocaleCode(tag), tag);
}
public LocaleDescriptor(Locale locale, String tag) {
this.tag = tag;
final String displayName = locale.getDisplayName(locale);
if (TextUtils.isEmpty(displayName)) {
// There's nothing sane we can do.
Log.w(LOG_TAG, "Display name is empty. Using " + locale.toString());
this.nativeName = locale.toString();
return;
}
// For now, uppercase the first character of LTR locale names.
// This is pretty much what Android does. This is a reasonable hack
// for Bug 1014602, but it won't generalize to all locales.
final byte directionality = Character.getDirectionality(displayName.charAt(0));
if (directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
this.nativeName = displayName.substring(0, 1).toUpperCase(locale) +
displayName.substring(1);
return;
}
this.nativeName = displayName;
}
public String getTag() {
return this.tag;
}
public String getDisplayName() {
return this.nativeName;
}
@Override
public String toString() {
return this.nativeName;
}
@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(CharacterValidator validator) {
if (Versions.preLollipop && this.tag.matches("[a-zA-Z]{3}.*")) {
// Earlier versions of Android can't load three-char locale code
// resources.
return false;
}
// 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("my") ||
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.
*
* This method filters down the list before generating the descriptor array.
*/
private LocaleDescriptor[] getUsableLocales() {
Collection<String> shippingLocales = BrowserLocaleManager.getPackagedLocaleTags(getContext());
// Future: single-locale builds should be specified, too.
if (shippingLocales == null) {
final String fallbackTag = BrowserLocaleManager.getInstance().getFallbackLocaleTag();
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(this.characterValidator)) {
Log.w(LOG_TAG, "Skipping locale " + tag + " on this device.");
continue;
}
locales.add(descriptor);
}
final int usableCount = locales.size();
final LocaleDescriptor[] descriptors = locales.toArray(new LocaleDescriptor[usableCount]);
Arrays.sort(descriptors, 0, usableCount);
return descriptors;
}
@Override
protected void onDialogClosed(boolean positiveResult) {
// The superclass will take care of persistence.
super.onDialogClosed(positiveResult);
// Use this hook to try to fix up the environment ASAP.
// Do this so that the redisplayed fragment is inflated
// with the right locale.
final Locale selectedLocale = getSelectedLocale();
final Context context = getContext();
BrowserLocaleManager.getInstance().updateConfiguration(context, selectedLocale);
}
private Locale getSelectedLocale() {
final String tag = getValue();
if (tag == null || tag.equals("")) {
return Locale.getDefault();
}
return Locales.parseLocaleCode(tag);
}
@Override
public CharSequence getSummary() {
final String value = getValue();
if (TextUtils.isEmpty(value)) {
return getContext().getString(R.string.locale_system_default);
}
// We can't trust super.getSummary() across locale changes,
// apparently, so let's do the same work.
return new LocaleDescriptor(value).getDisplayName();
}
private void buildList() {
final Locale currentLocale = Locale.getDefault();
Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale);
if (currentLocale.equals(this.entriesLocale) &&
getEntries() != null) {
Log.v(LOG_TAG, "No need to build list.");
return;
}
final LocaleDescriptor[] descriptors = getUsableLocales();
final int count = descriptors.length;
this.entriesLocale = currentLocale;
// We leave room for "System default".
final String[] entries = new String[count + 1];
final String[] values = new String[count + 1];
entries[0] = getContext().getString(R.string.locale_system_default);
values[0] = "";
for (int i = 0; i < count; ++i) {
final String displayName = descriptors[i].getDisplayName();
final String tag = descriptors[i].getTag();
Log.v(LOG_TAG, displayName + " => " + tag);
entries[i + 1] = displayName;
values[i + 1] = tag;
}
setEntries(entries);
setEntryValues(values);
}
}