/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.devrel.gmscore.tools.apk.arsc; import com.google.auto.value.AutoValue; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.primitives.Bytes; import com.google.common.primitives.UnsignedBytes; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** Describes a particular resource configuration. */ @AutoValue public abstract class ResourceConfiguration implements SerializableResource { /** * The different types of configs that can be present in a {@link ResourceConfiguration}. * * <p>The ordering of these types is roughly the same as {@code #isBetterThan}, but is not * guaranteed to be the same. */ public enum Type { MCC, MNC, LANGUAGE_STRING, LOCALE_SCRIPT_STRING, REGION_STRING, LOCALE_VARIANT_STRING, SCREEN_LAYOUT_DIRECTION, SMALLEST_SCREEN_WIDTH_DP, SCREEN_WIDTH_DP, SCREEN_HEIGHT_DP, SCREEN_LAYOUT_SIZE, SCREEN_LAYOUT_LONG, SCREEN_LAYOUT_ROUND, COLOR_MODE_WIDE_COLOR_GAMUT, // NB: COLOR_GAMUT takes priority over HDR in #isBetterThan. COLOR_MODE_HDR, ORIENTATION, UI_MODE_TYPE, UI_MODE_NIGHT, DENSITY_DPI, TOUCHSCREEN, KEYBOARD_HIDDEN, KEYBOARD, NAVIGATION_HIDDEN, NAVIGATION, SCREEN_SIZE, SDK_VERSION } private static final ResourceConfiguration.Builder DEFAULT_BUILDER = builder(); /** * The default configuration. This configuration acts as a "catch-all" for looking up resources * when no better configuration can be found. */ public static final ResourceConfiguration DEFAULT = DEFAULT_BUILDER.build(); /** The below constants are from android.content.res.Configuration. */ static final int COLOR_MODE_WIDE_COLOR_GAMUT_MASK = 0x03; static final int COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED = 0; static final int COLOR_MODE_WIDE_COLOR_GAMUT_NO = 0x01; static final int COLOR_MODE_WIDE_COLOR_GAMUT_YES = 0x02; private static final Map<Integer, String> COLOR_MODE_WIDE_COLOR_GAMUT_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED, ""); map.put(COLOR_MODE_WIDE_COLOR_GAMUT_NO, "nowidecg"); map.put(COLOR_MODE_WIDE_COLOR_GAMUT_YES, "widecg"); COLOR_MODE_WIDE_COLOR_GAMUT_VALUES = Collections.unmodifiableMap(map); } static final int COLOR_MODE_HDR_MASK = 0x0C; static final int COLOR_MODE_HDR_UNDEFINED = 0; static final int COLOR_MODE_HDR_NO = 0x04; static final int COLOR_MODE_HDR_YES = 0x08; private static final Map<Integer, String> COLOR_MODE_HDR_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(COLOR_MODE_HDR_UNDEFINED, ""); map.put(COLOR_MODE_HDR_NO, "lowdr"); map.put(COLOR_MODE_HDR_YES, "highdr"); COLOR_MODE_HDR_VALUES = Collections.unmodifiableMap(map); } static final int DENSITY_DPI_UNDEFINED = 0; static final int DENSITY_DPI_LDPI = 120; static final int DENSITY_DPI_MDPI = 160; static final int DENSITY_DPI_TVDPI = 213; static final int DENSITY_DPI_HDPI = 240; static final int DENSITY_DPI_XHDPI = 320; static final int DENSITY_DPI_XXHDPI = 480; static final int DENSITY_DPI_XXXHDPI = 640; static final int DENSITY_DPI_ANY = 0xFFFE; static final int DENSITY_DPI_NONE = 0xFFFF; private static final Map<Integer, String> DENSITY_DPI_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(DENSITY_DPI_UNDEFINED, ""); map.put(DENSITY_DPI_LDPI, "ldpi"); map.put(DENSITY_DPI_MDPI, "mdpi"); map.put(DENSITY_DPI_TVDPI, "tvdpi"); map.put(DENSITY_DPI_HDPI, "hdpi"); map.put(DENSITY_DPI_XHDPI, "xhdpi"); map.put(DENSITY_DPI_XXHDPI, "xxhdpi"); map.put(DENSITY_DPI_XXXHDPI, "xxxhdpi"); map.put(DENSITY_DPI_ANY, "anydpi"); map.put(DENSITY_DPI_NONE, "nodpi"); DENSITY_DPI_VALUES = Collections.unmodifiableMap(map); } static final int KEYBOARD_NOKEYS = 1; static final int KEYBOARD_QWERTY = 2; static final int KEYBOARD_12KEY = 3; private static final Map<Integer, String> KEYBOARD_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(KEYBOARD_NOKEYS, "nokeys"); map.put(KEYBOARD_QWERTY, "qwerty"); map.put(KEYBOARD_12KEY, "12key"); KEYBOARD_VALUES = Collections.unmodifiableMap(map); } static final int KEYBOARDHIDDEN_MASK = 0x03; static final int KEYBOARDHIDDEN_NO = 1; static final int KEYBOARDHIDDEN_YES = 2; static final int KEYBOARDHIDDEN_SOFT = 3; private static final Map<Integer, String> KEYBOARDHIDDEN_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(KEYBOARDHIDDEN_NO, "keysexposed"); map.put(KEYBOARDHIDDEN_YES, "keyshidden"); map.put(KEYBOARDHIDDEN_SOFT, "keyssoft"); KEYBOARDHIDDEN_VALUES = Collections.unmodifiableMap(map); } static final int NAVIGATION_NONAV = 1; static final int NAVIGATION_DPAD = 2; static final int NAVIGATION_TRACKBALL = 3; static final int NAVIGATION_WHEEL = 4; private static final Map<Integer, String> NAVIGATION_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(NAVIGATION_NONAV, "nonav"); map.put(NAVIGATION_DPAD, "dpad"); map.put(NAVIGATION_TRACKBALL, "trackball"); map.put(NAVIGATION_WHEEL, "wheel"); NAVIGATION_VALUES = Collections.unmodifiableMap(map); } static final int NAVIGATIONHIDDEN_MASK = 0x0C; static final int NAVIGATIONHIDDEN_NO = 0x04; static final int NAVIGATIONHIDDEN_YES = 0x08; private static final Map<Integer, String> NAVIGATIONHIDDEN_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(NAVIGATIONHIDDEN_NO, "navexposed"); map.put(NAVIGATIONHIDDEN_YES, "navhidden"); NAVIGATIONHIDDEN_VALUES = Collections.unmodifiableMap(map); } static final int ORIENTATION_PORTRAIT = 0x01; static final int ORIENTATION_LANDSCAPE = 0x02; private static final Map<Integer, String> ORIENTATION_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(ORIENTATION_PORTRAIT, "port"); map.put(ORIENTATION_LANDSCAPE, "land"); ORIENTATION_VALUES = Collections.unmodifiableMap(map); } static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0xC0; static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; private static final Map<Integer, String> SCREENLAYOUT_LAYOUTDIR_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(SCREENLAYOUT_LAYOUTDIR_LTR, "ldltr"); map.put(SCREENLAYOUT_LAYOUTDIR_RTL, "ldrtl"); SCREENLAYOUT_LAYOUTDIR_VALUES = Collections.unmodifiableMap(map); } static final int SCREENLAYOUT_LONG_MASK = 0x30; static final int SCREENLAYOUT_LONG_NO = 0x10; static final int SCREENLAYOUT_LONG_YES = 0x20; private static final Map<Integer, String> SCREENLAYOUT_LONG_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(SCREENLAYOUT_LONG_NO, "notlong"); map.put(SCREENLAYOUT_LONG_YES, "long"); SCREENLAYOUT_LONG_VALUES = Collections.unmodifiableMap(map); } static final int SCREENLAYOUT_ROUND_MASK = 0x03; static final int SCREENLAYOUT_ROUND_NO = 0x01; static final int SCREENLAYOUT_ROUND_YES = 0x02; private static final Map<Integer, String> SCREENLAYOUT_ROUND_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(SCREENLAYOUT_ROUND_NO, "notround"); map.put(SCREENLAYOUT_ROUND_YES, "round"); SCREENLAYOUT_ROUND_VALUES = Collections.unmodifiableMap(map); } static final int SCREENLAYOUT_SIZE_MASK = 0x0F; static final int SCREENLAYOUT_SIZE_SMALL = 0x01; static final int SCREENLAYOUT_SIZE_NORMAL = 0x02; static final int SCREENLAYOUT_SIZE_LARGE = 0x03; static final int SCREENLAYOUT_SIZE_XLARGE = 0x04; private static final Map<Integer, String> SCREENLAYOUT_SIZE_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(SCREENLAYOUT_SIZE_SMALL, "small"); map.put(SCREENLAYOUT_SIZE_NORMAL, "normal"); map.put(SCREENLAYOUT_SIZE_LARGE, "large"); map.put(SCREENLAYOUT_SIZE_XLARGE, "xlarge"); SCREENLAYOUT_SIZE_VALUES = Collections.unmodifiableMap(map); } static final int TOUCHSCREEN_NOTOUCH = 1; static final int TOUCHSCREEN_FINGER = 3; private static final Map<Integer, String> TOUCHSCREEN_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(TOUCHSCREEN_NOTOUCH, "notouch"); map.put(TOUCHSCREEN_FINGER, "finger"); TOUCHSCREEN_VALUES = Collections.unmodifiableMap(map); } static final int UI_MODE_NIGHT_MASK = 0x30; static final int UI_MODE_NIGHT_NO = 0x10; static final int UI_MODE_NIGHT_YES = 0x20; private static final Map<Integer, String> UI_MODE_NIGHT_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(UI_MODE_NIGHT_NO, "notnight"); map.put(UI_MODE_NIGHT_YES, "night"); UI_MODE_NIGHT_VALUES = Collections.unmodifiableMap(map); } static final int UI_MODE_TYPE_MASK = 0x0F; static final int UI_MODE_TYPE_DESK = 0x02; static final int UI_MODE_TYPE_CAR = 0x03; static final int UI_MODE_TYPE_TELEVISION = 0x04; static final int UI_MODE_TYPE_APPLIANCE = 0x05; static final int UI_MODE_TYPE_WATCH = 0x06; static final int UI_MODE_TYPE_VR_HEADSET = 0x07; private static final Map<Integer, String> UI_MODE_TYPE_VALUES; static { Map<Integer, String> map = new HashMap<>(); map.put(UI_MODE_TYPE_DESK, "desk"); map.put(UI_MODE_TYPE_CAR, "car"); map.put(UI_MODE_TYPE_TELEVISION, "television"); map.put(UI_MODE_TYPE_APPLIANCE, "appliance"); map.put(UI_MODE_TYPE_WATCH, "watch"); map.put(UI_MODE_TYPE_VR_HEADSET, "vrheadset"); UI_MODE_TYPE_VALUES = Collections.unmodifiableMap(map); } /** The minimum size in bytes that a {@link ResourceConfiguration} can be. */ private static final int MIN_SIZE = 28; /** The minimum size in bytes that this configuration must be to contain screen config info. */ private static final int SCREEN_CONFIG_MIN_SIZE = 32; /** The minimum size in bytes that this configuration must be to contain screen dp info. */ private static final int SCREEN_DP_MIN_SIZE = 36; /** The minimum size in bytes that this configuration must be to contain locale info. */ private static final int LOCALE_MIN_SIZE = 48; /** The minimum size in bytes that this config must be to contain the screenConfig extension. */ private static final int SCREEN_CONFIG_EXTENSION_MIN_SIZE = 52; /** The size of resource configurations in bytes for the latest version of Android resources. */ public static final int SIZE = SCREEN_CONFIG_EXTENSION_MIN_SIZE; /** The number of bytes that this resource configuration takes up. */ public abstract int size(); public abstract int mcc(); public abstract int mnc(); /** Returns a packed 2-byte language code. */ @SuppressWarnings("mutable") public abstract byte[] language(); /** Returns {@link #language} as an unpacked string representation. */ public final String languageString() { return unpackLanguage(); } /** Returns a packed 2-byte region code. */ @SuppressWarnings("mutable") public abstract byte[] region(); /** Returns {@link #region} as an unpacked string representation. */ public final String regionString() { return unpackRegion(); } public abstract int orientation(); public abstract int touchscreen(); public abstract int density(); public abstract int keyboard(); public abstract int navigation(); public abstract int inputFlags(); public final int keyboardHidden() { return inputFlags() & KEYBOARDHIDDEN_MASK; } public final int navigationHidden() { return inputFlags() & NAVIGATIONHIDDEN_MASK; } public abstract int screenWidth(); public abstract int screenHeight(); public abstract int sdkVersion(); /** * Returns a copy of this resource configuration with a different {@link #sdkVersion}, or this * configuration if the {@code sdkVersion} is the same. * * @param sdkVersion The SDK version of the returned configuration. * @return A copy of this configuration with the only difference being #sdkVersion. */ public final ResourceConfiguration withSdkVersion(int sdkVersion) { return toBuilder().sdkVersion(sdkVersion).build(); } public abstract int minorVersion(); public abstract int screenLayout(); public final int screenLayoutDirection() { return screenLayout() & SCREENLAYOUT_LAYOUTDIR_MASK; } public final int screenLayoutSize() { return screenLayout() & SCREENLAYOUT_SIZE_MASK; } public final int screenLayoutLong() { return screenLayout() & SCREENLAYOUT_LONG_MASK; } public final int screenLayoutRound() { return screenLayout2() & SCREENLAYOUT_ROUND_MASK; } public abstract int uiMode(); public final int uiModeType() { return uiMode() & UI_MODE_TYPE_MASK; } public final int uiModeNight() { return uiMode() & UI_MODE_NIGHT_MASK; } public abstract int smallestScreenWidthDp(); public abstract int screenWidthDp(); public abstract int screenHeightDp(); /** The ISO-15924 short name for the script corresponding to this configuration. */ @SuppressWarnings("mutable") public abstract byte[] localeScript(); /** Returns the {@link #localeScript} as a string. */ public final String localeScriptString() { return byteArrayToString(localeScript()); } /** A single BCP-47 variant subtag. */ @SuppressWarnings("mutable") public abstract byte[] localeVariant(); /** Returns the {@link #localeVariant} as a string. */ public final String localeVariantString() { return byteArrayToString(localeVariant()); } /** An extension to {@link #screenLayout}. Contains round/notround qualifier. */ public abstract int screenLayout2(); /** Wide-gamut, HDR, etc. */ public abstract int colorMode(); /** Returns the wide color gamut section of {@link #colorMode}. */ public final int colorModeWideColorGamut() { return colorMode() & COLOR_MODE_WIDE_COLOR_GAMUT_MASK; } /** Returns the HDR section of {@link #colorMode}. */ public final int colorModeHdr() { return colorMode() & COLOR_MODE_HDR_MASK; } /** Any remaining bytes in this resource configuration that are unaccounted for. */ @SuppressWarnings("mutable") public abstract byte[] unknown(); /** Returns this {@link ResourceConfiguration} as a builder. */ public abstract Builder toBuilder(); /** Returns a {@link Builder} with sane default properties. */ public static Builder builder() { return new AutoValue_ResourceConfiguration.Builder() .size(SIZE) .mcc(0) .mnc(0) .language(new byte[2]) .region(new byte[2]) .orientation(0) .touchscreen(0) .density(0) .keyboard(0) .navigation(0) .inputFlags(0) .screenWidth(0) .screenHeight(0) .sdkVersion(0) .minorVersion(0) .screenLayout(0) .uiMode(0) .smallestScreenWidthDp(0) .screenWidthDp(0) .screenHeightDp(0) .localeScript(new byte[4]) .localeVariant(new byte[8]) .screenLayout2(0) .colorMode(0) .unknown(new byte[0]); } static ResourceConfiguration create(ByteBuffer buffer) { int startPosition = buffer.position(); // The starting buffer position to calculate bytes read. int size = buffer.getInt(); Preconditions.checkArgument(size >= MIN_SIZE, "Expected minimum ResourceConfiguration size of %s, got %s", MIN_SIZE, size); // Builder order is important here. It's the same order as the data stored in the buffer. // The order of the builder's method calls, such as #mcc and #mnc, should not be changed. Builder configurationBuilder = builder() .size(size) .mcc(buffer.getShort() & 0xFFFF) .mnc(buffer.getShort() & 0xFFFF); byte[] language = new byte[2]; buffer.get(language); byte[] region = new byte[2]; buffer.get(region); configurationBuilder.language(language) .region(region) .orientation(UnsignedBytes.toInt(buffer.get())) .touchscreen(UnsignedBytes.toInt(buffer.get())) .density(buffer.getShort() & 0xFFFF) .keyboard(UnsignedBytes.toInt(buffer.get())) .navigation(UnsignedBytes.toInt(buffer.get())) .inputFlags(UnsignedBytes.toInt(buffer.get())); buffer.get(); // 1 byte of padding configurationBuilder.screenWidth(buffer.getShort() & 0xFFFF) .screenHeight(buffer.getShort() & 0xFFFF) .sdkVersion(buffer.getShort() & 0xFFFF) .minorVersion(buffer.getShort() & 0xFFFF); // At this point, the configuration's size needs to be taken into account as not all // configurations have all values. if (size >= SCREEN_CONFIG_MIN_SIZE) { configurationBuilder.screenLayout(UnsignedBytes.toInt(buffer.get())) .uiMode(UnsignedBytes.toInt(buffer.get())) .smallestScreenWidthDp(buffer.getShort() & 0xFFFF); } if (size >= SCREEN_DP_MIN_SIZE) { configurationBuilder.screenWidthDp(buffer.getShort() & 0xFFFF) .screenHeightDp(buffer.getShort() & 0xFFFF); } if (size >= LOCALE_MIN_SIZE) { byte[] localeScript = new byte[4]; buffer.get(localeScript); byte[] localeVariant = new byte[8]; buffer.get(localeVariant); configurationBuilder.localeScript(localeScript) .localeVariant(localeVariant); } if (size >= SCREEN_CONFIG_EXTENSION_MIN_SIZE) { configurationBuilder.screenLayout2(UnsignedBytes.toInt(buffer.get())); configurationBuilder.colorMode(UnsignedBytes.toInt(buffer.get())); buffer.getShort(); // More reserved padding } // After parsing everything that's known, account for anything that's unknown. int bytesRead = buffer.position() - startPosition; byte[] unknown = new byte[size - bytesRead]; buffer.get(unknown); configurationBuilder.unknown(unknown); return configurationBuilder.build(); } private String unpackLanguage() { return unpackLanguage(language()); } public static String unpackLanguage(byte[] language) { return unpackLanguageOrRegion(language, 0x61); } private String unpackRegion() { return unpackLanguageOrRegion(region(), 0x30); } private static String unpackLanguageOrRegion(byte[] value, int base) { Preconditions.checkState(value.length == 2, "Language or region value must be 2 bytes."); if (value[0] == 0 && value[1] == 0) { return ""; } if ((UnsignedBytes.toInt(value[0]) & 0x80) != 0) { byte[] result = new byte[3]; result[0] = (byte) (base + (value[1] & 0x1F)); result[1] = (byte) (base + ((value[1] & 0xE0) >>> 5) + ((value[0] & 0x03) << 3)); result[2] = (byte) (base + ((value[0] & 0x7C) >>> 2)); return new String(result, Charsets.US_ASCII); } return new String(value, Charsets.US_ASCII); } /** * Packs a 2 or 3 character language string into two bytes. If this is a 2 character string the * returned bytes is simply the string bytes, if this is a 3 character string we use a packed * format where the two bytes are: * * <pre> * +--+--+--+--+--+--+--+--+ +--+--+--+--+--+--+--+--+ * |B |2 |2 |2 |2 |2 |1 |1 | |1 |1 |1 |0 |0 |0 |0 |0 | * +--+--+--+--+--+--+--+--+ +--+--+--+--+--+--+--+--+ * </pre> * * <p>B : if bit set indicates this is a 3 character string (languages are always old style 7 bit * ascii chars only, so this is never set for a two character language) * * <p>2: The third character - 0x61 * * <p>1: The second character - 0x61 * * <p>0: The first character - 0x61 * * <p>Languages are always lower case chars, so max is within 5 bits (z = 11001) * * @param language The language to pack. * @return The two byte representation of the language */ public static byte[] packLanguage(String language) { byte[] unpacked = language.getBytes(Charsets.US_ASCII); if (unpacked.length == 2) { return unpacked; } int base = 0x61; byte[] result = new byte[2]; Preconditions.checkState(unpacked.length == 3); for (byte value : unpacked) { Preconditions.checkState(value >= 'a' && value <= 'z'); } result[0] = (byte) (((unpacked[2] - base) << 2) | ((unpacked[1] - base) >> 3) | 0x80); result[1] = (byte) ((unpacked[0] - base) | ((unpacked[1] - base) << 5)); return result; } private String byteArrayToString(byte[] data) { int length = Bytes.indexOf(data, (byte) 0); return new String(data, 0, length >= 0 ? length : data.length, Charsets.US_ASCII); } /** Returns true if this is the default "any" configuration. */ public final boolean isDefault() { // Ignore size and unknown when checking if this is the default configuration. It's possible // that we're comparing against a different version. return DEFAULT_BUILDER.size(size()).unknown(unknown()).build().equals(this) && Arrays.equals(unknown(), new byte[unknown().length]); } public final boolean isDensityCompatibleWith(int deviceDensityDpi) { int configDensity = density(); switch (configDensity) { case DENSITY_DPI_UNDEFINED: case DENSITY_DPI_ANY: case DENSITY_DPI_NONE: return true; default: return configDensity <= deviceDensityDpi; } } @Override public final byte[] toByteArray() { return toByteArray(SerializableResource.NONE); } @Override public final byte[] toByteArray(int options) { ByteBuffer buffer = ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(size()); buffer.putShort((short) mcc()); buffer.putShort((short) mnc()); buffer.put(language()); buffer.put(region()); buffer.put((byte) orientation()); buffer.put((byte) touchscreen()); buffer.putShort((short) density()); buffer.put((byte) keyboard()); buffer.put((byte) navigation()); buffer.put((byte) inputFlags()); buffer.put((byte) 0); // Padding buffer.putShort((short) screenWidth()); buffer.putShort((short) screenHeight()); buffer.putShort((short) sdkVersion()); buffer.putShort((short) minorVersion()); if (size() >= SCREEN_CONFIG_MIN_SIZE) { buffer.put((byte) screenLayout()); buffer.put((byte) uiMode()); buffer.putShort((short) smallestScreenWidthDp()); } if (size() >= SCREEN_DP_MIN_SIZE) { buffer.putShort((short) screenWidthDp()); buffer.putShort((short) screenHeightDp()); } if (size() >= LOCALE_MIN_SIZE) { buffer.put(localeScript()); buffer.put(localeVariant()); } if (size() >= SCREEN_CONFIG_EXTENSION_MIN_SIZE) { buffer.put((byte) screenLayout2()); buffer.put((byte) colorMode()); buffer.putShort((short) 0); // Writing 2 bytes of padding } buffer.put(unknown()); return buffer.array(); } @Override public final String toString() { if (isDefault()) { // Prevent the default configuration from returning the empty string return "default"; } Map<Type, String> parts = toStringParts(); mergeLocale(parts); Collection<String> values = parts.values(); values.removeAll(Collections.singleton("")); return Joiner.on('-').join(values); } /** * Merges the locale for {@code parts} if necessary. * * <p>Android supports a modified BCP 47 tag containing script and variant. If script or variant * are provided in the configuration, then the locale section should appear as: * * <p>{@code b+language+script+region+variant} */ private void mergeLocale(Map<Type, String> parts) { String script = localeScriptString(); String variant = localeVariantString(); if (script.isEmpty() && variant.isEmpty()) { return; } StringBuilder locale = new StringBuilder("b+").append(languageString()); if (!script.isEmpty()) { locale.append("+" + script); } String region = regionString(); if (!region.isEmpty()) { locale.append("+" + region); } if (!variant.isEmpty()) { locale.append("+" + variant); } parts.put(Type.LANGUAGE_STRING, locale.toString()); parts.remove(Type.LOCALE_SCRIPT_STRING); parts.remove(Type.REGION_STRING); parts.remove(Type.LOCALE_VARIANT_STRING); } /** * Returns a map of the configuration parts for {@link #toString}. * * <p>If a configuration part is not defined for this {@link ResourceConfiguration}, its value * will be the empty string. */ public final Map<Type, String> toStringParts() { Map<Type, String> result = new LinkedHashMap<>(); // Preserve order for #toString(). result.put(Type.MCC, mcc() != 0 ? "mcc" + mcc() : ""); result.put(Type.MNC, mnc() != 0 ? "mnc" + mnc() : ""); result.put(Type.LANGUAGE_STRING, languageString()); result.put(Type.LOCALE_SCRIPT_STRING, localeScriptString()); result.put(Type.REGION_STRING, !regionString().isEmpty() ? "r" + regionString() : ""); result.put(Type.LOCALE_VARIANT_STRING, localeVariantString()); result.put(Type.SCREEN_LAYOUT_DIRECTION, getOrDefault(SCREENLAYOUT_LAYOUTDIR_VALUES, screenLayoutDirection(), "")); result.put(Type.SMALLEST_SCREEN_WIDTH_DP, smallestScreenWidthDp() != 0 ? "sw" + smallestScreenWidthDp() + "dp" : ""); result.put(Type.SCREEN_WIDTH_DP, screenWidthDp() != 0 ? "w" + screenWidthDp() + "dp" : ""); result.put(Type.SCREEN_HEIGHT_DP, screenHeightDp() != 0 ? "h" + screenHeightDp() + "dp" : ""); result.put(Type.SCREEN_LAYOUT_SIZE, getOrDefault(SCREENLAYOUT_SIZE_VALUES, screenLayoutSize(), "")); result.put(Type.SCREEN_LAYOUT_LONG, getOrDefault(SCREENLAYOUT_LONG_VALUES, screenLayoutLong(), "")); result.put(Type.SCREEN_LAYOUT_ROUND, getOrDefault(SCREENLAYOUT_ROUND_VALUES, screenLayoutRound(), "")); result.put(Type.COLOR_MODE_HDR, getOrDefault(COLOR_MODE_HDR_VALUES, colorModeHdr(), "")); result.put( Type.COLOR_MODE_WIDE_COLOR_GAMUT, getOrDefault(COLOR_MODE_WIDE_COLOR_GAMUT_VALUES, colorModeWideColorGamut(), "")); result.put(Type.ORIENTATION, getOrDefault(ORIENTATION_VALUES, orientation(), "")); result.put(Type.UI_MODE_TYPE, getOrDefault(UI_MODE_TYPE_VALUES, uiModeType(), "")); result.put(Type.UI_MODE_NIGHT, getOrDefault(UI_MODE_NIGHT_VALUES, uiModeNight(), "")); result.put(Type.DENSITY_DPI, getOrDefault(DENSITY_DPI_VALUES, density(), density() + "dpi")); result.put(Type.TOUCHSCREEN, getOrDefault(TOUCHSCREEN_VALUES, touchscreen(), "")); result.put(Type.KEYBOARD_HIDDEN, getOrDefault(KEYBOARDHIDDEN_VALUES, keyboardHidden(), "")); result.put(Type.KEYBOARD, getOrDefault(KEYBOARD_VALUES, keyboard(), "")); result.put(Type.NAVIGATION_HIDDEN, getOrDefault(NAVIGATIONHIDDEN_VALUES, navigationHidden(), "")); result.put(Type.NAVIGATION, getOrDefault(NAVIGATION_VALUES, navigation(), "")); result.put(Type.SCREEN_SIZE, screenWidth() != 0 || screenHeight() != 0 ? screenWidth() + "x" + screenHeight() : ""); String sdkVersion = ""; if (sdkVersion() != 0) { sdkVersion = "v" + sdkVersion(); if (minorVersion() != 0) { sdkVersion += "." + minorVersion(); } } result.put(Type.SDK_VERSION, sdkVersion); return result; } private <K, V> V getOrDefault(Map<K, V> map, K key, V defaultValue) { // TODO(acornwall): Remove this when Java 8's Map#getOrDefault is available. // Null is not returned, even if the map contains a key whose value is null. This is intended. V value = map.get(key); return value != null ? value : defaultValue; } /** Provides a builder for creating {@link ResourceConfiguration} instances. */ @AutoValue.Builder public abstract static class Builder { public abstract Builder size(int size); public abstract Builder mcc(int mcc); public abstract Builder mnc(int mnc); public abstract Builder language(byte[] language); public abstract Builder region(byte[] region); public abstract Builder orientation(int orientation); public abstract Builder touchscreen(int touchscreen); public abstract Builder density(int density); public abstract Builder keyboard(int keyboard); public abstract Builder navigation(int navigation); public abstract Builder inputFlags(int inputFlags); public abstract Builder screenWidth(int screenWidth); public abstract Builder screenHeight(int screenHeight); public abstract Builder sdkVersion(int sdkVersion); public abstract Builder minorVersion(int minorVersion); public abstract Builder screenLayout(int screenLayout); public abstract Builder uiMode(int uiMode); public abstract Builder smallestScreenWidthDp(int smallestScreenWidthDp); public abstract Builder screenWidthDp(int screenWidthDp); public abstract Builder screenHeightDp(int screenHeightDp); public abstract Builder localeScript(byte[] localeScript); public abstract Builder localeVariant(byte[] localeVariant); public abstract Builder screenLayout2(int screenLayout2); public abstract Builder colorMode(int colorMode); abstract Builder unknown(byte[] unknown); public abstract ResourceConfiguration build(); } }