/* * 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 org.cf.apkfile.res; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; 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.LinkedHashMap; import java.util.Map; import static java.nio.charset.StandardCharsets.US_ASCII; /** * Describes a particular resource configuration. */ @AutoValue public abstract class ResourceConfiguration implements SerializableResource { /** * The below constants are from android.content.res.Configuration. */ private static final int DENSITY_DPI_UNDEFINED = 0; private static final int DENSITY_DPI_LDPI = 120; private static final int DENSITY_DPI_MDPI = 160; private static final int DENSITY_DPI_TVDPI = 213; private static final int DENSITY_DPI_HDPI = 240; private static final int DENSITY_DPI_XHDPI = 320; private static final int DENSITY_DPI_XXHDPI = 480; private static final int DENSITY_DPI_XXXHDPI = 640; private static final int DENSITY_DPI_ANY = 0xFFFE; private static final int DENSITY_DPI_NONE = 0xFFFF; private static final Map<Integer, String> DENSITY_DPI_VALUES = ImmutableMap.<Integer, String>builder().put(DENSITY_DPI_UNDEFINED, "") .put(DENSITY_DPI_LDPI, "ldpi").put(DENSITY_DPI_MDPI, "mdpi").put(DENSITY_DPI_TVDPI, "tvdpi").put(DENSITY_DPI_HDPI, "hdpi") .put(DENSITY_DPI_XHDPI, "xhdpi").put(DENSITY_DPI_XXHDPI, "xxhdpi").put(DENSITY_DPI_XXXHDPI, "xxxhdpi").put(DENSITY_DPI_ANY, "anydpi") .put(DENSITY_DPI_NONE, "nodpi").build(); private static final int KEYBOARD_NOKEYS = 1; private static final int KEYBOARD_QWERTY = 2; private static final int KEYBOARD_12KEY = 3; private static final Map<Integer, String> KEYBOARD_VALUES = ImmutableMap .of(KEYBOARD_NOKEYS, "nokeys", KEYBOARD_QWERTY, "qwerty", KEYBOARD_12KEY, "12key"); private static final int KEYBOARDHIDDEN_MASK = 0x03; private static final int KEYBOARDHIDDEN_NO = 1; private static final int KEYBOARDHIDDEN_YES = 2; private static final int KEYBOARDHIDDEN_SOFT = 3; private static final Map<Integer, String> KEYBOARDHIDDEN_VALUES = ImmutableMap .of(KEYBOARDHIDDEN_NO, "keysexposed", KEYBOARDHIDDEN_YES, "keyshidden", KEYBOARDHIDDEN_SOFT, "keyssoft"); private static final int NAVIGATION_NONAV = 1; private static final int NAVIGATION_DPAD = 2; private static final int NAVIGATION_TRACKBALL = 3; private static final int NAVIGATION_WHEEL = 4; private static final Map<Integer, String> NAVIGATION_VALUES = ImmutableMap .of(NAVIGATION_NONAV, "nonav", NAVIGATION_DPAD, "dpad", NAVIGATION_TRACKBALL, "trackball", NAVIGATION_WHEEL, "wheel"); private static final int NAVIGATIONHIDDEN_MASK = 0x0C; private static final int NAVIGATIONHIDDEN_NO = 0x04; private static final int NAVIGATIONHIDDEN_YES = 0x08; private static final Map<Integer, String> NAVIGATIONHIDDEN_VALUES = ImmutableMap .of(NAVIGATIONHIDDEN_NO, "navexposed", NAVIGATIONHIDDEN_YES, "navhidden"); private static final int ORIENTATION_PORTRAIT = 0x01; private static final int ORIENTATION_LANDSCAPE = 0x02; private static final Map<Integer, String> ORIENTATION_VALUES = ImmutableMap.of(ORIENTATION_PORTRAIT, "port", ORIENTATION_LANDSCAPE, "land"); private static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0xC0; private static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; private static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; private static final Map<Integer, String> SCREENLAYOUT_LAYOUTDIR_VALUES = ImmutableMap .of(SCREENLAYOUT_LAYOUTDIR_LTR, "ldltr", SCREENLAYOUT_LAYOUTDIR_RTL, "ldrtl"); private static final int SCREENLAYOUT_LONG_MASK = 0x30; private static final int SCREENLAYOUT_LONG_NO = 0x10; private static final int SCREENLAYOUT_LONG_YES = 0x20; private static final Map<Integer, String> SCREENLAYOUT_LONG_VALUES = ImmutableMap .of(SCREENLAYOUT_LONG_NO, "notlong", SCREENLAYOUT_LONG_YES, "long"); private static final int SCREENLAYOUT_ROUND_MASK = 0x0300; private static final int SCREENLAYOUT_ROUND_NO = 0x0100; private static final int SCREENLAYOUT_ROUND_YES = 0x0200; private static final Map<Integer, String> SCREENLAYOUT_ROUND_VALUES = ImmutableMap .of(SCREENLAYOUT_ROUND_NO, "notround", SCREENLAYOUT_ROUND_YES, "round"); private static final int SCREENLAYOUT_SIZE_MASK = 0x0F; private static final int SCREENLAYOUT_SIZE_SMALL = 0x01; private static final int SCREENLAYOUT_SIZE_NORMAL = 0x02; private static final int SCREENLAYOUT_SIZE_LARGE = 0x03; private static final int SCREENLAYOUT_SIZE_XLARGE = 0x04; private static final Map<Integer, String> SCREENLAYOUT_SIZE_VALUES = ImmutableMap .of(SCREENLAYOUT_SIZE_SMALL, "small", SCREENLAYOUT_SIZE_NORMAL, "normal", SCREENLAYOUT_SIZE_LARGE, "large", SCREENLAYOUT_SIZE_XLARGE, "xlarge"); private static final int TOUCHSCREEN_NOTOUCH = 1; private static final int TOUCHSCREEN_FINGER = 3; private static final Map<Integer, String> TOUCHSCREEN_VALUES = ImmutableMap.of(TOUCHSCREEN_NOTOUCH, "notouch", TOUCHSCREEN_FINGER, "finger"); private static final int UI_MODE_NIGHT_MASK = 0x30; private static final int UI_MODE_NIGHT_NO = 0x10; private static final int UI_MODE_NIGHT_YES = 0x20; private static final Map<Integer, String> UI_MODE_NIGHT_VALUES = ImmutableMap.of(UI_MODE_NIGHT_NO, "notnight", UI_MODE_NIGHT_YES, "night"); private static final int UI_MODE_TYPE_MASK = 0x0F; private static final int UI_MODE_TYPE_DESK = 0x02; private static final int UI_MODE_TYPE_CAR = 0x03; private static final int UI_MODE_TYPE_TELEVISION = 0x04; private static final int UI_MODE_TYPE_APPLIANCE = 0x05; private static final int UI_MODE_TYPE_WATCH = 0x06; private static final Map<Integer, String> UI_MODE_TYPE_VALUES = ImmutableMap .of(UI_MODE_TYPE_DESK, "desk", UI_MODE_TYPE_CAR, "car", UI_MODE_TYPE_TELEVISION, "television", UI_MODE_TYPE_APPLIANCE, "appliance", UI_MODE_TYPE_WATCH, "watch"); /** * 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; static ResourceConfiguration create(ByteBuffer buffer) { int startPosition = buffer.position(); // The starting buffer position to calculate bytes read. int size = buffer.getInt(); int mcc = buffer.getShort() & 0xFFFF; int mnc = buffer.getShort() & 0xFFFF; byte[] language = new byte[2]; buffer.get(language); byte[] region = new byte[2]; buffer.get(region); int orientation = UnsignedBytes.toInt(buffer.get()); int touchscreen = UnsignedBytes.toInt(buffer.get()); int density = buffer.getShort() & 0xFFFF; int keyboard = UnsignedBytes.toInt(buffer.get()); int navigation = UnsignedBytes.toInt(buffer.get()); int inputFlags = UnsignedBytes.toInt(buffer.get()); buffer.get(); // 1 byte of padding int screenWidth = buffer.getShort() & 0xFFFF; int screenHeight = buffer.getShort() & 0xFFFF; int sdkVersion = buffer.getShort() & 0xFFFF; int minorVersion = buffer.getShort() & 0xFFFF; // At this point, the configuration's size needs to be taken into account as not all // configurations have all values. int screenLayout = 0; int uiMode = 0; int smallestScreenWidthDp = 0; int screenWidthDp = 0; int screenHeightDp = 0; byte[] localeScript = new byte[4]; byte[] localeVariant = new byte[8]; int screenLayout2 = 0; if (size >= SCREEN_CONFIG_MIN_SIZE) { screenLayout = UnsignedBytes.toInt(buffer.get()); uiMode = UnsignedBytes.toInt(buffer.get()); smallestScreenWidthDp = buffer.getShort() & 0xFFFF; } if (size >= SCREEN_DP_MIN_SIZE) { screenWidthDp = buffer.getShort() & 0xFFFF; screenHeightDp = buffer.getShort() & 0xFFFF; } if (size >= LOCALE_MIN_SIZE) { buffer.get(localeScript); buffer.get(localeVariant); } if (size >= SCREEN_CONFIG_EXTENSION_MIN_SIZE) { screenLayout2 = UnsignedBytes.toInt(buffer.get()); buffer.get(); // Reserved padding 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); return new AutoValue_ResourceConfiguration(size, mcc, mnc, language, region, orientation, touchscreen, density, keyboard, navigation, inputFlags, screenWidth, screenHeight, sdkVersion, minorVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, screenHeightDp, localeScript, localeVariant, screenLayout2, unknown); } /** * 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) { if (sdkVersion == sdkVersion()) { return this; } return new AutoValue_ResourceConfiguration(size(), mcc(), mnc(), language(), region(), orientation(), touchscreen(), density(), keyboard(), navigation(), inputFlags(), screenWidth(), screenHeight(), sdkVersion, minorVersion(), screenLayout(), uiMode(), smallestScreenWidthDp(), screenWidthDp(), screenHeightDp(), localeScript(), localeVariant(), screenLayout2(), unknown()); } 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 screenLayout() & 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(); /** * A single BCP-47 variant subtag. */ @SuppressWarnings("mutable") public abstract byte[] localeVariant(); /** * An extension to {@link #screenLayout}. Contains round/notround qualifier. */ public abstract int screenLayout2(); /** * Any remaining bytes in this resource configuration that are unaccounted for. */ @SuppressWarnings("mutable") public abstract byte[] unknown(); private String unpackLanguage() { return unpackLanguageOrRegion(language(), 0x61); } private String unpackRegion() { return unpackLanguageOrRegion(region(), 0x30); } private 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, US_ASCII); } return new String(value, US_ASCII); } /** * Returns true if this is the default "any" configuration. */ public final boolean isDefault() { return mcc() == 0 && mnc() == 0 && Arrays.equals(language(), new byte[2]) && Arrays.equals(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 && Arrays .equals(localeScript(), new byte[4]) && Arrays.equals(localeVariant(), new byte[8]) && screenLayout2() == 0; } @Override public final byte[] toByteArray() { return toByteArray(false); } @Override public final byte[] toByteArray(boolean shrink) { 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.putInt(screenLayout2()); // Writing an unsigned byte + 3 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"; } Collection<String> parts = toStringParts().values(); parts.removeAll(Collections.singleton("")); return Joiner.on('-').join(parts); } /** * 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().isEmpty() ? "" + languageString() : ""); result.put(Type.REGION_STRING, !regionString().isEmpty() ? "r" + regionString() : ""); 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.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.SDK_VERSION, sdkVersion() != 0 ? "v" + 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; } /** * The different types of configs that can be present in a {@link ResourceConfiguration}. */ public enum Type { MCC, MNC, LANGUAGE_STRING, REGION_STRING, SCREEN_LAYOUT_DIRECTION, SMALLEST_SCREEN_WIDTH_DP, SCREEN_WIDTH_DP, SCREEN_HEIGHT_DP, SCREEN_LAYOUT_SIZE, SCREEN_LAYOUT_LONG, SCREEN_LAYOUT_ROUND, ORIENTATION, UI_MODE_TYPE, UI_MODE_NIGHT, DENSITY_DPI, TOUCHSCREEN, KEYBOARD_HIDDEN, KEYBOARD, NAVIGATION_HIDDEN, NAVIGATION, SDK_VERSION } }