
pink.madis.apk.arsc.ResourceConfiguration Maven / Gradle / Ivy
/*
* 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 pink.madis.apk.arsc;
import static java.nio.charset.StandardCharsets.US_ASCII;
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;
/** 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}. */
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
}
/** 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 DENSITY_DPI_VALUES =
ImmutableMap.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 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 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 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 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 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 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 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 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 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 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 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 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;
/** 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();
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);
}
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 parts = toStringParts().values();
parts.removeAll(Collections.singleton(""));
return Joiner.on('-').join(parts);
}
/**
* Returns a map of the configuration parts for {@link #toString}.
*
* If a configuration part is not defined for this {@link ResourceConfiguration}, its value
* will be the empty string.
*/
public final Map toStringParts() {
Map 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 V getOrDefault(Map 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;
}
}