mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-07-01 14:29:41 +00:00
317 lines
12 KiB
Java
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);
|
|
}
|
|
}
|