Merge branch 'main' into feat/app_pin

This commit is contained in:
aliveoutside
2023-02-20 21:10:07 +03:00
committed by GitHub
97 changed files with 6727 additions and 2098 deletions
+1
View File
@@ -107,6 +107,7 @@ dependencies {
implementation 'net.sourceforge.streamsupport:streamsupport-minifuture:1.7.4'
implementation fileTree(dir: 'libs', include: ['*.aar'])
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.guava:guava:23.3-android'
}
task prepareDexForMerge(type: Copy) {
@@ -56,22 +56,22 @@ public class LibVKXClient {
}
public static void lambdaplay(List<MusicTrack> list, MusicTrack musicTrack, ILibVkxService iLibVkxService, MusicPlaybackLaunchContext playerRefer) {
var playbackContext = "";
if (playerRefer != null) {
playbackContext = playerRefer.v0();
}
int indexOf = list.indexOf(musicTrack);
var ids = new ArrayList<String>();
for (var track : list) {
ids.add(asId(track));
}
if (indexOf == -1) {
indexOf = 0;
}
try {
var playbackContext = "";
if (playerRefer != null) {
playbackContext = playerRefer.v0();
}
int indexOf = list.indexOf(musicTrack);
var ids = new ArrayList<String>();
for (var track : list) {
ids.add(asId(track));
}
if (indexOf == -1) {
indexOf = 0;
}
iLibVkxService.play(ids, indexOf, playbackContext);
} catch (RemoteException e) {
e.printStackTrace();
@@ -0,0 +1,277 @@
/*
* 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.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import java.util.*;
import java.util.Map.Entry;
/**
* Analyzes an APK to:
*
* <ul>
* <li>Blame resource configurations on their entry count (entries keeping the config around).
* <li>Blame strings in resources.arsc that have no base configuration.
* <li>Blame resources on their different configurations.
* </ul>
*/
public class ArscBlamer {
/** Maps package key pool indices to blamed resources. */
private final Map<PackageChunk, List<ResourceEntry>[]> keyToBlame = new HashMap<>();
/** Maps types to blamed resources. */
private final Map<PackageChunk, List<ResourceEntry>[]> typeToBlame = new HashMap<>();
/** Maps package to blamed resources. */
private final Multimap<PackageChunk, ResourceEntry> packageToBlame = HashMultimap.create();
/** Maps string indices to blamed resources. */
private final List<ResourceEntry>[] stringToBlame;
/** Maps type chunk entries to blamed resources. */
private final Multimap<TypeChunk.Entry, ResourceEntry> typeEntryToBlame = HashMultimap.create();
/** Maps resources to the type chunk entries they reference. */
private Multimap<ResourceEntry, TypeChunk.Entry> resourceEntries;
/** Maps resources which have no base config to the type chunk entries they reference. */
private Multimap<ResourceEntry, TypeChunk.Entry> baselessKeys;
/** Contains all of the type chunks in {@link #resourceTable}. */
private List<TypeChunk> typeChunks;
/** This is the {@link ResourceTableChunk} inside of the resources.arsc file in the APK. */
private final ResourceTableChunk resourceTable;
/**
* Creates a new {@link ArscBlamer}.
*
* @param resourceTable The resources.arsc resource table to blame.
*/
public ArscBlamer(ResourceTableChunk resourceTable) {
this.resourceTable = resourceTable;
this.stringToBlame = createEntryListArray(resourceTable.getStringPool().getStringCount());
}
/** Generates blame mappings. */
public void blame() {
Multimap<ResourceEntry, TypeChunk.Entry> entries = getResourceEntries();
for (Entry<ResourceEntry, Collection<TypeChunk.Entry>> entry : entries.asMap().entrySet()) {
ResourceEntry resourceEntry = entry.getKey();
PackageChunk packageChunk = Preconditions.checkNotNull(
resourceTable.getPackage(resourceEntry.packageName()));
int keyCount = packageChunk.getKeyStringPool().getStringCount();
int typeCount = packageChunk.getTypeStringPool().getStringCount();
for (TypeChunk.Entry chunkEntry : entry.getValue()) {
blameKeyOrType(keyToBlame, packageChunk, chunkEntry.keyIndex(), resourceEntry, keyCount);
blameKeyOrType(typeToBlame, packageChunk, chunkEntry.parent().getId() - 1, resourceEntry,
typeCount);
blameFromTypeChunkEntry(chunkEntry);
}
blamePackage(packageChunk, resourceEntry);
}
Multimaps.invertFrom(entries, typeEntryToBlame);
for (TypeChunk.Entry entry : typeEntryToBlame.keySet()) {
blameFromTypeChunkEntry(entry);
}
}
private void blameKeyOrType(Map<PackageChunk, List<ResourceEntry>[]> keyOrType,
PackageChunk packageChunk, int keyIndex, ResourceEntry entry, int entryCount) {
if (!keyOrType.containsKey(packageChunk)) {
keyOrType.put(packageChunk, createEntryListArray(entryCount));
}
keyOrType.get(packageChunk)[keyIndex].add(entry);
}
private void blamePackage(PackageChunk packageChunk, ResourceEntry entry) {
packageToBlame.put(packageChunk, entry);
}
private void blameFromTypeChunkEntry(TypeChunk.Entry chunkEntry) {
for (BinaryResourceValue value : getAllResourceValues(chunkEntry)) {
for (ResourceEntry entry : typeEntryToBlame.get(chunkEntry)) {
switch (value.type()) {
case STRING:
blameString(value.data(), entry);
break;
default:
break;
}
}
}
}
/** Returns all {@link BinaryResourceValue} for a single {@code entry}. */
private Collection<BinaryResourceValue> getAllResourceValues(TypeChunk.Entry entry) {
Set<BinaryResourceValue> values = new HashSet<BinaryResourceValue>();
BinaryResourceValue binaryResourceValue = entry.value();
if (binaryResourceValue != null) {
values.add(binaryResourceValue);
}
for (BinaryResourceValue value : entry.values().values()) {
values.add(value);
}
return values;
}
private void blameString(int stringIndex, ResourceEntry entry) {
stringToBlame[stringIndex].add(entry);
}
/** Must first call {@link #blame}. */
public Map<PackageChunk, List<ResourceEntry>[]> getKeyToBlamedResources() {
return Collections.unmodifiableMap(keyToBlame);
}
/** Must first call {@link #blame}. */
public Map<PackageChunk, List<ResourceEntry>[]> getTypeToBlamedResources() {
return Collections.unmodifiableMap(typeToBlame);
}
/** Must first call {@link #blame}. */
public Multimap<PackageChunk, ResourceEntry> getPackageToBlamedResources() {
return Multimaps.unmodifiableMultimap(packageToBlame);
}
/** Must first call {@link #blame}. */
public List<ResourceEntry>[] getStringToBlamedResources() {
return stringToBlame;
}
/** Must first call {@link #blame}. */
public Multimap<TypeChunk.Entry, ResourceEntry> getTypeEntryToBlamedResources() {
return Multimaps.unmodifiableMultimap(typeEntryToBlame);
}
/** Returns a multimap of keys for which there is no default resource. */
public Multimap<ResourceEntry, TypeChunk.Entry> getBaselessKeys() {
if (baselessKeys != null) {
return baselessKeys;
}
Multimap<ResourceEntry, TypeChunk.Entry> result = HashMultimap.create();
for (Entry<ResourceEntry, Collection<TypeChunk.Entry>> entry
: getResourceEntries().asMap().entrySet()) {
Collection<TypeChunk.Entry> chunkEntries = entry.getValue();
if (!hasBaseConfiguration(chunkEntries)) {
result.putAll(entry.getKey(), chunkEntries);
}
}
baselessKeys = result;
return result;
}
/** Returns a multimap of resource entries to the chunk entries they reference in this APK. */
public Multimap<ResourceEntry, TypeChunk.Entry> getResourceEntries() {
if (resourceEntries != null) {
return resourceEntries;
}
Multimap<ResourceEntry, TypeChunk.Entry> result = HashMultimap.create();
for (TypeChunk typeChunk : getTypeChunks()) {
for (TypeChunk.Entry entry : typeChunk.getEntries().values()) {
result.put(ResourceEntry.create(entry), entry);
}
}
resourceEntries = result;
return result;
}
/** Returns all {@link TypeChunk} in resources.arsc. */
public List<TypeChunk> getTypeChunks() {
if (typeChunks != null) {
return typeChunks;
}
List<TypeChunk> result = new ArrayList<>();
for (PackageChunk packageChunk : resourceTable.getPackages()) {
for (TypeChunk typeChunk : packageChunk.getTypeChunks()) {
result.add(typeChunk);
}
}
typeChunks = result;
return result;
}
private boolean hasBaseConfiguration(Collection<TypeChunk.Entry> entries) {
for (TypeChunk.Entry entry : entries) {
if (entry.parent().getConfiguration().isDefault()) {
return true;
}
}
return false;
}
private static List<ResourceEntry>[] createEntryListArray(int size) {
ArrayListResourceEntry[] result = new ArrayListResourceEntry[size];
for (int i = 0; i < size; ++i) {
result[i] = new ArrayListResourceEntry();
}
return result;
}
/** Allows creation of concrete parameterized type arr {@link ArscBlamer#createEntryListArray}. */
private static class ArrayListResourceEntry extends ArrayList<ResourceEntry> {
private ArrayListResourceEntry() {
super(2); // ~90-95% of these lists end up with only 1 or 2 elements.
}
}
/** Describes a single resource entry. */
public static class ResourceEntry {
private final String packageName;
private final String typeName;
private final String entryName;
static ResourceEntry create(TypeChunk.Entry entry) {
PackageChunk packageChunk = Preconditions.checkNotNull(entry.parent().getPackageChunk());
String packageName = packageChunk.getPackageName();
String typeName = entry.typeName();
String entryName = entry.key();
return new ResourceEntry(packageName, typeName, entryName);
}
private ResourceEntry(String packageName, String typeName, String entryName) {
this.packageName = packageName;
this.typeName = typeName;
this.entryName = entryName;
}
public String packageName() { return packageName; }
public String typeName() { return typeName; }
public String entryName() { return entryName; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ResourceEntry that = (ResourceEntry)o;
return Objects.equals(packageName, that.packageName) &&
Objects.equals(typeName, that.typeName) &&
Objects.equals(entryName, that.entryName);
}
@Override
public int hashCode() {
return Objects.hash(packageName, typeName, entryName);
}
}
}
@@ -0,0 +1,745 @@
/*
* 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.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.*;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* Describes a particular resource configuration.
*/
public class BinaryResourceConfiguration implements SerializableResource {
private final int size;
private final int mcc;
private final int mnc;
private final byte[] language;
private final byte[] region;
private final int orientation;
private final int touchscreen;
private final int density;
private final int keyboard;
private final int navigation;
private final int inputFlags;
private final int screenWidth;
private final int screenHeight;
private final int sdkVersion;
private final int minorVersion;
private final int screenLayout;
private final int uiMode;
private final int smallestScreenWidthDp;
private final int screenWidthDp;
private final int screenHeightDp;
private final byte[] localeScript;
private final byte[] localeVariant;
private final int screenLayout2;
private final byte[] unknown;
static BinaryResourceConfiguration 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 BinaryResourceConfiguration(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 BinaryResourceConfiguration(int size,
int mcc,
int mnc,
byte[] language,
byte[] region,
int orientation,
int touchscreen,
int density,
int keyboard,
int navigation,
int inputFlags,
int screenWidth,
int screenHeight,
int sdkVersion,
int minorVersion,
int screenLayout,
int uiMode,
int smallestScreenWidthDp,
int screenWidthDp,
int screenHeightDp,
byte[] localeScript,
byte[] localeVariant,
int screenLayout2,
byte[] unknown) {
this.size = size;
this.mcc = mcc;
this.mnc = mnc;
this.language = language;
this.region = region;
this.orientation = orientation;
this.touchscreen = touchscreen;
this.density = density;
this.keyboard = keyboard;
this.navigation = navigation;
this.inputFlags = inputFlags;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
this.sdkVersion = sdkVersion;
this.minorVersion = minorVersion;
this.screenLayout = screenLayout;
this.uiMode = uiMode;
this.smallestScreenWidthDp = smallestScreenWidthDp;
this.screenWidthDp = screenWidthDp;
this.screenHeightDp = screenHeightDp;
this.localeScript = localeScript;
this.localeVariant = localeVariant;
this.screenLayout2 = screenLayout2;
this.unknown = unknown;
}
/**
* The different types of configs that can be present in a {@link BinaryResourceConfiguration}.
*/
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<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 = 0x03;
private static final int SCREENLAYOUT_ROUND_NO = 0x01;
private static final int SCREENLAYOUT_ROUND_YES = 0x02;
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 int UI_MODE_TYPE_VR_HEADSET = 0x07;
// start - miui
private static final int UI_MODE_TYPE_GODZILLAUI = 0x0b;
private static final int UI_MODE_TYPE_SMALLUI = 0x0c;
private static final int UI_MODE_TYPE_MEDIUMUI = 0x0d;
private static final int UI_MODE_TYPE_LARGEUI = 0x0e;
private static final int UI_MODE_TYPE_HUGEUI = 0x0f;
// end - miui
private static final Map<Integer, String> UI_MODE_TYPE_VALUES =
ImmutableMap.<Integer, String>builder()
.put(UI_MODE_TYPE_DESK, "desk")
.put(UI_MODE_TYPE_CAR, "car")
.put(UI_MODE_TYPE_TELEVISION, "television")
.put(UI_MODE_TYPE_APPLIANCE, "appliance")
.put(UI_MODE_TYPE_WATCH, "watch")
.put(UI_MODE_TYPE_VR_HEADSET, "vrheadset")
.put(UI_MODE_TYPE_GODZILLAUI, "godzillaui")
.put(UI_MODE_TYPE_SMALLUI, "smallui")
.put(UI_MODE_TYPE_MEDIUMUI, "mediumui")
.put(UI_MODE_TYPE_LARGEUI, "largeui")
.put(UI_MODE_TYPE_HUGEUI, "hugeui").build();
/**
* 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 int size() {
return size;
}
public int mcc() {
return mcc;
}
public int mnc() {
return mnc;
}
/**
* Returns a packed 2-byte language code.
*/
@SuppressWarnings("mutable")
public byte[] language() {
return 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 byte[] region() {
return region;
}
/**
* Returns {@link #region} as an unpacked string representation.
*/
public final String regionString() {
return unpackRegion();
}
public int orientation() {
return orientation;
}
public int touchscreen() {
return touchscreen;
}
public int density() {
return density;
}
public int keyboard() {
return keyboard;
}
public int navigation() {
return navigation;
}
public int inputFlags() {
return inputFlags;
}
public final int keyboardHidden() {
return inputFlags() & KEYBOARDHIDDEN_MASK;
}
public final int navigationHidden() {
return inputFlags() & NAVIGATIONHIDDEN_MASK;
}
public int screenWidth() {
return screenWidth;
}
public int screenHeight() {
return screenHeight;
}
public int sdkVersion() {
return 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 BinaryResourceConfiguration withSdkVersion(int sdkVersion) {
if (sdkVersion == sdkVersion()) {
return this;
}
return new BinaryResourceConfiguration(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 int minorVersion() {
return minorVersion;
}
public int screenLayout() {
return 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 int uiMode() {
return uiMode;
}
public final int uiModeType() {
return uiMode() & UI_MODE_TYPE_MASK;
}
public final int uiModeNight() {
return uiMode() & UI_MODE_NIGHT_MASK;
}
public int smallestScreenWidthDp() {
return smallestScreenWidthDp;
}
public int screenWidthDp() {
return screenWidthDp;
}
public int screenHeightDp() {
return screenHeightDp;
}
/**
* The ISO-15924 short name for the script corresponding to this configuration.
*/
@SuppressWarnings("mutable")
public byte[] localeScript() {
return localeScript;
}
/**
* A single BCP-47 variant subtag.
*/
@SuppressWarnings("mutable")
public byte[] localeVariant() {
return localeVariant;
}
/**
* An extension to {@link #screenLayout}. Contains round/notround qualifier.
*/
public int screenLayout2() {
return screenLayout2;
}
/**
* Any remaining bytes in this resource configuration that are unaccounted for.
*/
@SuppressWarnings("mutable")
public byte[] unknown() {
return 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 boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BinaryResourceConfiguration that = (BinaryResourceConfiguration) o;
return size == that.size &&
mcc == that.mcc &&
mnc == that.mnc &&
orientation == that.orientation &&
touchscreen == that.touchscreen &&
density == that.density &&
keyboard == that.keyboard &&
navigation == that.navigation &&
inputFlags == that.inputFlags &&
screenWidth == that.screenWidth &&
screenHeight == that.screenHeight &&
sdkVersion == that.sdkVersion &&
minorVersion == that.minorVersion &&
screenLayout == that.screenLayout &&
uiMode == that.uiMode &&
smallestScreenWidthDp == that.smallestScreenWidthDp &&
screenWidthDp == that.screenWidthDp &&
screenHeightDp == that.screenHeightDp &&
screenLayout2 == that.screenLayout2 &&
Arrays.equals(language, that.language) &&
Arrays.equals(region, that.region) &&
Arrays.equals(localeScript, that.localeScript) &&
Arrays.equals(localeVariant, that.localeVariant) &&
Arrays.equals(unknown, that.unknown);
}
@Override
public int hashCode() {
return Objects
.hash(size, mcc, mnc, Arrays.hashCode(language), Arrays.hashCode(region), orientation, touchscreen, density, keyboard, navigation, inputFlags, screenWidth,
screenHeight,
sdkVersion, minorVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, screenHeightDp, Arrays.hashCode(localeScript),
Arrays.hashCode(localeVariant),
screenLayout2, Arrays.hashCode(unknown));
}
@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 BinaryResourceConfiguration}, 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;
}
}
@@ -0,0 +1,74 @@
/*
* 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.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Given an arsc file, maps the contents of the file. */
public final class BinaryResourceFile implements SerializableResource {
/** The chunks contained in this resource file. */
private final List<Chunk> chunks = new ArrayList<>();
public BinaryResourceFile(byte[] buf) {
ByteBuffer buffer = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
while (buffer.remaining() > 0) {
chunks.add(Chunk.newInstance(buffer));
}
}
/**
* Given an input stream, reads the stream until the end and returns a {@link BinaryResourceFile}
* representing the contents of the stream.
*
* @param is The input stream to read from.
* @return BinaryResourceFile represented by the @{link InputStream}.
* @throws IOException
*/
public static BinaryResourceFile fromInputStream(InputStream is) throws IOException {
byte[] buf = ByteStreams.toByteArray(is);
return new BinaryResourceFile(buf);
}
/** Returns the chunks in this resource file. */
public List<Chunk> getChunks() {
return Collections.unmodifiableList(chunks);
}
@Override
public byte[] toByteArray() throws IOException {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) throws IOException {
ByteArrayDataOutput output = ByteStreams.newDataOutput();
for (Chunk chunk : chunks) {
output.write(chunk.toByteArray(shrink));
}
return output.toByteArray();
}
}
@@ -0,0 +1,99 @@
/*
* 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.common.base.Preconditions;
import java.util.Objects;
/**
* Resources in a {@link ResourceTableChunk} are identified by an integer of the form 0xpptteeee,
* where pp is the {@link PackageChunk} id, tt is the {@link TypeChunk} id, and eeee is the index of
* the entry in the {@link TypeChunk}.
*/
public class BinaryResourceIdentifier {
/** The {@link PackageChunk} id mask for a packed resource id of the form 0xpptteeee. */
private static final int PACKAGE_ID_MASK = 0xFF000000;
private static final int PACKAGE_ID_SHIFT = 24;
/** The {@link TypeChunk} id mask for a packed resource id of the form 0xpptteeee. */
private static final int TYPE_ID_MASK = 0x00FF0000;
private static final int TYPE_ID_SHIFT = 16;
/** The {@link TypeChunk.Entry} id mask for a packed resource id of the form 0xpptteeee. */
private static final int ENTRY_ID_MASK = 0xFFFF;
private static final int ENTRY_ID_SHIFT = 0;
private final int packageId;
private final int typeId;
private final int entryId;
/** Returns a {@link BinaryResourceIdentifier} from a {@code resourceId} of the form 0xpptteeee. */
public static BinaryResourceIdentifier create(int resourceId) {
int packageId = (resourceId & PACKAGE_ID_MASK) >>> PACKAGE_ID_SHIFT;
int typeId = (resourceId & TYPE_ID_MASK) >>> TYPE_ID_SHIFT;
int entryId = (resourceId & ENTRY_ID_MASK) >>> ENTRY_ID_SHIFT;
return create(packageId, typeId, entryId);
}
/** Returns a {@link BinaryResourceIdentifier} with the given identifiers. */
public static BinaryResourceIdentifier create(int packageId, int typeId, int entryId) {
Preconditions.checkState((packageId & 0xFF) == packageId, "packageId must be <= 0xFF.");
Preconditions.checkState((typeId & 0xFF) == typeId, "typeId must be <= 0xFF.");
Preconditions.checkState((entryId & 0xFFFF) == entryId, "entryId must be <= 0xFFFF.");
return new BinaryResourceIdentifier(packageId, typeId, entryId);
}
private BinaryResourceIdentifier(int packageId, int typeId, int entryId) {
this.packageId = packageId;
this.typeId = typeId;
this.entryId = entryId;
}
/** The (1-based) id of the {@link PackageChunk} containing this resource. */
public int packageId() { return packageId; }
/** The (1-based) id of the {@link TypeChunk} containing this resource. */
public int typeId() { return typeId; }
/** The (0-based) index of the entry in a {@link TypeChunk} containing this resource. */
public int entryId() { return entryId; }
public int resId() { return packageId << PACKAGE_ID_SHIFT | typeId << TYPE_ID_SHIFT | entryId; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BinaryResourceIdentifier that = (BinaryResourceIdentifier)o;
return packageId == that.packageId &&
typeId == that.typeId &&
entryId == that.entryId;
}
@Override
public int hashCode() {
return Objects.hash(packageId, typeId, entryId);
}
@Override
public String toString() {
int v = packageId << PACKAGE_ID_SHIFT | typeId << TYPE_ID_SHIFT | entryId;
return String.format("0x%1$08x", v);
}
}
@@ -0,0 +1,164 @@
/*
* 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.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.UnsignedBytes;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import static java.nio.charset.StandardCharsets.UTF_16LE;
import static java.nio.charset.StandardCharsets.UTF_8;
/** Provides utilities to decode/encode a String packed in an arsc resource file. */
public final class BinaryResourceString {
/** Type of {@link BinaryResourceString} to encode / decode. */
public enum Type {
UTF8(UTF_8),
UTF16(UTF_16LE);
private final Charset charset;
Type(Charset charset) {
this.charset = charset;
}
public Charset charset() {
return charset;
}
}
private BinaryResourceString() {} // Private constructor
/**
* Given a buffer and an offset into the buffer, returns a String. The {@code offset} is the
* 0-based byte offset from the start of the buffer where the string resides. This should be the
* location in memory where the string's character count, followed by its byte count, and then
* followed by the actual string is located.
*
* <p>Here's an example UTF-8-encoded string of ab©:
* <pre>
* 03 04 61 62 C2 A9 00
* ^ Offset should be here
* </pre>
*
* @param buffer The buffer containing the string to decode.
* @param offset Offset into the buffer where the string resides.
* @param type The encoding type that the {@link BinaryResourceString} is encoded in.
* @return The decoded string.
*/
public static String decodeString(ByteBuffer buffer, int offset, Type type) {
int length;
int characterCount = decodeLength(buffer, offset, type);
offset += computeLengthOffset(characterCount, type);
// UTF-8 strings have 2 lengths: the number of characters, and then the encoding length.
// UTF-16 strings, however, only have 1 length: the number of characters.
if (type == Type.UTF8) {
length = decodeLength(buffer, offset, type);
offset += computeLengthOffset(length, type);
} else {
length = characterCount * 2;
}
return new String(buffer.array(), offset, length, type.charset());
}
/**
* Encodes a string in either UTF-8 or UTF-16 and returns the bytes of the encoded string.
* Strings are prefixed by 2 values. The first is the number of characters in the string.
* The second is the encoding length (number of bytes in the string).
*
* <p>Here's an example UTF-8-encoded string of ab©:
* <pre>03 04 61 62 C2 A9 00</pre>
*
* @param str The string to be encoded.
* @param type The encoding type that the {@link BinaryResourceString} should be encoded in.
* @return The encoded string.
*/
public static byte[] encodeString(String str, Type type) {
byte[] bytes = str.getBytes(type.charset());
// The extra 5 bytes is for metadata (character count + byte count) and the NULL terminator.
ByteArrayDataOutput output = ByteStreams.newDataOutput(bytes.length + 5);
encodeLength(output, str.length(), type);
if (type == Type.UTF8) { // Only UTF-8 strings have the encoding length.
encodeLength(output, bytes.length, type);
}
output.write(bytes);
// NULL-terminate the string
if (type == Type.UTF8) {
output.write(0);
} else {
output.writeShort(0);
}
return output.toByteArray();
}
private static void encodeLength(ByteArrayDataOutput output, int length, Type type) {
if (length < 0) {
output.write(0);
return;
}
if (type == Type.UTF8) {
if (length > 0x7F) {
output.write(((length & 0x7F00) >> 8) | 0x80);
}
output.write(length & 0xFF);
} else { // UTF-16
// TODO(acornwall): Replace output with a little-endian output.
if (length > 0x7FFF) {
int highBytes = ((length & 0x7FFF0000) >> 16) | 0x8000;
output.write(highBytes & 0xFF);
output.write((highBytes & 0xFF00) >> 8);
}
int lowBytes = length & 0xFFFF;
output.write(lowBytes & 0xFF);
output.write((lowBytes & 0xFF00) >> 8);
}
}
private static int computeLengthOffset(int length, Type type) {
return (type == Type.UTF8 ? 1 : 2) * (length >= (type == Type.UTF8 ? 0x80 : 0x8000) ? 2 : 1);
}
private static int decodeLength(ByteBuffer buffer, int offset, Type type) {
return type == Type.UTF8 ? decodeLengthUTF8(buffer, offset) : decodeLengthUTF16(buffer, offset);
}
private static int decodeLengthUTF8(ByteBuffer buffer, int offset) {
// UTF-8 strings use a clever variant of the 7-bit integer for packing the string length.
// If the first byte is >= 0x80, then a second byte follows. For these values, the length
// is WORD-length in big-endian & 0x7FFF.
int length = UnsignedBytes.toInt(buffer.get(offset));
if ((length & 0x80) != 0) {
length = ((length & 0x7F) << 8) | UnsignedBytes.toInt(buffer.get(offset + 1));
}
return length;
}
private static int decodeLengthUTF16(ByteBuffer buffer, int offset) {
// UTF-16 strings use a clever variant of the 7-bit integer for packing the string length.
// If the first word is >= 0x8000, then a second word follows. For these values, the length
// is DWORD-length in big-endian & 0x7FFFFFFF.
int length = (buffer.getShort(offset) & 0xFFFF);
if ((length & 0x8000) != 0) {
length = ((length & 0x7FFF) << 16) | (buffer.getShort(offset + 2) & 0xFFFF);
}
return length;
}
}
@@ -0,0 +1,151 @@
/*
* 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.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.primitives.UnsignedBytes;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.Objects;
/** Represents a single typed resource value. */
public class BinaryResourceValue implements SerializableResource {
/** Resource type codes. */
public enum Type {
/** {@code data} is either 0 (undefined) or 1 (empty). */
NULL(0x00),
/** {@code data} holds a {@link ResourceTableChunk} entry reference. */
REFERENCE(0x01),
/** {@code data} holds an attribute resource identifier. */
ATTRIBUTE(0x02),
/** {@code data} holds an index into the containing resource table's string pool. */
STRING(0x03),
/** {@code data} holds a single-precision floating point number. */
FLOAT(0x04),
/** {@code data} holds a complex number encoding a dimension value, such as "100in". */
DIMENSION(0x05),
/** {@code data} holds a complex number encoding a fraction of a container. */
FRACTION(0x06),
/** {@code data} holds a dynamic {@link ResourceTableChunk} entry reference. */
DYNAMIC_REFERENCE(0x07),
/** {@code data} holds a dynamic attribute resource identifier. */
DYNAMIC_ATTRIBUTE(0x08),
/** {@code data} is a raw integer value of the form n..n. */
INT_DEC(0x10),
/** {@code data} is a raw integer value of the form 0xn..n. */
INT_HEX(0x11),
/** {@code data} is either 0 (false) or 1 (true). */
INT_BOOLEAN(0x12),
/** {@code data} is a raw integer value of the form #aarrggbb. */
INT_COLOR_ARGB8(0x1c),
/** {@code data} is a raw integer value of the form #rrggbb. */
INT_COLOR_RGB8(0x1d),
/** {@code data} is a raw integer value of the form #argb. */
INT_COLOR_ARGB4(0x1e),
/** {@code data} is a raw integer value of the form #rgb. */
INT_COLOR_RGB4(0x1f);
private final byte code;
private static final Map<Byte, Type> FROM_BYTE;
static {
Builder<Byte, Type> builder = ImmutableMap.builder();
for (Type type : values()) {
builder.put(type.code(), type);
}
FROM_BYTE = builder.build();
}
Type(int code) {
this.code = UnsignedBytes.checkedCast(code);
}
public byte code() {
return code;
}
public static Type fromCode(byte code) {
return Preconditions.checkNotNull(FROM_BYTE.get(code), "Unknown resource type: %s", code);
}
}
/** The serialized size in bytes of a {@link BinaryResourceValue}. */
public static final int SIZE = 8;
private final int size;
private final Type type;
private final int data;
public static BinaryResourceValue create(ByteBuffer buffer) {
int size = (buffer.getShort() & 0xFFFF);
buffer.get(); // Unused
Type type = Type.fromCode(buffer.get());
int data = buffer.getInt();
return new BinaryResourceValue(size, type, data);
}
public BinaryResourceValue(int size, Type type, int data) {
this.size = size;
this.type = type;
this.data = data;
}
/** The length in bytes of this value. */
public int size() { return size; }
/** The raw data type of this value. */
public Type type() { return type; }
/** The actual 4-byte value; interpretation of the value depends on {@code dataType}. */
public int data() { return data; }
@Override
public byte[] toByteArray() {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) {
ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short) size());
buffer.put((byte) 0); // Unused
buffer.put(type().code());
buffer.putInt(data());
return buffer.array();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BinaryResourceValue that = (BinaryResourceValue)o;
return size == that.size &&
data == that.data &&
type == that.type;
}
@Override
public int hashCode() {
return Objects.hash(size, type, data);
}
}
@@ -0,0 +1,301 @@
/*
* 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 androidx.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.io.LittleEndianDataOutputStream;
import com.google.common.primitives.Shorts;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
/** Represents a generic chunk. */
public abstract class Chunk implements SerializableResource {
/** Types of chunks that can exist. */
public enum Type {
NULL(0x0000),
STRING_POOL(0x0001),
TABLE(0x0002),
XML(0x0003),
XML_START_NAMESPACE(0x0100),
XML_END_NAMESPACE(0x0101),
XML_START_ELEMENT(0x0102),
XML_END_ELEMENT(0x0103),
XML_CDATA(0x0104),
XML_RESOURCE_MAP(0x0180),
TABLE_PACKAGE(0x0200),
TABLE_TYPE(0x0201),
TABLE_TYPE_SPEC(0x0202),
TABLE_LIBRARY(0x0203);
private final short code;
private static final Map<Short, Type> FROM_SHORT;
static {
Builder<Short, Type> builder = ImmutableMap.builder();
for (Type type : values()) {
builder.put(type.code(), type);
}
FROM_SHORT = builder.build();
}
Type(int code) {
this.code = Shorts.checkedCast(code);
}
public short code() {
return code;
}
public static Type fromCode(short code) {
return Preconditions.checkNotNull(FROM_SHORT.get(code), "Unknown chunk type: %s", code);
}
}
/** The byte boundary to pad chunks on. */
public static final int PAD_BOUNDARY = 4;
/** The number of bytes in every chunk that describes chunk type, header size, and chunk size. */
public static final int METADATA_SIZE = 8;
/** The offset in bytes, from the start of the chunk, where the chunk size can be found. */
private static final int CHUNK_SIZE_OFFSET = 4;
/** The parent to this chunk, if any. */
@Nullable
private final Chunk parent;
/** Size of the chunk header in bytes. */
protected final int headerSize;
/** headerSize + dataSize. The total size of this chunk. */
protected final int chunkSize;
/** Offset of this chunk from the start of the file. */
protected final int offset;
protected Chunk(ByteBuffer buffer, @Nullable Chunk parent) {
this.parent = parent;
offset = buffer.position() - 2;
headerSize = (buffer.getShort() & 0xFFFF);
chunkSize = buffer.getInt();
}
/**
* Finishes initialization of a chunk. This should be called immediately after the constructor.
* This is separate from the constructor so that the header of a chunk can be fully initialized
* before the payload of that chunk is initialized for chunks that require such behavior.
*
* @param buffer The buffer that the payload will be initialized from.
*/
protected void init(ByteBuffer buffer) {}
/**
* Returns the parent to this chunk, if any. A parent is a chunk whose payload contains this
* chunk. If there's no parent, null is returned.
*/
@Nullable
public Chunk getParent() {
return parent;
}
protected abstract Type getType();
/** Returns the size of this chunk's header. */
public final int getHeaderSize() {
return headerSize;
}
/**
* Returns the size of this chunk when it was first read from a buffer. A chunk's size can deviate
* from this value when its data is modified (e.g. adding an entry, changing a string).
*
* <p>A chunk's current size can be determined from the length of the byte array returned from
* {@link #toByteArray}.
*/
public final int getOriginalChunkSize() {
return chunkSize;
}
/**
* Reposition the buffer after this chunk. Use this at the end of a Chunk constructor.
* @param buffer The buffer to be repositioned.
*/
private final void seekToEndOfChunk(ByteBuffer buffer) {
buffer.position(offset + chunkSize);
}
/**
* Writes the type and header size. We don't know how big this chunk will be (it could be
* different since the last time we checked), so this needs to be passed in.
*
* @param output The buffer that will be written to.
* @param chunkSize The total size of this chunk in bytes, including the header.
*/
protected final void writeHeader(ByteBuffer output, int chunkSize) {
int start = output.position();
output.putShort(getType().code());
output.putShort((short) headerSize);
output.putInt(chunkSize);
writeHeader(output);
int headerBytes = output.position() - start;
Preconditions.checkState(headerBytes == getHeaderSize(),
"Written header is wrong size. Got %s, want %s", headerBytes, getHeaderSize());
}
/**
* Writes the remaining header (after the type, {@code headerSize}, and {@code chunkSize}).
*
* @param output The buffer that the header will be written to.
*/
protected void writeHeader(ByteBuffer output) {}
/**
* Writes the chunk payload. The payload is data in a chunk which is not in
* the first {@code headerSize} bytes of the chunk.
*
* @param output The stream that the payload will be written to.
* @param header The already-written header. This can be modified to fix payload offsets.
* @param shrink True if this payload should be optimized for size.
* @throws IOException Thrown if {@code output} could not be written to (out of memory).
*/
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {}
/**
* Pads {@code output} until {@code currentLength} is on a 4-byte boundary.
*
* @param output The {@link DataOutput} that will be padded.
* @param currentLength The current length, in bytes, of {@code output}
* @return The new length of {@code output}
* @throws IOException Thrown if {@code output} could not be written to.
*/
protected int writePad(DataOutput output, int currentLength) throws IOException {
while (currentLength % PAD_BOUNDARY != 0) {
output.write(0);
++currentLength;
}
return currentLength;
}
@Override
public final byte[] toByteArray() throws IOException {
return toByteArray(false);
}
/**
* Converts this chunk into an array of bytes representation. Normally you will not need to
* override this method unless your header changes based on the contents / size of the payload.
*/
@Override
public final byte[] toByteArray(boolean shrink) throws IOException {
ByteBuffer header = ByteBuffer.allocate(getHeaderSize()).order(ByteOrder.LITTLE_ENDIAN);
writeHeader(header, 0); // The chunk size isn't known yet. This will be filled in later.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
writePayload(payload, header, shrink);
}
byte[] payloadBytes = baos.toByteArray();
int chunkSize = getHeaderSize() + payloadBytes.length;
header.putInt(CHUNK_SIZE_OFFSET, chunkSize);
// Combine results
ByteBuffer result = ByteBuffer.allocate(chunkSize).order(ByteOrder.LITTLE_ENDIAN);
result.put(header.array());
result.put(payloadBytes);
return result.array();
}
/**
* Creates a new chunk whose contents start at {@code buffer}'s current position.
*
* @param buffer A buffer positioned at the start of a chunk.
* @return new chunk
*/
public static Chunk newInstance(ByteBuffer buffer) {
return newInstance(buffer, null);
}
/**
* Creates a new chunk whose contents start at {@code buffer}'s current position.
*
* @param buffer A buffer positioned at the start of a chunk.
* @param parent The parent to this chunk (or null if there's no parent).
* @return new chunk
*/
public static Chunk newInstance(ByteBuffer buffer, @Nullable Chunk parent) {
Chunk result;
Type type = Type.fromCode(buffer.getShort());
switch (type) {
case STRING_POOL:
result = new StringPoolChunk(buffer, parent);
break;
case TABLE:
result = new ResourceTableChunk(buffer, parent);
break;
case XML:
result = new XmlChunk(buffer, parent);
break;
case XML_START_NAMESPACE:
result = new XmlNamespaceStartChunk(buffer, parent);
break;
case XML_END_NAMESPACE:
result = new XmlNamespaceEndChunk(buffer, parent);
break;
case XML_START_ELEMENT:
result = new XmlStartElementChunk(buffer, parent);
break;
case XML_END_ELEMENT:
result = new XmlEndElementChunk(buffer, parent);
break;
case XML_CDATA:
result = new XmlCdataChunk(buffer, parent);
break;
case XML_RESOURCE_MAP:
result = new XmlResourceMapChunk(buffer, parent);
break;
case TABLE_PACKAGE:
result = new PackageChunk(buffer, parent);
break;
case TABLE_TYPE:
result = new TypeChunk(buffer, parent);
break;
case TABLE_TYPE_SPEC:
result = new TypeSpecChunk(buffer, parent);
break;
case TABLE_LIBRARY:
result = new LibraryChunk(buffer, parent);
break;
default:
result = new UnknownChunk(buffer, parent);
}
result.init(buffer);
result.seekToEndOfChunk(buffer);
return result;
}
}
@@ -0,0 +1,73 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;
/** Represents a chunk whose payload is a list of sub-chunks. */
public abstract class ChunkWithChunks extends Chunk {
private final Map<Integer, Chunk> chunks = new LinkedHashMap<>();
protected ChunkWithChunks(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
chunks.clear();
int start = this.offset + getHeaderSize();
int offset = start;
int end = this.offset + getOriginalChunkSize();
int position = buffer.position();
buffer.position(start);
while (offset < end) {
Chunk chunk = Chunk.newInstance(buffer, this);
chunks.put(offset, chunk);
offset += chunk.getOriginalChunkSize();
}
buffer.position(position);
}
/**
* Retrieves the @{code chunks} contained in this chunk.
*
* @return map of buffer offset -> chunk contained in this chunk.
*/
public final Map<Integer, Chunk> getChunks() {
return chunks;
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
for (Chunk chunk : getChunks().values()) {
byte[] chunkBytes = chunk.toByteArray(shrink);
output.write(chunkBytes);
writePad(output, chunkBytes.length);
}
}
}
@@ -0,0 +1,136 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Contains a list of package-id to package name mappings for any shared libraries used in this
* {@link ResourceTableChunk}. The package-id's encoded in this resource table may be different
* than the id's assigned at runtime
*/
public final class LibraryChunk extends Chunk {
/** The number of resources of this type at creation time. */
private final int entryCount;
/** The libraries used in this chunk (package id + name). */
private final List<Entry> entries = new ArrayList<>();
protected LibraryChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
entryCount = buffer.getInt();
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
entries.addAll(enumerateEntries(buffer));
}
private List<Entry> enumerateEntries(ByteBuffer buffer) {
List<Entry> result = new ArrayList<>(entryCount);
int offset = this.offset + getHeaderSize();
int endOffset = offset + Entry.SIZE * entryCount;
while (offset < endOffset) {
result.add(Entry.create(buffer, offset));
offset += Entry.SIZE;
}
return result;
}
@Override
protected Type getType() {
return Type.TABLE_LIBRARY;
}
@Override
protected void writeHeader(ByteBuffer output) {
super.writeHeader(output);
output.putInt(entries.size());
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
for (Entry entry : entries) {
output.write(entry.toByteArray(shrink));
}
}
/** A shared library package-id to package name entry. */
protected static class Entry implements SerializableResource {
/** Library entries only contain a package ID (4 bytes) and a package name. */
private static final int SIZE = 4 + PackageUtils.PACKAGE_NAME_SIZE;
private final int packageId;
private final String packageName;
static Entry create(ByteBuffer buffer, int offset) {
int packageId = buffer.getInt(offset);
String packageName = PackageUtils.readPackageName(buffer, offset + 4);
return new Entry(packageId, packageName);
}
private Entry(int packageId, String packageName) {
this.packageId = packageId;
this.packageName = packageName;
}
/** The id assigned to the shared library at build time. */
public int packageId() { return packageId; }
/** The package name of the shared library. */
public String packageName() { return packageName; }
@Override
public byte[] toByteArray() throws IOException {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(packageId());
PackageUtils.writePackageName(buffer, packageName());
return buffer.array();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entry entry = (Entry)o;
return packageId == entry.packageId &&
Objects.equals(packageName, entry.packageName);
}
@Override
public int hashCode() {
return Objects.hash(packageId, packageName);
}
}
}
@@ -0,0 +1,239 @@
/*
* 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.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/** A package chunk is a collection of resource data types within a package. */
public final class PackageChunk extends ChunkWithChunks {
/** Offset in bytes, from the start of the chunk, where {@code typeStringsOffset} can be found. */
private static final int TYPE_OFFSET_OFFSET = 268;
/** Offset in bytes, from the start of the chunk, where {@code keyStringsOffset} can be found. */
private static final int KEY_OFFSET_OFFSET = 276;
/** The package id if this is a base package, or 0 if not a base package. */
private int id;
/** The name of the package. */
private String packageName;
/** The offset (from {@code offset}) in the original buffer where type strings start. */
private final int typeStringsOffset;
/** The index into the type string pool of the last public type. */
private final int lastPublicType;
/** An offset to the string pool that contains the key strings for this package. */
private final int keyStringsOffset;
/** The index into the key string pool of the last public key. */
private final int lastPublicKey;
/** An offset to the type ID(s). This is undocumented in the original code. */
private final int typeIdOffset;
/** Contains a mapping of a type index to its {@link TypeSpecChunk}. */
private final Map<Integer, TypeSpecChunk> typeSpecs = new HashMap<>();
/** Contains a mapping of a type index to all of the {@link TypeChunk} with that index. */
private final Multimap<Integer, TypeChunk> types = ArrayListMultimap.create();
/** May contain a library chunk for mapping dynamic references to resolved references. */
private Optional<LibraryChunk> libraryChunk = Optional.empty();
protected PackageChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
id = buffer.getInt();
packageName = PackageUtils.readPackageName(buffer, buffer.position());
typeStringsOffset = buffer.getInt();
lastPublicType = buffer.getInt();
keyStringsOffset = buffer.getInt();
lastPublicKey = buffer.getInt();
typeIdOffset = buffer.getInt();
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
for (Chunk chunk : getChunks().values()) {
if (chunk instanceof TypeChunk) {
TypeChunk typeChunk = (TypeChunk) chunk;
types.put(typeChunk.getId(), typeChunk);
} else if (chunk instanceof TypeSpecChunk) {
TypeSpecChunk typeSpecChunk = (TypeSpecChunk) chunk;
typeSpecs.put(typeSpecChunk.getId(), typeSpecChunk);
} else if (chunk instanceof LibraryChunk) {
if (libraryChunk.isPresent()) {
throw new IllegalStateException(
"Multiple library chunks present in package chunk.");
}
libraryChunk = Optional.of((LibraryChunk) chunk);
} else if (!(chunk instanceof StringPoolChunk)) {
throw new IllegalStateException(
String.format("PackageChunk contains an unexpected chunk: %s", chunk.getClass()));
}
}
}
/** Returns the package id if this is a base package, or 0 if not a base package. */
public int getId() {
return id;
}
/** Sets the package id */
public void setId(int id) {
this.id = id;
}
/**
* Returns the string pool that contains the names of the resources in this package.
*/
public StringPoolChunk getKeyStringPool() {
Chunk chunk = Preconditions.checkNotNull(getChunks().get(keyStringsOffset + offset));
Preconditions.checkState(chunk instanceof StringPoolChunk, "Key string pool not found.");
return (StringPoolChunk) chunk;
}
/**
* Get the type string for a specific id, e.g., (e.g. string, attr, id).
*
* @param id The id to get the type for.
* @return The type string.
*/
public String getTypeString(int id) {
StringPoolChunk typePool = getTypeStringPool();
Preconditions.checkNotNull(typePool, "Package has no type pool.");
Preconditions.checkState(typePool.getStyleCount() >= id, "No type for id: " + id);
return typePool.getString(id - 1); // - 1 here to convert to 0-based index
}
/**
* Returns the string pool that contains the type strings for this package, such as "layout",
* "string", "color".
*/
public StringPoolChunk getTypeStringPool() {
Chunk chunk = Preconditions.checkNotNull(getChunks().get(typeStringsOffset + offset));
Preconditions.checkState(chunk instanceof StringPoolChunk, "Type string pool not found.");
return (StringPoolChunk) chunk;
}
/** Returns all {@link TypeChunk} in this package. */
public Collection<TypeChunk> getTypeChunks() {
return types.values();
}
/**
* For a given type id, returns the {@link TypeChunk} objects that match that id. The type id is
* the 1-based index of the type in the type string pool (returned by {@link #getTypeStringPool}).
*
* @param id The 1-based type id to return {@link TypeChunk} objects for.
* @return The matching {@link TypeChunk} objects, or an empty collection if there are none.
*/
public Collection<TypeChunk> getTypeChunks(int id) {
return types.get(id);
}
/**
* For a given type, returns the {@link TypeChunk} objects that match that type
* (e.g. "attr", "id", "string", ...).
*
* @param type The type to return {@link TypeChunk} objects for.
* @return The matching {@link TypeChunk} objects, or an empty collection if there are none.
*/
public Collection<TypeChunk> getTypeChunks(String type) {
StringPoolChunk typeStringPool = Preconditions.checkNotNull(getTypeStringPool());
return getTypeChunks(typeStringPool.indexOf(type) + 1); // Convert 0-based index to 1-based
}
/** Returns all {@link TypeSpecChunk} in this package. */
public Collection<TypeSpecChunk> getTypeSpecChunks() {
return typeSpecs.values();
}
/** For a given (1-based) type id, returns the {@link TypeSpecChunk} matching it. */
public TypeSpecChunk getTypeSpecChunk(int id) {
return Preconditions.checkNotNull(typeSpecs.get(id));
}
/**
* For a given {@code type}, returns the {@link TypeSpecChunk} that matches it
* (e.g. "attr", "id", "string", ...).
*/
public TypeSpecChunk getTypeSpecChunk(String type) {
StringPoolChunk typeStringPool = Preconditions.checkNotNull(getTypeStringPool());
return getTypeSpecChunk(typeStringPool.indexOf(type) + 1); // Convert 0-based index to 1-based
}
/** Returns the name of this package. */
public String getPackageName() {
return packageName;
}
/** Set the package name */
public void setPackageName(String packageName) {
this.packageName = packageName;
}
@Override
protected Type getType() {
return Type.TABLE_PACKAGE;
}
@Override
protected void writeHeader(ByteBuffer output) {
output.putInt(id);
PackageUtils.writePackageName(output, packageName);
output.putInt(0); // typeStringsOffset. This value can't be computed here.
output.putInt(lastPublicType);
output.putInt(0); // keyStringsOffset. This value can't be computed here.
output.putInt(lastPublicKey);
output.putInt(typeIdOffset);
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
int typeOffset = typeStringsOffset;
int keyOffset = keyStringsOffset;
int payloadOffset = 0;
for (Chunk chunk : getChunks().values()) {
if (chunk == getTypeStringPool()) {
typeOffset = payloadOffset + getHeaderSize();
} else if (chunk == getKeyStringPool()) {
keyOffset = payloadOffset + getHeaderSize();
}
byte[] chunkBytes = chunk.toByteArray(shrink);
output.write(chunkBytes);
payloadOffset = writePad(output, chunkBytes.length);
}
header.putInt(TYPE_OFFSET_OFFSET, typeOffset);
header.putInt(KEY_OFFSET_OFFSET, keyOffset);
}
}
@@ -0,0 +1,67 @@
/*
* 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 java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/** Provides utility methods for package names. */
public final class PackageUtils {
public static final int PACKAGE_NAME_SIZE = 256;
private PackageUtils() {} // Prevent instantiation
/**
* Reads the package name from the buffer and repositions the buffer to point directly after
* the package name.
* @param buffer The buffer containing the package name.
* @param offset The offset in the buffer to read from.
* @return The package name.
*/
public static String readPackageName(ByteBuffer buffer, int offset) {
byte[] data = buffer.array();
int length = 0;
// Look for the null terminator for the string instead of using the entire buffer.
// It's UTF-16 so check 2 bytes at a time to see if its double 0.
for (int i = offset; i < data.length && i < PACKAGE_NAME_SIZE + offset; i += 2) {
if (data[i] == 0 && data[i + 1] == 0) {
length = i - offset;
break;
}
}
Charset utf16 = StandardCharsets.UTF_16LE;
String str = new String(data, offset, length, utf16);
buffer.position(offset + PACKAGE_NAME_SIZE);
return str;
}
/**
* Writes the provided package name to the buffer in UTF-16.
* @param buffer The buffer that will be written to.
* @param packageName The package name that will be written to the buffer.
*/
public static void writePackageName(ByteBuffer buffer, String packageName) {
byte[] nameBytes = packageName.getBytes(StandardCharsets.UTF_16LE);
buffer.put(nameBytes, 0, Math.min(nameBytes.length, PACKAGE_NAME_SIZE));
if (nameBytes.length < PACKAGE_NAME_SIZE) {
// pad out the remaining space with an empty array.
buffer.put(new byte[PACKAGE_NAME_SIZE - nameBytes.length]);
}
}
}
@@ -0,0 +1,280 @@
/*
* 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.common.base.Preconditions;
import com.google.devrel.gmscore.tools.apk.arsc.ArscBlamer.ResourceEntry;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
/**
* Calculates extra information about an {@link ResourceEntry}, such as the total
* APK size the entry is responsible for.
*
* This class is not thread-safe.
*/
public class ResourceEntryStatsCollector {
/** The size in bytes of an offset in a chunk. */
private static final int OFFSET_SIZE = 4;
/** The size in bytes of overhead for styles, if present, in {@link StringPoolChunk}. */
private static final int STYLE_OVERHEAD = 8;
/**
* The number of bytes, in addition to the header, that the {@link PackageChunk} has in overhead
* excluding the chunks it contains.
*/
private static final int PACKAGE_CHUNK_OVERHEAD = 8;
private final Map<ResourceEntry, ResourceStatistics> stats = new HashMap<>();
private final ArscBlamer blamer;
private final ResourceTableChunk resourceTable;
/**
* Creates a new {@link ResourceEntryStatsCollector}.
*
* @param blamer The blamer that maps resource entries to what they use.
* @param resourceTable The resource table that {@code blamer} is blamed on.
*/
public ResourceEntryStatsCollector(ArscBlamer blamer, ResourceTableChunk resourceTable) {
this.resourceTable = resourceTable;
this.blamer = blamer;
}
public void compute() throws IOException {
Preconditions.checkState(stats.isEmpty(), "Must only call #compute once.");
blamer.blame();
computeStringPoolSizes();
computePackageSizes();
}
/** Returns entries for which there are computed stats. Must first call {@link #compute}. */
public Map<ResourceEntry, ResourceStatistics> getStats() {
Preconditions.checkState(!stats.isEmpty(), "Must call #compute() first.");
return Collections.unmodifiableMap(stats);
}
/** Returns computed stats for a given entry. Must first call {@link #compute}. */
public ResourceStatistics getStats(ResourceEntry entry) {
Preconditions.checkState(!stats.isEmpty(), "Must call #compute() first.");
return stats.containsKey(entry) ? stats.get(entry) : ResourceStatistics.EMPTY;
}
private void computeStringPoolSizes() throws IOException {
computePoolSizes(resourceTable.getStringPool(), blamer.getStringToBlamedResources());
}
private void computePackageSizes() throws IOException {
computeTypePoolSizes();
computeKeyPoolSizes();
computeTypeSpecSizes();
computeTypeChunkSizes();
computePackageChunkSizes();
}
private void computeTypePoolSizes() throws IOException {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getTypeToBlamedResources().entrySet()) {
computePoolSizes(entry.getKey().getTypeStringPool(), entry.getValue());
}
}
private void computeKeyPoolSizes() throws IOException {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getKeyToBlamedResources().entrySet()) {
computePoolSizes(entry.getKey().getKeyStringPool(), entry.getValue());
}
}
private void computeTypeSpecSizes() {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getTypeToBlamedResources().entrySet()) {
computeTypeSpecSizes(entry.getKey(), entry.getValue());
}
}
private void computeTypeChunkSizes() {
for (Entry<TypeChunk.Entry, Collection<ResourceEntry>> entry
: blamer.getTypeEntryToBlamedResources().asMap().entrySet()) {
TypeChunk.Entry chunkEntry = entry.getKey();
TypeChunk typeChunk = chunkEntry.parent();
int size = chunkEntry.size() + OFFSET_SIZE;
int count = typeChunk.getEntries().size();
int nullEntries = typeChunk.getTotalEntryCount() - typeChunk.getEntries().size();
int overhead = typeChunk.getHeaderSize() + nullEntries * OFFSET_SIZE;
addSizes(entry.getValue(), overhead, size, count);
}
}
private void computePackageChunkSizes() {
for (Entry<PackageChunk, Collection<ResourceEntry>> entry
: blamer.getPackageToBlamedResources().asMap().entrySet()) {
int overhead = entry.getKey().getHeaderSize() + PACKAGE_CHUNK_OVERHEAD;
addSizes(entry.getValue(), overhead, 0, 1);
}
}
private void computePoolSizes(StringPoolChunk stringPool,
List<ResourceEntry>[] usages) throws IOException {
int overhead = stringPool.getHeaderSize();
if (stringPool.getStyleCount() > 0) {
overhead += STYLE_OVERHEAD;
}
// We have to iterate over the indices of the string pool, because it is possible that there are
// indices which have *no* associated resource entry (i.e. references from XML files without an
// entry in R).
int count = 0;
for (int i = 0; i < usages.length; ++i) {
if (usages[i].isEmpty()) {
overhead += computeStringAndStyleSize(stringPool, i);
} else {
++count;
}
}
// Now that we know the number of actual entries, we can compute the size.
for (int i = 0; i < usages.length; ++i) {
if (usages[i].isEmpty()) {
continue;
}
int size = computeStringAndStyleSize(stringPool, i);
addSizes(usages[i], overhead, size, count);
}
}
private void computeTypeSpecSizes(PackageChunk packageChunk,
List<ResourceEntry>[] usages) {
for (int i = 0; i < usages.length; ++i) {
// The 1 here is to convert back to a 1-based index.
TypeSpecChunk typeSpec = packageChunk.getTypeSpecChunk(i + 1);
// TypeSpecChunk entries share everything equally.
addSizes(usages[i], typeSpec.getOriginalChunkSize(), 0, 1);
}
}
/**
* Given an {@code index} into a {@code stringPool}, return string's total size in bytes plus its
* style, if any.
*
* @param stringPool The string pool containing the {@code index}.
* @param index The (0-based) index of the string and (optional) style.
* @throws IOException Thrown if the style's length could not be computed.
*/
private int computeStringAndStyleSize(StringPoolChunk stringPool, int index)
throws IOException {
return computeStringSize(stringPool, index) + computeStyleSize(stringPool, index);
}
/** Given an {@code index} into a {@code stringPool}, return string's total size in bytes. */
private int computeStringSize(StringPoolChunk stringPool, int index) {
String string = stringPool.getString(index);
int result = BinaryResourceString.encodeString(string, stringPool.getStringType()).length;
result += OFFSET_SIZE;
return result;
}
/**
* Given an {@code index} into a {@code stringPool}, return style's total size in bytes or 0 if
* there's no style at that index.
*
* @throws IOException Thrown if the style's length could not be computed.
*/
private int computeStyleSize(StringPoolChunk stringPool, int index) throws IOException {
if (index >= stringPool.getStyleCount()) { // No style at index
return 0;
}
return stringPool.getStyle(index).toByteArray().length + OFFSET_SIZE;
}
/**
* Adds to the {@link #stats} of {@code entries} that reference a value in a chunk the bytes it's
* responsible for. This should only be called once per chunk-value pair.
*
* @param entries The resource entries referencing a single value in a chunk.
* @param overhead The number of bytes of overhead of a chunk. Typically the header size.
* @param size The size in bytes of a value in a chunk that {@code entries} reference.
* @param count The total number of values in the chunk.
*/
private void addSizes(Collection<ResourceEntry> entries, int overhead, int size, int count) {
int usageCount = entries.size();
for (ResourceEntry resourceEntry : entries) {
// TODO(acornwall): Replace with Java 8's #getOrDefault when possible.
if (!stats.containsKey(resourceEntry)) {
stats.put(resourceEntry, new ResourceStatistics());
}
ResourceStatistics resourceStats = stats.get(resourceEntry);
if (usageCount == 1) {
resourceStats.addPrivateSize(size);
} else {
resourceStats.addSharedSize(size);
}
// Special case: If the chunk only has one relevant value, removing this entry will remove the
// entire chunk.
if (usageCount == 1 && count == 1) {
resourceStats.addPrivateSize(overhead);
}
resourceStats.addProportionalSize(size, usageCount);
resourceStats.addProportionalSize(overhead, usageCount * count);
}
}
/** Stats for an individual {@link ResourceEntry}. */
public static class ResourceStatistics {
/** The empty, immutable instance of ResourceStatistics which contains 0 for all values. */
public static final ResourceStatistics EMPTY = new ResourceStatistics();
private int privateSize = 0;
private int sharedSize = 0;
private double proportionalSize = 0;
private ResourceStatistics() {}
/** The number of bytes that would be freed if this resource was removed. */
public int getPrivateSize() {
return privateSize;
}
/** The number of bytes taken up by this resource that are also shared with other resources. */
public int getSharedSize() {
return sharedSize;
}
/** The total size this resource is responsible for. */
public double getProportionalSize() {
return proportionalSize;
}
private void addPrivateSize(int privateSize) {
this.privateSize += privateSize;
}
private void addSharedSize(int sharedSize) {
this.sharedSize += sharedSize;
}
private void addProportionalSize(int numerator, int denominator) {
this.proportionalSize += 1.0 * numerator / denominator;
}
}
}
@@ -0,0 +1,92 @@
/*
* 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.common.base.Preconditions;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a resource table structure. Its sub-chunks contain:
*
* <ul>
* <li>A {@link StringPoolChunk} containing all string values in the entire resource table. It does
* not, however, contain the names of entries or type identifiers.
* <li>One or more {@link PackageChunk}.
* </ul>
*/
public final class ResourceTableChunk extends ChunkWithChunks {
/** A string pool containing all string resource values in the entire resource table. */
private StringPoolChunk stringPool;
/** The packages contained in this resource table. */
private final Map<String, PackageChunk> packages = new HashMap<>();
protected ResourceTableChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
// packageCount. We ignore this, because we already know how many chunks we have.
Preconditions.checkState(buffer.getInt() >= 1, "ResourceTableChunk package count was < 1.");
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
packages.clear();
for (Chunk chunk : getChunks().values()) {
if (chunk instanceof PackageChunk) {
PackageChunk packageChunk = (PackageChunk) chunk;
packages.put(packageChunk.getPackageName(), packageChunk);
} else if (chunk instanceof StringPoolChunk) {
stringPool = (StringPoolChunk) chunk;
}
}
Preconditions.checkNotNull(stringPool, "ResourceTableChunk must have a string pool.");
}
/** Returns the string pool containing all string resource values in the resource table. */
public StringPoolChunk getStringPool() {
return stringPool;
}
/** Returns the package with the given {@code packageName}. Else, returns null. */
@Nullable
public PackageChunk getPackage(String packageName) {
return packages.get(packageName);
}
/** Returns the packages contained in this resource table. */
public Collection<PackageChunk> getPackages() {
return Collections.unmodifiableCollection(packages.values());
}
@Override
protected Type getType() {
return Type.TABLE;
}
@Override
protected void writeHeader(ByteBuffer output) {
super.writeHeader(output);
output.putInt(packages.size());
}
}
@@ -0,0 +1,41 @@
/*
* 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 java.io.IOException;
/**
* A resource, typically a @{link Chunk}, that can be converted to an array of bytes.
*/
public interface SerializableResource {
/**
* Converts this resource into an array of bytes representation.
* @return An array of bytes representing this resource.
* @throws IOException
*/
byte[] toByteArray() throws IOException;
/**
* Converts this resource into an array of bytes representation.
* @param shrink True if, when converting to a byte array, this resource can modify the returned
* bytes in an effort to reduce the size.
* @return An array of bytes representing this resource.
* @throws IOException
*/
byte[] toByteArray(boolean shrink) throws IOException;
}
@@ -0,0 +1,423 @@
/*
* 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.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.io.LittleEndianDataOutputStream;
import androidx.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
/** Represents a string pool structure. */
public final class StringPoolChunk extends Chunk {
// These are the defined flags for the "flags" field of ResourceStringPoolHeader
private static final int SORTED_FLAG = 1 << 0;
private static final int UTF8_FLAG = 1 << 8;
/** The offset from the start of the header that the stylesStart field is at. */
private static final int STYLE_START_OFFSET = 24;
/** Flags. */
private final int flags;
/** Index from header of the string data. */
private final int stringsStart;
/** Index from header of the style data. */
private final int stylesStart;
/**
* Number of strings in the original buffer. This is not necessarily the number of strings in
* {@code strings}.
*/
private final int stringCount;
/**
* Number of styles in the original buffer. This is not necessarily the number of styles in
* {@code styles}.
*/
private final int styleCount;
/**
* The strings ordered as they appear in the arsc file. e.g. strings.get(1234) gets the 1235th
* string in the arsc file.
*/
private final List<String> strings = new ArrayList<>();
/**
* These styles have a 1:1 relationship with the strings. For example, styles.get(3) refers to
* the string at location strings.get(3). There are never more styles than strings (though there
* may be less). Inside of that are all of the styles referenced by that string.
*/
private final List<StringPoolStyle> styles = new ArrayList<>();
/**
* True if the original {@link StringPoolChunk} shows signs of being deduped. Specifically, this
* is set to true if there exists a string whose offset is <= the previous offset. This is used to
* preserve the deduping of strings for pools that have been deduped.
*/
private boolean isOriginalDeduped = false;
protected StringPoolChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
stringCount = buffer.getInt();
styleCount = buffer.getInt();
flags = buffer.getInt();
stringsStart = buffer.getInt();
stylesStart = buffer.getInt();
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
strings.addAll(readStrings(buffer, offset + stringsStart, stringCount));
styles.addAll(readStyles(buffer, offset + stylesStart, styleCount));
}
/**
* Returns the 0-based index of the first occurrence of the given string, or -1 if the string is
* not in the pool. This runs in O(n) time.
*
* @param string The string to check the pool for.
* @return Index of the string, or -1 if not found.
*/
public int indexOf(String string) {
return strings.indexOf(string);
}
/**
* Returns a string at the given (0-based) index.
*
* @param index The (0-based) index of the string to return.
* @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()).
*/
public String getString(int index) {
return strings.get(index);
}
public void updateString(int index, String value){
strings.set(index, value);
}
/** Returns the number of strings in this pool. */
public int getStringCount() {
return strings.size();
}
/**
* Returns a style at the given (0-based) index.
*
* @param index The (0-based) index of the style to return.
* @throws IndexOutOfBoundsException If the index is out of range (index < 0 || index >= size()).
*/
public StringPoolStyle getStyle(int index) {
return styles.get(index);
}
/** Returns the number of styles in this pool. */
public int getStyleCount() {
return styles.size();
}
/** Returns the type of strings in this pool. */
public BinaryResourceString.Type getStringType() {
return isUTF8() ? BinaryResourceString.Type.UTF8 : BinaryResourceString.Type.UTF16;
}
@Override
protected Type getType() {
return Type.STRING_POOL;
}
/** Returns the number of bytes needed for offsets based on {@code strings} and {@code styles}. */
private int getOffsetSize() {
return (strings.size() + styles.size()) * 4;
}
/**
* True if this string pool contains strings in UTF-8 format. Otherwise, strings are in UTF-16.
*
* @return true if @{code strings} are in UTF-8; false if they're in UTF-16.
*/
public boolean isUTF8() {
return (flags & UTF8_FLAG) != 0;
}
/**
* True if this string pool contains already-sorted strings.
*
* @return true if @{code strings} are sorted.
*/
public boolean isSorted() {
return (flags & SORTED_FLAG) != 0;
}
private List<String> readStrings(ByteBuffer buffer, int offset, int count) {
List<String> result = new ArrayList<>();
int previousOffset = -1;
// After the header, we now have an array of offsets for the strings in this pool.
for (int i = 0; i < count; ++i) {
int stringOffset = offset + buffer.getInt();
result.add(BinaryResourceString.decodeString(buffer, stringOffset, getStringType()));
if (stringOffset <= previousOffset) {
isOriginalDeduped = true;
}
previousOffset = stringOffset;
}
return result;
}
private List<StringPoolStyle> readStyles(ByteBuffer buffer, int offset, int count) {
List<StringPoolStyle> result = new ArrayList<>();
// After the array of offsets for the strings in the pool, we have an offset for the styles
// in this pool.
for (int i = 0; i < count; ++i) {
int styleOffset = offset + buffer.getInt();
result.add(StringPoolStyle.create(buffer, styleOffset, this));
}
return result;
}
private int writeStrings(DataOutput payload, ByteBuffer offsets, boolean shrink)
throws IOException {
int stringOffset = 0;
Map<String, Integer> used = new HashMap<>(); // Keeps track of strings already written
for (String string : strings) {
// Dedupe everything except stylized strings, unless shrink is true (then dedupe everything)
if (used.containsKey(string) && (shrink || isOriginalDeduped)) {
Integer offset = used.get(string);
offsets.putInt(offset == null ? 0 : offset);
} else {
byte[] encodedString = BinaryResourceString.encodeString(string, getStringType());
payload.write(encodedString);
used.put(string, stringOffset);
offsets.putInt(stringOffset);
stringOffset += encodedString.length;
}
}
// ARSC files pad to a 4-byte boundary. We should do so too.
stringOffset = writePad(payload, stringOffset);
return stringOffset;
}
private int writeStyles(DataOutput payload, ByteBuffer offsets, boolean shrink)
throws IOException {
int styleOffset = 0;
if (!styles.isEmpty()) {
Map<StringPoolStyle, Integer> used = new HashMap<>(); // Keeps track of bytes already written
for (StringPoolStyle style : styles) {
if (!used.containsKey(style) || !shrink) {
byte[] encodedStyle = style.toByteArray(shrink);
payload.write(encodedStyle);
used.put(style, styleOffset);
offsets.putInt(styleOffset);
styleOffset += encodedStyle.length;
} else { // contains key and shrink is true
Integer offset = used.get(style);
offsets.putInt(offset == null ? 0 : offset);
}
}
// The end of the spans are terminated with another sentinel value
payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END);
styleOffset += 4;
// TODO(acornwall): There appears to be an extra SPAN_END here... why?
payload.writeInt(StringPoolStyle.RES_STRING_POOL_SPAN_END);
styleOffset += 4;
styleOffset = writePad(payload, styleOffset);
}
return styleOffset;
}
@Override
protected void writeHeader(ByteBuffer output) {
int stringsStart = getHeaderSize() + getOffsetSize();
output.putInt(strings.size());
output.putInt(styles.size());
output.putInt(flags);
output.putInt(strings.isEmpty() ? 0 : stringsStart);
output.putInt(0); // Placeholder. The styles starting offset cannot be computed at this point.
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int stringOffset = 0;
ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize());
offsets.order(ByteOrder.LITTLE_ENDIAN);
// Write to a temporary payload so we can rearrange this and put the offsets first
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
stringOffset = writeStrings(payload, offsets, shrink);
writeStyles(payload, offsets, shrink);
}
output.write(offsets.array());
output.write(baos.toByteArray());
if (!styles.isEmpty()) {
header.putInt(STYLE_START_OFFSET, getHeaderSize() + getOffsetSize() + stringOffset);
}
}
/**
* Represents all of the styles for a particular string. The string is determined by its index
* in {@link StringPoolChunk}.
*/
public static class StringPoolStyle implements SerializableResource {
// Styles are a list of integers with 0xFFFFFFFF serving as a sentinel value.
static final int RES_STRING_POOL_SPAN_END = 0xFFFFFFFF;
private final List<StringPoolSpan> spans;
static StringPoolStyle create(ByteBuffer buffer, int offset, StringPoolChunk parent) {
Builder<StringPoolSpan> spans = ImmutableList.builder();
int nameIndex = buffer.getInt(offset);
while (nameIndex != RES_STRING_POOL_SPAN_END) {
spans.add(StringPoolSpan.create(buffer, offset, parent));
offset += StringPoolSpan.SPAN_LENGTH;
nameIndex = buffer.getInt(offset);
}
return new StringPoolStyle(spans.build());
}
private StringPoolStyle(List<StringPoolSpan> spans) {
this.spans = spans;
}
@Override
public byte[] toByteArray() throws IOException {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
for (StringPoolSpan span : spans) {
byte[] encodedSpan = span.toByteArray(shrink);
if (encodedSpan.length != StringPoolSpan.SPAN_LENGTH) {
throw new IllegalStateException("Encountered a span of invalid length.");
}
payload.write(encodedSpan);
}
payload.writeInt(RES_STRING_POOL_SPAN_END);
}
return baos.toByteArray();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StringPoolStyle that = (StringPoolStyle)o;
return Objects.equals(spans, that.spans);
}
@Override
public int hashCode() {
return Objects.hash(spans);
}
/**
* Returns a brief description of the contents of this style. The representation of this
* information is subject to change, but below is a typical example:
*
* <pre>"StringPoolStyle{spans=[StringPoolSpan{foo, start=0, stop=5}, ...]}"</pre>
*/
@Override
public String toString() {
return String.format("StringPoolStyle{spans=%s}", spans);
}
}
/** Represents a styled span associated with a specific string. */
private static class StringPoolSpan implements SerializableResource {
static final int SPAN_LENGTH = 12;
private final int nameIndex;
private final int start;
private final int stop;
private final StringPoolChunk parent;
static StringPoolSpan create(ByteBuffer buffer, int offset, StringPoolChunk parent) {
int nameIndex = buffer.getInt(offset);
int start = buffer.getInt(offset + 4);
int stop = buffer.getInt(offset + 8);
return new StringPoolSpan(nameIndex, start, stop, parent);
}
private StringPoolSpan(int nameIndex, int start, int stop, StringPoolChunk parent) {
this.nameIndex = nameIndex;
this.start = start;
this.stop = stop;
this.parent = parent;
}
@Override
public final byte[] toByteArray() {
return toByteArray(false);
}
@Override
public final byte[] toByteArray(boolean shrink) {
ByteBuffer buffer = ByteBuffer.allocate(SPAN_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(nameIndex);
buffer.putInt(start);
buffer.putInt(stop);
return buffer.array();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StringPoolSpan that = (StringPoolSpan)o;
return nameIndex == that.nameIndex &&
start == that.start &&
stop == that.stop &&
Objects.equals(parent, that.parent);
}
@Override
public int hashCode() {
return Objects.hash(nameIndex, start, stop, parent);
}
/**
* Returns a brief description of this span. The representation of this information is subject
* to change, but below is a typical example:
*
* <pre>"StringPoolSpan{foo, start=0, stop=5}"</pre>
*/
@Override
public String toString() {
return String.format("StringPoolSpan{%s, start=%d, stop=%d}",
parent.getString(nameIndex), start, stop);
}
}
}
@@ -0,0 +1,449 @@
/*
* 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.common.base.Preconditions;
import com.google.common.io.LittleEndianDataOutputStream;
import com.google.common.primitives.UnsignedBytes;
import androidx.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
/**
* Represents a type chunk, which contains the resource values for a specific resource type and
* configuration in a {@link PackageChunk}. The resource values in this chunk correspond to
* the array of type strings in the enclosing {@link PackageChunk}.
*
* <p>A {@link PackageChunk} can have multiple of these chunks for different
* (configuration, resource type) combinations.
*/
public final class TypeChunk extends Chunk {
/** The type identifier of the resource type this chunk is holding. */
private final int id;
/** The number of resources of this type at creation time. */
private final int entryCount;
/** The offset (from {@code offset}) in the original buffer where {@code entries} start. */
private final int entriesStart;
/** The resource configuration that these resource entries correspond to. */
private BinaryResourceConfiguration configuration;
/** A sparse list of resource entries defined by this chunk. */
private final Map<Integer, Entry> entries = new TreeMap<>();
protected TypeChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
id = UnsignedBytes.toInt(buffer.get());
buffer.position(buffer.position() + 3); // Skip 3 bytes for packing
entryCount = buffer.getInt();
entriesStart = buffer.getInt();
configuration = BinaryResourceConfiguration.create(buffer);
}
@Override
protected void init(ByteBuffer buffer) {
int offset = this.offset + entriesStart;
for (int i = 0; i < entryCount; ++i) {
Entry entry = Entry.create(buffer, offset, this);
if (entry != null) {
entries.put(i, entry);
}
}
}
/** Returns the (1-based) type id of the resource types that this {@link TypeChunk} is holding. */
public int getId() {
return id;
}
/** Returns the name of the type this chunk represents (e.g. string, attr, id). */
public String getTypeName() {
PackageChunk packageChunk = getPackageChunk();
Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass());
StringPoolChunk typePool = packageChunk.getTypeStringPool();
Preconditions.checkNotNull(typePool, "%s's parent package has no type pool.", getClass());
return typePool.getString(getId() - 1); // - 1 here to convert to 0-based index
}
/** Returns the resource configuration that these resource entries correspond to. */
public BinaryResourceConfiguration getConfiguration() {
return configuration;
}
/**
* Sets the resource configuration that this chunk's entries correspond to.
*
* @param configuration The new configuration.
*/
public void setConfiguration(BinaryResourceConfiguration configuration) {
this.configuration = configuration;
}
/** Returns the total number of entries for this type + configuration, including null entries. */
public int getTotalEntryCount() {
return entryCount;
}
/** Returns a sparse list of 0-based indices to resource entries defined by this chunk. */
public Map<Integer, Entry> getEntries() {
return Collections.unmodifiableMap(entries);
}
/** Returns true if this chunk contains an entry for {@code resourceId}. */
public boolean containsResource(BinaryResourceIdentifier resourceId) {
PackageChunk packageChunk = Preconditions.checkNotNull(getPackageChunk());
int packageId = packageChunk.getId();
int typeId = getId();
return resourceId.packageId() == packageId
&& resourceId.typeId() == typeId
&& entries.containsKey(resourceId.entryId());
}
/**
* Overrides the entries in this chunk at the given index:entry pairs in {@code entries}.
* For example, if the current list of entries is {0: foo, 1: bar, 2: baz}, and {@code entries}
* is {1: qux, 3: quux}, then the entries will be changed to {0: foo, 1: qux, 2: baz}. If an entry
* has an index that does not exist in the dense entry list, then it is considered a no-op for
* that single entry.
*
* @param entries A sparse list containing index:entry pairs to override.
*/
public void overrideEntries(Map<Integer, Entry> entries) {
for (Map.Entry<Integer, Entry> entry : entries.entrySet()) {
int index = entry.getKey() != null ? entry.getKey() : -1;
overrideEntry(index, entry.getValue());
}
}
/**
* Overrides an entry at the given index. Passing null for the {@code entry} will remove that
* entry from {@code entries}. Indices < 0 or >= {@link #getTotalEntryCount()} are a no-op.
*
* @param index The 0-based index for the entry to override.
* @param entry The entry to override, or null if the entry should be removed at this location.
*/
public void overrideEntry(int index, @Nullable Entry entry) {
if (index >= 0 && index < entryCount) {
if (entry != null) {
entries.put(index, entry);
} else {
entries.remove(index);
}
}
}
protected String getString(int index) {
ResourceTableChunk resourceTable = getResourceTableChunk();
Preconditions.checkNotNull(resourceTable, "%s has no resource table.", getClass());
return resourceTable.getStringPool().getString(index);
}
protected String getKeyName(int index) {
PackageChunk packageChunk = getPackageChunk();
Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass());
StringPoolChunk keyPool = packageChunk.getKeyStringPool();
Preconditions.checkNotNull(keyPool, "%s's parent package has no key pool.", getClass());
return keyPool.getString(index);
}
protected void updateKey(int index, String value) {
PackageChunk packageChunk = getPackageChunk();
Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass());
StringPoolChunk keyPool = packageChunk.getKeyStringPool();
Preconditions.checkNotNull(keyPool, "%s's parent package has no key pool.", getClass());
keyPool.updateString(index, value);
}
@Nullable
private ResourceTableChunk getResourceTableChunk() {
Chunk chunk = getParent();
while (chunk != null && !(chunk instanceof ResourceTableChunk)) {
chunk = chunk.getParent();
}
return chunk != null ? (ResourceTableChunk) chunk : null;
}
/** Returns the package enclosing this chunk, if any. Else, returns null. */
@Nullable
public PackageChunk getPackageChunk() {
Chunk chunk = getParent();
while (chunk != null && !(chunk instanceof PackageChunk)) {
chunk = chunk.getParent();
}
return chunk != null ? (PackageChunk) chunk : null;
}
@Override
protected Type getType() {
return Type.TABLE_TYPE;
}
/** Returns the number of bytes needed for offsets based on {@code entries}. */
private int getOffsetSize() {
return entryCount * 4;
}
private int writeEntries(DataOutput payload, ByteBuffer offsets, boolean shrink)
throws IOException {
int entryOffset = 0;
for (int i = 0; i < entryCount; ++i) {
Entry entry = entries.get(i);
if (entry == null) {
offsets.putInt(Entry.NO_ENTRY);
} else {
byte[] encodedEntry = entry.toByteArray(shrink);
payload.write(encodedEntry);
offsets.putInt(entryOffset);
entryOffset += encodedEntry.length;
}
}
entryOffset = writePad(payload, entryOffset);
return entryOffset;
}
@Override
protected void writeHeader(ByteBuffer output) {
int entriesStart = getHeaderSize() + getOffsetSize();
output.putInt(id); // Write an unsigned byte with 3 bytes padding
output.putInt(entryCount);
output.putInt(entriesStart);
output.put(configuration.toByteArray(false));
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteBuffer offsets = ByteBuffer.allocate(getOffsetSize()).order(ByteOrder.LITTLE_ENDIAN);
try (LittleEndianDataOutputStream payload = new LittleEndianDataOutputStream(baos)) {
writeEntries(payload, offsets, shrink);
}
output.write(offsets.array());
output.write(baos.toByteArray());
}
/** An {@link Entry} in a {@link TypeChunk}. Contains one or more {@link BinaryResourceValue}. */
public static class Entry implements SerializableResource {
/** An entry offset that indicates that a given resource is not present. */
public static final int NO_ENTRY = 0xFFFFFFFF;
/** Set if this is a complex resource. Otherwise, it's a simple resource. */
private static final int FLAG_COMPLEX = 0x0001;
/** Size of a single resource id + value mapping entry. */
private static final int MAPPING_SIZE = 4 + BinaryResourceValue.SIZE;
private final int headerSize;
private final int flags;
private final int keyIndex;
private BinaryResourceValue value;
private Map<Integer, BinaryResourceValue> values;
private final int parentEntry;
private final TypeChunk parent;
private Entry(int headerSize,
int flags,
int keyIndex,
BinaryResourceValue value,
Map<Integer, BinaryResourceValue> values,
int parentEntry,
TypeChunk parent) {
this.headerSize = headerSize;
this.flags = flags;
this.keyIndex = keyIndex;
this.value = value;
this.values = values;
this.parentEntry = parentEntry;
this.parent = parent;
}
/** Number of bytes in the header of the {@link Entry}. */
public int headerSize() { return headerSize; }
/** Resource entry flags. */
public int flags() { return flags; }
/** Index into {@link PackageChunk#getKeyStringPool} identifying this entry. */
public int keyIndex() { return keyIndex; }
/** The value of this resource entry, if this is not a complex entry. Else, null. */
@Nullable
public BinaryResourceValue value() { return value; }
/**
* Update the value of this resource entry
* @param value
*/
public void updateValue(@Nullable BinaryResourceValue value){
this.value = value;
}
/** The extra values in this resource entry if this {@link #isComplex}. */
public Map<Integer, BinaryResourceValue> values() { return values; }
/** Update the value in this resource entry if this {@link #isComplex}. */
public void updateValue(int index, BinaryResourceValue value){
if (index >= 0) {
if (value != null) {
values.put(index, value);
} else {
values.remove(index);
}
}
}
/**
* Entry into {@link PackageChunk} that is the parent {@link Entry} to this entry.
* This value only makes sense when this is complex ({@link #isComplex} returns true).
*/
public int parentEntry() { return parentEntry; }
/** The {@link TypeChunk} that this resource entry belongs to. */
public TypeChunk parent() { return parent; }
/** Returns the name of the type this chunk represents (e.g. string, attr, id). */
public final String typeName() {
return parent().getTypeName();
}
/** The total number of bytes that this {@link Entry} takes up. */
public final int size() {
return headerSize() + (isComplex() ? values().size() * MAPPING_SIZE : BinaryResourceValue.SIZE);
}
/** Returns the key name identifying this resource entry. */
public final String key() {
return parent().getKeyName(keyIndex());
}
/** Update the key name in this resource entry. */
public final void updateKey(int keyIndex, String value) {
parent().updateKey(keyIndex, value);
}
/** Returns true if this is a complex resource. */
public final boolean isComplex() {
return (flags() & FLAG_COMPLEX) != 0;
}
/**
* Creates a new {@link Entry} whose contents start at the 0-based position in
* {@code buffer} given by a 4-byte value read from {@code buffer} and then added to
* {@code baseOffset}. If the value read from {@code buffer} is equal to {@link #NO_ENTRY}, then
* null is returned as there is no resource at that position.
*
* <p>Otherwise, this position is parsed and returned as an {@link Entry}.
*
* @param buffer A buffer positioned at an offset to an {@link Entry}.
* @param baseOffset Offset that must be added to the value at {@code buffer}'s position.
* @param parent The {@link TypeChunk} that this resource entry belongs to.
* @return New {@link Entry} or null if there is no resource at this location.
*/
@Nullable
public static Entry create(ByteBuffer buffer, int baseOffset, TypeChunk parent) {
int offset = buffer.getInt();
if (offset == NO_ENTRY) {
return null;
}
int position = buffer.position();
buffer.position(baseOffset + offset); // Set buffer position to resource entry start
Entry result = newInstance(buffer, parent);
buffer.position(position); // Restore buffer position
return result;
}
@Nullable
private static Entry newInstance(ByteBuffer buffer, TypeChunk parent) {
int headerSize = buffer.getShort() & 0xFFFF;
int flags = buffer.getShort() & 0xFFFF;
int keyIndex = buffer.getInt();
BinaryResourceValue value = null;
Map<Integer, BinaryResourceValue> values = new LinkedHashMap<>();
int parentEntry = 0;
if ((flags & FLAG_COMPLEX) != 0) {
parentEntry = buffer.getInt();
int valueCount = buffer.getInt();
for (int i = 0; i < valueCount; ++i) {
values.put(buffer.getInt(), BinaryResourceValue.create(buffer));
}
} else {
value = BinaryResourceValue.create(buffer);
}
return new Entry(headerSize, flags, keyIndex, value, values, parentEntry, parent);
}
@Override
public final byte[] toByteArray() {
return toByteArray(false);
}
@Override
public final byte[] toByteArray(boolean shrink) {
ByteBuffer buffer = ByteBuffer.allocate(size());
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short) headerSize());
buffer.putShort((short) flags());
buffer.putInt(keyIndex());
if (isComplex()) {
buffer.putInt(parentEntry());
buffer.putInt(values().size());
for (Map.Entry<Integer, BinaryResourceValue> entry : values().entrySet()) {
buffer.putInt(entry.getKey());
buffer.put(entry.getValue().toByteArray(shrink));
}
} else {
BinaryResourceValue value = value();
Preconditions.checkNotNull(value, "A non-complex TypeChunk entry must have a value.");
buffer.put(value.toByteArray());
}
return buffer.array();
}
@Override
public final String toString() {
return String.format("Entry{key=%s}", key());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entry entry = (Entry)o;
return headerSize == entry.headerSize &&
flags == entry.flags &&
keyIndex == entry.keyIndex &&
parentEntry == entry.parentEntry &&
Objects.equals(value, entry.value) &&
Objects.equals(values, entry.values) &&
Objects.equals(parent, entry.parent);
}
@Override
public int hashCode() {
return Objects.hash(headerSize, flags, keyIndex, value, values, parentEntry, parent);
}
}
}
@@ -0,0 +1,101 @@
/*
* 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.common.base.Preconditions;
import com.google.common.primitives.UnsignedBytes;
import androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
/** A chunk that contains a collection of resource entries for a particular resource data type. */
public final class TypeSpecChunk extends Chunk {
/** The id of the resource type that this type spec refers to. */
private final int id;
/** Resource configuration masks. */
private final int[] resources;
protected TypeSpecChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
id = UnsignedBytes.toInt(buffer.get());
buffer.position(buffer.position() + 3); // Skip 3 bytes for packing
int resourceCount = buffer.getInt();
resources = new int[resourceCount];
for (int i = 0; i < resourceCount; ++i) {
resources[i] = buffer.getInt();
}
}
/**
* Returns the (1-based) type id of the resources that this {@link TypeSpecChunk} has
* configuration masks for.
*/
public int getId() {
return id;
}
/** Returns the number of resource entries that this chunk has configuration masks for. */
public int getResourceCount() {
return resources.length;
}
@Override
protected Type getType() {
return Type.TABLE_TYPE_SPEC;
}
/** Returns the name of the type this chunk represents (e.g. string, attr, id). */
public String getTypeName() {
PackageChunk packageChunk = getPackageChunk();
Preconditions.checkNotNull(packageChunk, "%s has no parent package.", getClass());
StringPoolChunk typePool = packageChunk.getTypeStringPool();
Preconditions.checkNotNull(typePool, "%s's parent package has no type pool.", getClass());
return typePool.getString(getId() - 1); // - 1 here to convert to 0-based index
}
/** Returns the package enclosing this chunk, if any. Else, returns null. */
@Nullable
private PackageChunk getPackageChunk() {
Chunk chunk = getParent();
while (chunk != null && !(chunk instanceof PackageChunk)) {
chunk = chunk.getParent();
}
return chunk != null ? (PackageChunk) chunk : null;
}
@Override
protected void writeHeader(ByteBuffer output) {
super.writeHeader(output);
// id is an unsigned byte in the range [0-255]. It is guaranteed to be non-negative.
// Because our output is in little-endian, we are making use of the 4 byte packing here
output.putInt(id);
output.putInt(resources.length);
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
for (int resource : resources) {
output.writeInt(resource);
}
}
}
@@ -0,0 +1,61 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A chunk whose contents are unknown. This is a placeholder until we add a proper chunk for the
* unknown type.
*/
public final class UnknownChunk extends Chunk {
private final Type type;
private final byte[] header;
private final byte[] payload;
protected UnknownChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
type = Type.fromCode(buffer.getShort(offset));
header = new byte[headerSize - Chunk.METADATA_SIZE];
payload = new byte[chunkSize - headerSize];
buffer.get(header);
buffer.get(payload);
}
@Override
protected void writeHeader(ByteBuffer output) {
output.put(header);
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
output.write(payload);
}
@Override
protected Type getType() {
return type;
}
}
@@ -0,0 +1,148 @@
/*
* 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 java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
/** Represents an XML attribute and value. */
public class XmlAttribute implements SerializableResource {
/** The serialized size in bytes of an {@link XmlAttribute}. */
public static final int SIZE = 12 + BinaryResourceValue.SIZE;
private final int namespaceIndex;
private final int nameIndex;
private final int rawValueIndex;
private final BinaryResourceValue typedValue;
private final XmlNodeChunk parent;
/**
* Creates a new {@link XmlAttribute} based on the bytes at the current {@code buffer} position.
*
* @param buffer A buffer whose position is at the start of a {@link XmlAttribute}.
* @param parent The parent chunk that contains this attribute; used for string lookups.
*/
public static XmlAttribute create(ByteBuffer buffer, XmlNodeChunk parent) {
int namespace = buffer.getInt();
int name = buffer.getInt();
int rawValue = buffer.getInt();
BinaryResourceValue typedValue = BinaryResourceValue.create(buffer);
return new XmlAttribute(namespace, name, rawValue, typedValue, parent);
}
private XmlAttribute(int namespaceIndex,
int nameIndex,
int rawValueIndex,
BinaryResourceValue typedValue,
XmlNodeChunk parent) {
this.namespaceIndex = namespaceIndex;
this.nameIndex = nameIndex;
this.rawValueIndex = rawValueIndex;
this.typedValue = typedValue;
this.parent = parent;
}
/** A string reference to the namespace URI, or -1 if not present. */
public int namespaceIndex() {
return namespaceIndex;
}
/** A string reference to the attribute name. */
public int nameIndex() {
return nameIndex;
}
/** A string reference to a string containing the character value. */
public int rawValueIndex() {
return rawValueIndex;
}
/** A {@link BinaryResourceValue} instance containing the parsed value. */
public BinaryResourceValue typedValue() {
return typedValue;
}
/** The parent of this XML attribute; used for dereferencing the namespace and name. */
public XmlNodeChunk parent() {
return parent;
}
/** The namespace URI, or the empty string if not present. */
public final String namespace() {
return getString(namespaceIndex());
}
/** The attribute name, or the empty string if not present. */
public final String name() {
return getString(nameIndex());
}
/** The raw character value. */
public final String rawValue() {
return getString(rawValueIndex());
}
private String getString(int index) {
return parent().getString(index);
}
@Override
public byte[] toByteArray() {
return toByteArray(false);
}
@Override
public byte[] toByteArray(boolean shrink) {
ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(namespaceIndex());
buffer.putInt(nameIndex());
buffer.putInt(rawValueIndex());
buffer.put(typedValue().toByteArray(shrink));
return buffer.array();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
XmlAttribute that = (XmlAttribute)o;
return namespaceIndex == that.namespaceIndex &&
nameIndex == that.nameIndex &&
rawValueIndex == that.rawValueIndex &&
Objects.equals(typedValue, that.typedValue) &&
Objects.equals(parent, that.parent);
}
@Override
public int hashCode() {
return Objects.hash(namespaceIndex, nameIndex, rawValueIndex, typedValue, parent);
}
/**
* Returns a brief description of this XML attribute. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>"XmlAttribute{namespace=foo, name=bar, value=1234}"</pre>
*/
@Override
public String toString() {
return String.format("XmlAttribute{namespace=%s, name=%s, value=%s}",
namespace(), name(), rawValue());
}
}
@@ -0,0 +1,73 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
/** Represents an XML cdata node. */
public final class XmlCdataChunk extends XmlNodeChunk {
/** A string reference to a string containing the raw character data. */
private final int rawValue;
/** A {@link BinaryResourceValue} instance containing the parsed value. */
private final BinaryResourceValue binaryResourceValue;
protected XmlCdataChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
rawValue = buffer.getInt();
binaryResourceValue = BinaryResourceValue.create(buffer);
}
/** Returns a string containing the raw character data of this chunk. */
public String getRawValue() {
return getString(rawValue);
}
/** Returns a {@link BinaryResourceValue} instance containing the parsed cdata value. */
public BinaryResourceValue getResourceValue() {
return binaryResourceValue;
}
@Override
protected Type getType() {
return Type.XML_CDATA;
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
super.writePayload(output, header, shrink);
output.writeInt(rawValue);
output.write(binaryResourceValue.toByteArray());
}
/**
* Returns a brief description of this XML node. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>"XmlCdataChunk{line=1234, comment=My awesome comment., value=1234}"</pre>
*/
@Override
public String toString() {
return String.format("XmlCdataChunk{line=%d, comment=%s, value=%s}",
getLineNumber(), getComment(), getRawValue());
}
}
@@ -0,0 +1,48 @@
/*
* 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 androidx.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Represents an XML chunk structure.
*
* <p>An XML chunk can contain many nodes as well as a string pool which contains all of the strings
* referenced by the nodes.
*/
public final class XmlChunk extends ChunkWithChunks {
protected XmlChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
}
@Override
protected Type getType() {
return Type.XML;
}
/** Returns a string at the provided (0-based) index if the index exists in the string pool. */
public String getString(int index) {
for (Chunk chunk : getChunks().values()) {
if (chunk instanceof StringPoolChunk) {
return ((StringPoolChunk) chunk).getString(index);
}
}
throw new IllegalStateException("XmlChunk did not contain a string pool.");
}
}
@@ -0,0 +1,75 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
/** Represents the end of an XML node. */
public final class XmlEndElementChunk extends XmlNodeChunk {
/** A string reference to the namespace URI, or -1 if not present. */
private final int namespace;
/** A string reference to the attribute name. */
private final int name;
protected XmlEndElementChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
namespace = buffer.getInt();
name = buffer.getInt();
}
/** Returns the namespace URI, or the empty string if no namespace is present. */
public String getNamespace() {
return getString(namespace);
}
/** Returns the attribute name. */
public String getName() {
return getString(name);
}
@Override
protected Type getType() {
return Type.XML_END_ELEMENT;
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
super.writePayload(output, header, shrink);
output.writeInt(namespace);
output.writeInt(name);
}
/**
* Returns a brief description of this XML node. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>
* "XmlEndElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar}"
* </pre>
*/
@Override
public String toString() {
return String.format("XmlEndElementChunk{line=%d, comment=%s, namespace=%s, name=%s}",
getLineNumber(), getComment(), getNamespace(), getName());
}
}
@@ -0,0 +1,70 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
/** Represents the start/end of a namespace in an XML document. */
public abstract class XmlNamespaceChunk extends XmlNodeChunk {
/** A string reference to the namespace prefix. */
private final int prefix;
/** A string reference to the namespace URI. */
private final int uri;
protected XmlNamespaceChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
prefix = buffer.getInt();
uri = buffer.getInt();
}
/** Returns the namespace prefix. */
public String getPrefix() {
return getString(prefix);
}
/** Returns the namespace URI. */
public String getUri() {
return getString(uri);
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
super.writePayload(output, header, shrink);
output.writeInt(prefix);
output.writeInt(uri);
}
/**
* Returns a brief description of this namespace chunk. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>
* "XmlNamespaceChunk{line=1234, comment=My awesome comment., prefix=foo, uri=com.google.foo}"
* </pre>
*/
@Override
public String toString() {
return String.format("XmlNamespaceChunk{line=%d, comment=%s, prefix=%s, uri=%s}",
getLineNumber(), getComment(), getPrefix(), getUri());
}
}
@@ -0,0 +1,33 @@
/*
* 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 androidx.annotation.Nullable;
import java.nio.ByteBuffer;
/** Represents the ending tag of a namespace in an XML document. */
public final class XmlNamespaceEndChunk extends XmlNamespaceChunk {
protected XmlNamespaceEndChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
}
@Override
protected Type getType() {
return Type.XML_END_NAMESPACE;
}
}
@@ -0,0 +1,33 @@
/*
* 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 androidx.annotation.Nullable;
import java.nio.ByteBuffer;
/** Represents the starting tag of a namespace in an XML document. */
public final class XmlNamespaceStartChunk extends XmlNamespaceChunk {
protected XmlNamespaceStartChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
}
@Override
protected Type getType() {
return Type.XML_START_NAMESPACE;
}
}
@@ -0,0 +1,96 @@
/*
* 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 androidx.annotation.Nullable;
import java.nio.ByteBuffer;
/** The common superclass for the various types of XML nodes. */
public abstract class XmlNodeChunk extends Chunk {
/** The line number in the original source at which this node appeared. */
private final int lineNumber;
/** A string reference of this node's comment. If this is -1, then there is no comment. */
private final int comment;
protected XmlNodeChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
lineNumber = buffer.getInt();
comment = buffer.getInt();
}
/** Returns true if this XML node contains a comment. Else, returns false. */
public boolean hasComment() {
return comment != -1;
}
/** Returns the line number in the original source at which this node appeared. */
public int getLineNumber() {
return lineNumber;
}
/** Returns the comment associated with this node, if any. Else, returns the empty string. */
public String getComment() {
return getString(comment);
}
/**
* An {@link XmlNodeChunk} does not know by itself what strings its indices reference. In order
* to get the actual string, the first {@link XmlChunk} ancestor is found. The
* {@link XmlChunk} ancestor should have a string pool which {@code index} references.
*
* @param index The index of the string.
* @return String that the given {@code index} references, or empty string if {@code index} is -1.
*/
protected String getString(int index) {
if (index == -1) { // Special case. Packed XML files use -1 for "no string entry"
return "";
}
Chunk parent = getParent();
while (parent != null) {
if (parent instanceof XmlChunk) {
return ((XmlChunk) parent).getString(index);
}
parent = parent.getParent();
}
throw new IllegalStateException("XmlNodeChunk did not have an XmlChunk parent.");
}
/**
* An {@link XmlNodeChunk} and anything that is itself an {@link XmlNodeChunk} has a header size
* of 16. Anything else is, interestingly, considered to be a payload. For that reason, this
* method is final.
*/
@Override
protected final void writeHeader(ByteBuffer output) {
super.writeHeader(output);
output.putInt(lineNumber);
output.putInt(comment);
}
/**
* Returns a brief description of this XML node. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>"XmlNodeChunk{line=1234, comment=My awesome comment.}"</pre>
*/
@Override
public String toString() {
return String.format("XmlNodeChunk{line=%d, comment=%s}", getLineNumber(), getComment());
}
}
@@ -0,0 +1,86 @@
/*
* 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 androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Represents an XML resource map chunk.
*
* <p>This chunk maps attribute ids to the resource ids of the attribute resource that defines the
* attribute (e.g. type, enum values, etc.).
*/
public class XmlResourceMapChunk extends Chunk {
/** The size of a resource reference for {@code resources} in bytes. */
private static final int RESOURCE_SIZE = 4;
/**
* Contains a mapping of attributeID to resourceID. For example, the attributeID 2 refers to the
* resourceID returned by {@code resources.get(2)}.
*/
private final List<Integer> resources = new ArrayList<>();
protected XmlResourceMapChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
resources.addAll(enumerateResources(buffer));
}
private List<Integer> enumerateResources(ByteBuffer buffer) {
int resourceCount = (getOriginalChunkSize() - getHeaderSize()) / RESOURCE_SIZE;
List<Integer> result = new ArrayList<>(resourceCount);
int offset = this.offset + getHeaderSize();
buffer.mark();
buffer.position(offset);
for (int i = 0; i < resourceCount; ++i) {
result.add(buffer.getInt());
}
buffer.reset();
return result;
}
/** Returns the resource ID that this {@code attributeId} maps to. */
public BinaryResourceIdentifier getResourceId(int attributeId) {
return BinaryResourceIdentifier.create(resources.get(attributeId));
}
@Override
protected Type getType() {
return Type.XML_RESOURCE_MAP;
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
super.writePayload(output, header, shrink);
for (Integer resource : resources) {
output.writeInt(resource);
}
}
}
@@ -0,0 +1,145 @@
/*
* 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.common.base.Preconditions;
import androidx.annotation.Nullable;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Represents the beginning of an XML node. */
public final class XmlStartElementChunk extends XmlNodeChunk {
/** A string reference to the namespace URI, or -1 if not present. */
private final int namespace;
/** A string reference to the element name that this chunk represents. */
private final int name;
/** The offset to the start of the attributes payload. */
private final int attributeStart;
/** The number of attributes in the original buffer. */
private final int attributeCount;
/** The (0-based) index of the id attribute, or -1 if not present. */
private final int idIndex;
/** The (0-based) index of the class attribute, or -1 if not present. */
private final int classIndex;
/** The (0-based) index of the style attribute, or -1 if not present. */
private final int styleIndex;
/** The XML attributes associated with this element. */
private final List<XmlAttribute> attributes = new ArrayList<>();
protected XmlStartElementChunk(ByteBuffer buffer, @Nullable Chunk parent) {
super(buffer, parent);
namespace = buffer.getInt();
name = buffer.getInt();
attributeStart = (buffer.getShort() & 0xFFFF);
int attributeSize = (buffer.getShort() & 0xFFFF);
Preconditions.checkState(attributeSize == XmlAttribute.SIZE,
"attributeSize is wrong size. Got %s, want %s", attributeSize, XmlAttribute.SIZE);
attributeCount = (buffer.getShort() & 0xFFFF);
// The following indices are 1-based and need to be adjusted.
idIndex = (buffer.getShort() & 0xFFFF) - 1;
classIndex = (buffer.getShort() & 0xFFFF) - 1;
styleIndex = (buffer.getShort() & 0xFFFF) - 1;
}
@Override
protected void init(ByteBuffer buffer) {
super.init(buffer);
attributes.addAll(enumerateAttributes(buffer));
}
private List<XmlAttribute> enumerateAttributes(ByteBuffer buffer) {
List<XmlAttribute> result = new ArrayList<>(attributeCount);
int offset = this.offset + getHeaderSize() + attributeStart;
int endOffset = offset + XmlAttribute.SIZE * attributeCount;
buffer.mark();
buffer.position(offset);
while (offset < endOffset) {
result.add(XmlAttribute.create(buffer, this));
offset += XmlAttribute.SIZE;
}
buffer.reset();
return result;
}
/** Returns the namespace URI, or the empty string if not present. */
public String getNamespace() {
return getString(namespace);
}
/** Returns the element name that this chunk represents. */
public String getName() {
return getString(name);
}
/** Returns an unmodifiable list of this XML element's attributes. */
public List<XmlAttribute> getAttributes() {
return Collections.unmodifiableList(attributes);
}
@Override
protected Type getType() {
return Type.XML_START_ELEMENT;
}
@Override
protected void writePayload(DataOutput output, ByteBuffer header, boolean shrink)
throws IOException {
super.writePayload(output, header, shrink);
output.writeInt(namespace);
output.writeInt(name);
output.writeShort((short) XmlAttribute.SIZE); // attribute start
output.writeShort((short) XmlAttribute.SIZE);
output.writeShort((short) attributes.size());
output.writeShort((short) (idIndex + 1));
output.writeShort((short) (classIndex + 1));
output.writeShort((short) (styleIndex + 1));
for (XmlAttribute attribute : attributes) {
output.write(attribute.toByteArray(shrink));
}
}
/**
* Returns a brief description of this XML node. The representation of this information is
* subject to change, but below is a typical example:
*
* <pre>
* "XmlStartElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar, ...}"
* </pre>
*/
@Override
public String toString() {
return String.format(
"XmlStartElementChunk{line=%d, comment=%s, namespace=%s, name=%s, attributes=%s}",
getLineNumber(), getComment(), getNamespace(), getName(), attributes.toString());
}
}
@@ -0,0 +1,76 @@
package com.vk.api.audio;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.vk.api.base.ApiRequest;
import com.vk.dto.music.MusicTrack;
import com.vk.log.L;
import org.json.JSONObject;
import ru.vtosters.lite.music.Genius;
import ru.vtosters.lite.utils.Preferences;
import java.util.ArrayList;
public class AudioGetLyrics extends ApiRequest<AudioGetLyrics.a> {
private final MusicTrack musicTrack;
public AudioGetLyrics(int lyrics_id, String audio_id, MusicTrack mt) {
super("audio.getLyrics");
c("audio_id", audio_id);
musicTrack = mt;
}
/* renamed from: a */
public a a(@NonNull JSONObject jSONObject) {
var aVar = new a();
if (Preferences.getBoolValue("useGenius", false)) {
aVar.a = Genius.getTextMusic(musicTrack);
return aVar;
}
try {
var lines = new ArrayList<>();
var lyrics = jSONObject.getJSONObject("response").getJSONObject("lyrics");
if (lyrics.has("timestamps")) {
var timestamps = lyrics.getJSONArray("timestamps");
for (int i = 0; i < timestamps.length(); i++) {
var timestamp = timestamps.getJSONObject(i);
if (timestamp.has("line")) {
lines.add(timestamp.getString("line"));
}
if (timestamp.optString("interlude").length() > 0) {
lines.add("♪");
}
}
aVar.a = TextUtils.join("\n", lines);
} else if (lyrics.has("text")) {
var text = lyrics.getJSONArray("text");
for (int i = 0; i < text.length(); i++) {
lines.add(text.getString(i));
}
aVar.a = TextUtils.join("\n", lines);
} else {
L.e("vk", "No text found");
aVar.a = "No text found";
}
return aVar;
} catch (Exception e) {
L.e("vk", e);
aVar.a = "Ошибка получения текста" + "\n\n" + "Ошибка: \n" + e.getLocalizedMessage();
return aVar;
}
}
public static class a {
public String a;
}
}
@@ -0,0 +1,239 @@
package com.vk.im.engine.commands.dialogs;
import androidx.collection.ArraySet;
import b.h.g.p.Transliteration;
import com.vk.auth.y.a.BuildConfig;
import com.vk.im.engine.ImEnvironment;
import com.vk.im.engine.commands.etc.ProfilesGetCmd;
import com.vk.im.engine.commands.etc.ProfilesInfoGetArgs;
import com.vk.im.engine.i.BaseImEngineCmd;
import com.vk.im.engine.internal.storage.utils.StringMatchStrategy;
import com.vk.im.engine.models.*;
import com.vk.im.engine.models.dialogs.DialogMember;
import com.vk.im.engine.utils.ImDialogsUtils;
import kotlin.NoWhenBranchMatchedException;
import kotlin.collections.l;
import kotlin.jvm.b.Functions2;
import kotlin.jvm.internal.Intrinsics;
import kotlin.sequences.m;
import kotlin.text.Regex;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
public final class DialogGetMentionSuggestionCmd extends BaseImEngineCmd<Suggestion> {
private final int f2365b;
private final String c;
private final Source d;
private final boolean e;
private final Object f;
public static final class a<T> implements Comparator<Member> {
List a;
a(List list) {
this.a = list;
}
@Override // java.util.Comparator
/* renamed from: a */
public int compare(Member member, Member member2) {
int indexOf = this.a.indexOf(member);
int indexOf2 = this.a.indexOf(member2);
int i = indexOf;
if (indexOf < 0) {
i = Integer.MAX_VALUE;
}
int i2 = indexOf2;
if (indexOf2 < 0) {
i2 = Integer.MAX_VALUE;
}
return Intrinsics.a(i, i2);
}
}
public DialogGetMentionSuggestionCmd(int i, String str, Source source, boolean z, Object obj) {
this.f2365b = i;
this.c = str;
this.d = source;
this.e = z;
this.f = obj;
}
private ProfilesInfo a(ImEnvironment imEnvironment, Collection<Member> collection, Source source, boolean z, Object obj) {
ProfilesInfoGetArgs.a aVar = new ProfilesInfoGetArgs.a();
aVar.a(collection);
aVar.a(source);
aVar.a(z);
aVar.a(obj);
Object a2 = null;
try {
a2 = imEnvironment.a(this, new ProfilesGetCmd(aVar.a()));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
Intrinsics.a(a2, "env.submitCommandDirect(this, cmd)");
return (ProfilesInfo) a2;
}
private String a(String str) {
String a2 = new Regex("\\W*").a(str, BuildConfig.g);
int length = a2.length() - 1;
int i = 0;
boolean z = false;
while (i <= length) {
boolean z2 = a2.charAt(!z ? i : length) <= ' ';
if (z) {
if (!z2) {
break;
}
length--;
} else if (z2) {
i++;
} else {
z = true;
}
}
return a2.subSequence(i, length + 1).toString();
}
private Collection<Member> a(ImEnvironment imEnvironment, int i) {
var d = imEnvironment.a0().f().b().d(i);
var arrayList = new ArrayList(l.a(d, 10));
for (DialogMember dialogMember : d) {
arrayList.add(dialogMember.Z());
}
// changed
var fakeMember = new Member();
fakeMember.b(MemberType.CUSTOM_BOT);
arrayList.add(fakeMember);
return arrayList;
}
private Collection<Member> a(ImEnvironment imEnvironment, int i, String str) {
if (a(str).length() == 0) {
return l.a();
}
List<String> c = l.c(Transliteration.a.a(str), Transliteration.a.b(str));
ArraySet arraySet = new ArraySet();
for (String str2 : c) {
arraySet.addAll(imEnvironment.a0().m().a(i, str2, StringMatchStrategy.STARTING_WITH));
}
// changed
var fakeMember = new Member();
fakeMember.b(MemberType.CUSTOM_BOT);
arraySet.add(fakeMember);
return arraySet;
}
private Comparator<Member> a(List<Member> list) {
return new a(list);
}
private void a(ImEnvironment imEnvironment, int i, Source source, boolean z, Object obj) {
try {
imEnvironment.a(this, new DialogGetMembersCmd(i, source, z, obj));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private List<Member> b(ImEnvironment imEnvironment, int i) {
return imEnvironment.a0().j().a(i, Weight.d.c(), Direction.BEFORE, 100);
}
@Override // com.vk.im.engine.i.ImEngineCmd
public Suggestion a(ImEnvironment imEnvironment) {
Collection<Member> a2;
Source source;
if (ImDialogsUtils.d(this.f2365b) != PeerType.CHAT) {
return new Suggestion((EntityValue) null, (ProfilesInfo) null);
}
Member Z = imEnvironment.Z();
int d = imEnvironment.a0().n().d();
Integer e = imEnvironment.a0().f().b().e(this.f2365b);
boolean z = false;
boolean z2 = e == null;
boolean z3 = e == null || e.intValue() != d;
boolean z4 = z2 || z3;
Source source2 = this.d;
boolean z5 = source2 == Source.NETWORK || (z4 && source2 == Source.ACTUAL);
if (z5) {
a(imEnvironment, this.f2365b, this.d, this.e, this.f);
z3 = false;
z2 = false;
}
List<Member> b2 = b(imEnvironment, this.f2365b);
if (this.c.length() == 0) {
z = true;
}
if (z) {
a2 = a(imEnvironment, this.f2365b);
} else if (z) {
throw new NoWhenBranchMatchedException();
} else {
a2 = a(imEnvironment, this.f2365b, this.c);
}
var sequense = l.d(a2);
var l = m.l(m.a(m.b(sequense, new Functions2<Member, Boolean>() {
@Override
public Boolean invoke(Member member) {
return member.e(Z) && (member.a(MemberType.USER) || member.a(MemberType.GROUP));
}
}), a(b2)));
EntityValue entityValue = new EntityValue(z2 ? null : l, z3);
switch (this.d)
{
case CACHE:
source = Source.CACHE;
break;
case ACTUAL:
source = Source.ACTUAL;
break;
case NETWORK:
source = Source.NETWORK;
break;
default:
throw new NoWhenBranchMatchedException();
}
return new Suggestion(entityValue, a(imEnvironment, l, source, this.e, this.f));
}
public boolean equals(Object obj) {
if (this != obj) {
if (obj instanceof DialogGetMentionSuggestionCmd) {
DialogGetMentionSuggestionCmd dialogGetMentionSuggestionCmd = (DialogGetMentionSuggestionCmd) obj;
return this.f2365b == dialogGetMentionSuggestionCmd.f2365b && this.e == dialogGetMentionSuggestionCmd.e;
}
return false;
}
return true;
}
public int hashCode() {
int i = this.f2365b;
String str = this.c;
int i2 = 0;
int hashCode = str != null ? str.hashCode() : 0;
Source source = this.d;
int hashCode2 = source != null ? source.hashCode() : 0;
boolean z = this.e;
int i3 = z ? 1 : 0;
if (z) {
i3 = 1;
}
Object obj = this.f;
if (obj != null) {
i2 = obj.hashCode();
}
return (((((((i * 31) + hashCode) * 31) + hashCode2) * 31) + i3) * 31) + i2;
}
public String toString() {
return "DialogGetMentionSuggestionCmd(dialogId=" + this.f2365b + ", query=" + this.c + ", source=" + this.d + ", isAwaitNetwork=" + this.e + ", changerTag=" + this.f + ")";
}
}
@@ -0,0 +1,32 @@
package com.vk.im.engine.models;
public enum MemberType {
UNKNOWN(0),
USER(1),
GROUP(2),
EMAIL(3),
CONTACT(4),
CUSTOM_BOT(5);
private final int mTypeAsInt;
MemberType(int i) {
this.mTypeAsInt = i;
}
public static MemberType a(int i) {
switch (i) {
case 0: return UNKNOWN;
case 1: return USER;
case 2: return GROUP;
case 3: return EMAIL;
case 4: return CONTACT;
case 5: return CUSTOM_BOT;
}
throw new IllegalArgumentException("Unknown member type value: " + i);
}
public int a() {
return this.mTypeAsInt;
}
}
@@ -0,0 +1,166 @@
package com.vk.im.ui.q.h.e;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.TextView;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.recyclerview.widget.RecyclerView;
import com.vk.core.ui.VkBottomSheetBehavior;
import com.vk.core.util.ContextExtKt;
import com.vk.core.util.Screen;
import com.vk.extensions.ViewExtKt;
import com.vk.im.engine.ImEngine;
import com.vk.im.engine.models.Member;
import com.vk.im.engine.models.ProfilesInfo;
import com.vk.im.engine.models.Suggestion;
import com.vk.im.ui.components.dialog_mention.DialogMentionComponent;
import com.vk.im.ui.components.dialog_mention.DialogMentionComponentCallback;
import com.vk.im.ui.formatters.MentionNameFormatter;
import com.vk.im.ui.utils.j.MentionSuggestionsSupplier;
import com.vk.im.ui.utils.j.MentionsManager;
import com.vk.im.ui.views.RichEditText;
import com.vtosters.lite.R;
public final class MentionsController {
private final RichEditText a;
private boolean state;
private CoordinatorLayout mCoordinatorLayout;
private VkBottomSheetBehavior<RecyclerView> mBottomSheetBehavior;
private boolean e;
private DialogMentionComponent mDialogMentionComponent;
private MentionsManager mMentionsManager;
private final ImEngine mImEngine;
private int i;
private final View j;
private final b k;
public interface b {
boolean c();
boolean d();
}
public MentionsController(ImEngine imEngine, int i, View view, b bVar) {
this.mImEngine = imEngine;
this.i = i;
this.j = view;
this.k = bVar;
this.a = (RichEditText) this.j.findViewById(R.id.writebar_edit);
this.mMentionsManager = new MentionsManager(this.a, new MentionSuggestionsSupplier() {
@Override
public boolean a() {
if(MentionsController.this.state)
return ViewExtKt.i(MentionsController.this.mCoordinatorLayout);
return false;
}
@Override
public boolean a(String str) {
if (MentionsController.this.c().c())
return false;
MentionsController.this.mDialogMentionComponent.a(MentionsController.this.d(), str);
MentionsController.this.e = true;
MentionsController.this.j();
return true;
}
@Override
public void b() {
MentionsController.this.mDialogMentionComponent.a(0, (String) null);
MentionsController.this.g();
}
});
this.mDialogMentionComponent = new DialogMentionComponent(this.mImEngine);
this.mDialogMentionComponent.a(new DialogMentionComponentCallback() {
@Override
public void a(Member member, ProfilesInfo profilesInfo) {
MentionsController.this.mMentionsManager.a(member, MentionNameFormatter.a.a(member, profilesInfo));
}
@Override
public void a(Suggestion suggestion) {
if (suggestion.b().b() == null || suggestion.b().b().isEmpty())
MentionsController.this.g();
else
MentionsController.this.i();
}
});
}
public final void g() {
this.e = false;
j();
}
private final void h() {
if (this.state) {
return;
}
View inflate = ((ViewStub) this.j.findViewById(R.id.mentions_container_stub)).inflate();
ViewGroup viewGroup = (ViewGroup) inflate.findViewById(R.id.mentions_container);
viewGroup.addView(this.mDialogMentionComponent.a(viewGroup, (Bundle) null));
this.mCoordinatorLayout = inflate.findViewById(R.id.mentions_cl);;
this.mBottomSheetBehavior = (VkBottomSheetBehavior<RecyclerView>) ((CoordinatorLayout.LayoutParams) viewGroup.getLayoutParams()).getBehavior();
this.mBottomSheetBehavior.a(new VkBottomSheetBehavior.b() {
@Override
public void a(View view, int i) {
if (i == 5) {
MentionsController.this.g();
}
}
});
this.mBottomSheetBehavior.b(true);
this.mBottomSheetBehavior.g(4);
this.mBottomSheetBehavior.e(Screen.a(160));
this.state = true;
}
public final void i() {
this.e = true;
j();
}
public final void j() {
if (this.k.d()) {
Context context = this.j.getContext();
Activity f = ContextExtKt.f(context);
boolean z = this.e && (Screen.k(f) || Screen.l(f));
if (z || this.state) {
if (!this.state) h();
this.mCoordinatorLayout.setVisibility(z ? View.VISIBLE : View.GONE);
if (z) return;
this.mCoordinatorLayout.postDelayed(() -> this.mBottomSheetBehavior.b(4), 300L);
}
}
}
public final void a() {
this.mDialogMentionComponent.g();
}
public final void b() {
this.mDialogMentionComponent.h();
}
public final b c() {
return this.k;
}
public final int d() {
return this.i;
}
public final void e() {
this.mDialogMentionComponent.q();
}
public final void f() {
this.mDialogMentionComponent.r();
}
}
@@ -0,0 +1,165 @@
package com.vk.music.view.x;
import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.vk.api.audio.AudioGetLyrics;
import com.vk.api.base.ApiCallback;
import com.vk.api.sdk.exceptions.VKApiExecutionException;
import com.vk.common.k.PodcastTimeCodeClickListener;
import com.vk.common.links.LinkParser;
import com.vk.core.util.ContextExtKt;
import com.vk.dto.music.Artist;
import com.vk.dto.music.MusicTrack;
import com.vk.music.artists.chooser.MusicArtistSelector;
import com.vk.music.common.MusicPlaybackLaunchContext;
import com.vk.music.dto.PlayerInfo;
import com.vk.music.player.PlayerModel;
import com.vk.music.player.TrackInfo;
import com.vk.profile.ui.BaseProfileFragment;
import com.vtosters.lite.LinkParserParams;
import com.vtosters.lite.R;
import com.vtosters.lite.ViewUtils;
import com.vtosters.lite.ui.MusicErrorViewHelper;
import com.vtosters.lite.ui.holder.RecyclerHolder;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/* loaded from: classes3.dex */
public class LyricsHolder extends RecyclerHolder<PlayerInfo> implements View.OnClickListener {
private final AtomicBoolean B;
private final MusicPlaybackLaunchContext C;
private final PodcastTimeCodeClickListener D;
private final LinkParserParams E;
private final TextView f18325c;
private final TextView f18326d;
private final TextView f18327e;
private final MusicErrorViewHelper f18328f;
private final View g;
private final View h;
public LyricsHolder(ViewGroup viewGroup, PlayerModel playerModel) {
super(R.layout.music_player_fr_lyrics, viewGroup);
this.B = new AtomicBoolean(false);
this.C = MusicPlaybackLaunchContext.p0.h(128);
this.E = new LinkParserParams();
this.f18325c = i(R.id.title);
this.f18326d = i(R.id.artist);
this.f18326d.setOnClickListener(this);
this.f18327e = i(R.id.lyrics);
this.g = i(R.id.progress);
this.h = i(R.id.content);
this.f18328f = new MusicErrorViewHelper.b(i(R.id.error)).a();
this.f18328f.a(new a());
this.f18328f.b(8);
this.D = new PodcastTimeCodeClickListener(viewGroup.getContext(), playerModel);
this.E.b(907);
this.E.a(this.D);
}
public void g0() {
TrackInfo trackInfo;
if (this.B.get() || (trackInfo = this.b.a) == null || !trackInfo.n() || trackInfo.e().G == 0) {
return;
}
MusicTrack e2 = trackInfo.e();
if (e2.G <= 0 || !TextUtils.isEmpty(e2.H)) {
return;
}
this.B.set(true);
this.f18328f.b(8);
ViewUtils.a(this.g, 0);
ViewUtils.a(this.h, 8);
new AudioGetLyrics(e2.G, e2.y1(), e2).a(new b(e2)).a();
}
@Override // android.view.View.OnClickListener
public void onClick(View view) {
Object obj = this.b;
MusicTrack e = ((PlayerInfo) obj).a != null ? ((PlayerInfo) obj).a.e() : null;
Activity e2 = ContextExtKt.e(view.getContext());
if (e == null || e2 == null) {
return;
}
if (e.D1()) {
new BaseProfileFragment.z(e.e).a(e2);
} else {
MusicArtistSelector.a(e2, e, this.b.d);
}
}
@Override // com.vtosters.lite.ui.holder.RecyclerHolder
public void b(PlayerInfo playerInfo) {
CharSequence charSequence;
TrackInfo trackInfo = playerInfo.a;
if (trackInfo == null || !trackInfo.n() || trackInfo.e().G == 0) {
return;
}
MusicTrack e2 = trackInfo.e();
this.f18325c.setText(e2.f);
this.f18325c.setSelected(true);
this.f18326d.setText(e2.C);
if (e2.D1()) {
this.E.a(e2.h);
this.D.a(e2, this.C);
charSequence = LinkParser.a(e2.H, this.E);
} else {
charSequence = e2.H;
}
if (!Objects.equals(this.f18327e.getText(), charSequence)) {
this.h.setScrollY(0);
}
this.f18327e.setText(charSequence);
g0();
}
class a implements View.OnClickListener {
a() {
}
@Override // android.view.View.OnClickListener
public void onClick(View view) {
LyricsHolder.this.g0();
}
}
public class b implements ApiCallback<AudioGetLyrics.a> {
final MusicTrack a;
b(MusicTrack musicTrack) {
this.a = musicTrack;
}
@Override // com.vk.api.base.ApiCallback
public void a(AudioGetLyrics.a aVar) {
this.a.H = aVar.a;
LyricsHolder.this.f18328f.b(8);
ViewUtils.a(LyricsHolder.this.g, 8);
ViewUtils.a(LyricsHolder.this.h, 0);
LyricsHolder.this.f0();
LyricsHolder.this.B.set(false);
}
@Override // com.vk.api.base.ApiCallback
public void a(VKApiExecutionException vKApiExecutionException) {
LyricsHolder.this.B.set(false);
LyricsHolder.this.f18328f.a(vKApiExecutionException);
LyricsHolder.this.f18328f.b(0);
ViewUtils.a(LyricsHolder.this.g, 8);
ViewUtils.a(LyricsHolder.this.h, 8);
}
}
}
@@ -1,6 +1,7 @@
package ru.vtosters.lite.deviceinfo;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.ThemesUtils;
import static ru.vtosters.lite.deviceinfo.OEMDetector.hasMiuiIncrCode;
import static ru.vtosters.lite.deviceinfo.OEMDetector.isEMUI;
@@ -91,8 +92,9 @@ public class Device {
public String toDeviceName() {
return "Device information: "
+ "commit='" + getBuildNumber()
+ "', sdkVersion=" + this.sdkVersion
+ ", productName='" + this.productName
+ "', isMilkshake='" + ThemesUtils.isMilkshake()
+ "', sdkVersion='" + this.sdkVersion
+ "', productName='" + this.productName
+ "', deviceName='" + this.deviceName
+ "', boardName='" + this.boardName
+ "', manufacturerName='" + this.manufacturerName
@@ -113,7 +115,7 @@ public class Device {
+ "- Board: " + this.boardName + "\n"
+ "- Manufacturer: " + this.manufacturerName + "\n"
+ "- Brand: " + this.brandName + "\n"
+ "- Model: " + this.modelName
+ "- Model: " + this.modelName + "\n"
+ (isMIUI() ? "- Miui Version Code: " + this.MiuiUiVersionCode + "\n" + "- Miui Version Name: " + this.MiuiUiVersionName + "\n" : "")
+ (isSamsung() ? "- OneUi Major Version: " + this.OneUiMajorVersion + "\n" + "- OneUi Minor Version: " + this.OneUiMinorVersion + "\n" : "")
+ (hasMiuiIncrCode() ? "- Miui Version Incremental Code: " + this.MiuiIncrementalCode + "\n" : "")
@@ -23,8 +23,8 @@ public class OEMDetector {
static {
try {
getPropMtd = ReflectionUtils.getDeclaredMethod("android.os.SystemProperties", "get", String.class);
semPlatformIntField = ReflectionUtils.getDeclaredField(Build.VERSION.class, "SEM_PLATFORM_INT");
getPropMtd = ReflectionUtils.findMethod(Class.forName("android.os.SystemProperties"), "get", String.class);
semPlatformIntField = ReflectionUtils.findField(Build.VERSION.class, "SEM_PLATFORM_INT");
} catch (NoSuchFieldException | ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
}
@@ -81,7 +81,7 @@ public class DNRInjector {
}
public static LinkedHashMap<DialogAction, Integer> injectToHashMap(LinkedHashMap<DialogAction, Integer> hashMap) {
hashMap.put(DialogAction.STAT, AndroidUtils.getIdentifier("dialogstats", "string"));
// hashMap.put(DialogAction.STAT, AndroidUtils.getIdentifier("dialogstats", "string"));
hashMap.put(DialogAction.DOWNLOAD, R.string.download_dl);
hashMap.put(DialogAction.DNR_ON, R.string.DNR_ON);
@@ -101,7 +101,7 @@ public class DNRInjector {
public static List<Object> injectToList(List<Object> actions) {
var list = new ArrayList<>(actions);
list.add(new DialogActionsListView.b.a(DialogAction.STAT, 1, R.attr.im_ic_stats, R.string.dialogstats)); // DialogAction, Int, Icon, String
// list.add(new DialogActionsListView.b.a(DialogAction.STAT, 1, R.attr.im_ic_stats, R.string.dialogstats)); // DialogAction, Int, Icon, String
list.add(new DialogActionsListView.b.a(DialogAction.DOWNLOAD, 1, R.attr.im_ic_msgdl, R.string.download_dl)); // DialogAction, Int, Icon, String
list.add(new DialogActionsListView.b.a(DialogAction.DNR_ON, 2, R.attr.im_ic_pinned_msg_hide, R.string.DNR_ON)); // DialogAction, Int, Icon, String
@@ -89,9 +89,8 @@ public class DockBarInjector {
for (DockBarTab tab : sManager.getSelectedTabs()) {
MenuItem add = menu.add(0, tab.id, 0, tab.titleID);
Drawable drawable = getResources().getDrawable(tab.iconID);
add.setIcon(new RecoloredDrawable(drawable, getCSTDock()));
add.setIcon(new RecoloredDrawable(getResources().getDrawable(tab.iconID), getCSTDock()));
add.setTitle(getGlobalContext().getString(tab.titleID));
add.setCheckable(true);
}
@@ -0,0 +1,103 @@
package ru.vtosters.lite.music;
import android.text.TextUtils;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import java.io.IOException;
public class Genius {
private static final String KEY = "Bearer ZTejoT_ojOEasIkT9WrMBhBQOz6eYKK5QULCMECmOhvwqjRZ6WbpamFe3geHnvp3";
private static final String URL = "https://api.genius.com";
private static final OkHttpClient client = VtOkHttpClient.getInstance();
public static String getTextMusic(MusicTrack musictrack) {
var uid = musictrack.y1();
var artist = musictrack.C;
var title = musictrack.f;
var duration = musictrack.h;
if (TextUtils.isEmpty(uid) || TextUtils.isEmpty(artist) || TextUtils.isEmpty(title) || duration == 0) {
Log.d("Genius", "grabTrackInfo: " + "Empty track, info: " + artist + " - " + title + " - " + duration + " - " + uid);
return "No text found";
}
return searchAudios(title, artist);
}
private static String searchAudios(String track, String artist) {
if (TextUtils.isEmpty(track) || TextUtils.isEmpty(artist)) {
Log.d("Genius", "No track/artist");
return null;
}
Log.d("Genius", "/search/multi?q=" + artist + "%20" + track + "&from_background=0");
var request = new Request.a()
.b(URL + "/search/multi?q=" + artist + "%20" + track + "&from_background=0")
.a("User-Agent", "Genius/5.14.0 (Android; Android 13; Google Pixel 4 XL)")
.a("Authorization", KEY)
.a("x-genius-app-background-request", "0")
.a("x-genius-logged-out", "true")
.a("host", "api.genius.com")
.a("Content-Type", "application/x-www-form-urlencoded")
.b()
.a();
try {
var payload = client.a(request).execute().a().g();
var sections = new JSONObject(payload).getJSONObject("response").getJSONArray("sections");
Log.d("Genius", payload);
for (int i = 0; i < sections.length(); i++) {
var json = sections.optJSONObject(i);
if (json.optString("type").equals("song")) {
var path = json.getJSONArray("hits").getJSONObject(0).getJSONObject("result").getString("api_path");
Log.d("Genius", path);
return getText(path);
}
}
return "Ошибка получения текста, возможно текст не найден";
} catch (JSONException | IOException e) {
return "Ошибка получения текста, возможно текст не найден" + "\n\n" + "Ошибка: \n" + e.getLocalizedMessage();
}
}
private static String getText(String track) {
if (TextUtils.isEmpty(track)) {
Log.d("Genius", "No track");
return null;
}
var request = new Request.a()
.b(URL + track + "?text_format=plain")
.a("User-Agent", "Genius/5.14.0 (Android; Android 13; Google Pixel 4 XL)")
.a("Authorization", KEY)
.a("x-genius-app-background-request", "0")
.a("x-genius-logged-out", "true")
.a("host", "api.genius.com")
.a("Content-Type", "application/x-www-form-urlencoded")
.b()
.a();
try {
var payload = new JSONObject(client.a(request).execute().a().g());
Log.d("Genius", String.valueOf(payload));
if (!payload.has("response")) {
return "Ошибка получения текста, возможно текст не найден";
}
return " " + payload.getJSONObject("response").getJSONObject("song").getJSONObject("lyrics").getString("plain");
} catch (JSONException | IOException e) {
return "Ошибка получения текста, возможно текст не найден" + "\n\n" + "Ошибка: \n" + e.getLocalizedMessage();
}
}
}
@@ -82,7 +82,7 @@ public class M3UDownloader implements ITrackDownloader {
String key = IOUtils.readAllLines(IOUtils.openStream(ts.getKeyURL()));
content = IOUtils.decodeStream(is, key);
} else {
content = IOUtils.readAllBytes(is);
content = IOUtils.readFully(is);
}
File tsDump = new File(tsesDir, ts.getName());
IOUtils.writeToFile(tsDump, content);
@@ -36,7 +36,7 @@ public class TSMerger {
for (var file : tsesList) {
if (file.getName().endsWith(".ts")) {
try {
mergedByteArrayStream.write(IOUtils.readAllBytes(file));
mergedByteArrayStream.write(IOUtils.readFully(file));
} catch (IOException e) {
e.printStackTrace();
return false;
@@ -41,7 +41,7 @@ public class ThumbnailDownloader {
private static void downloadThumbnail(String url, int res, String trackId)
throws IOException {
var data = IOUtils.readAllBytes(new URL(url).openStream());
var data = IOUtils.readFully(new URL(url).openStream());
IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data);
}
}
@@ -20,7 +20,7 @@ public class ColorReferences {
Color.parseColor("#5c9ce6"),
Color.parseColor("#518bcc"),
Color.parseColor("#74a2d6")
);
);
public static List<Integer> mutedAccentColors = Arrays.asList(
Color.parseColor("#663f8ae0"),
@@ -4,9 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import androidx.annotation.IdRes;
import com.vtosters.lite.R;
import ru.vtosters.lite.utils.ThemesUtils;
@@ -94,7 +92,7 @@ public class ThemesCore {
themedColors.put(R.attr.accentColor, accentColor);
themedColors.put(R.attr.text_name, accentColor);
themedColors.put(R.attr.tabbar_active_icon, accentColor);
// themedColors.put(com.vtosters.lite.R.attr.header_background, accentColor);
// themedColors.put(com.vtosters.lite.R.attr.header_alternate_background, accentColor);
// themedColors.put(com.vtosters.lite.R.attr.header_background_before_blur, accentColor);
@@ -168,23 +166,28 @@ public class ThemesCore {
// } catch (Exception e) {
// }
if (isCachedAccents()) {
if (ThemesUtils.isDarkTheme()) {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_DARK.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_DARK.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_DARK.get(attrID, true)));
if (ThemesUtils.isCustomAccentEnabled()) {
if (isCachedAccents()) {
if (ThemesUtils.isDarkTheme()) {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_DARK.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_DARK.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_DARK.get(attrID, true)));
} else {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_LIGHT.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_LIGHT.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_LIGHT.get(attrID, true)));
}
} else {
return (themedColors.get(attrID) != 0 && ACCENT_THEME_ONLY_LIGHT.get(attrID, true) && (
ThemesUtils.isMilkshake() ?
ACCENT_THEME_ONLY_MILK_LIGHT.get(attrID, true) :
ACCENT_THEME_ONLY_NOMILK_LIGHT.get(attrID, true)));
return themedColors.get(attrID) != 0;
}
} else {
return themedColors.get(attrID) != 0;
return false;
}
}
public static void clear() {
cachedAccents = false;
themedColors.clear();
@@ -12,26 +12,6 @@ import ru.vtosters.lite.themes.utils.RecolorUtils;
import ru.vtosters.lite.utils.ThemesUtils;
public class ThemesHacks {
// Фиксит селектор (все/свои/архив) в профиле
public static void fixProfileSelector(View view) {
if (!ThemesUtils.isMilkshake()) return;
var id = view.getId();
if (id == R.id.profile_wall_owner_posts || id == R.id.profile_wall_all_posts || id == R.id.profile_wall_archived_posts) {
StateListDrawable states = (StateListDrawable) ((LayerDrawable) view.getBackground()).getDrawable(0);
LayerDrawable selected = (LayerDrawable) ((DrawableContainer.DrawableContainerState) states.getConstantState()).getChildren()[0];
((ColorDrawable) selected.getDrawable(0)).setColor(ThemesUtils.getAccentColor());
}
}
public static void fixThemedFeed(TextView textView, int i) {
if (i != R.layout.themed_feed_tab_2) {
return;
}
textView.setTextColor(RecolorUtils.recolorCSL(textView.getTextColors()));
((InsetDrawable) ((DrawableContainer.DrawableContainerState) textView.getBackground().getConstantState()).getChildren()[0]).getDrawable().setTint(ThemesUtils.lighten(ThemesUtils.getAccentColor(), 0.15f));
}
public static void fixDropdown(Drawable drawable) {
if (!ThemesUtils.isDarkTheme() && ThemesUtils.isMilkshake() && ThemesCore.isCachedAccents()) {
StateListDrawable stateListDrawable = (StateListDrawable) drawable;
@@ -41,12 +21,15 @@ public class ThemesHacks {
}
public static int getHackedColor(@NonNull Context context, @ColorRes int color) {
if (ThemesCore.isCachedAccents() && ColorReferences.isAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getAccentColor();
}
if (ThemesCore.isCachedAccents() && ColorReferences.isMutedAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getMutedAccentColor();
if (ThemesUtils.isCustomAccentEnabled()) {
if (ThemesCore.isCachedAccents() && ColorReferences.isAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getAccentColor();
}
if (ThemesCore.isCachedAccents() && ColorReferences.isMutedAccentedColor(context.getResources().getColor(color))) {
return ThemesUtils.getMutedAccentColor();
}
}
return Build.VERSION.SDK_INT > 23 ? context.getColor(color) : context.getResources().getColor(color);
@@ -0,0 +1,238 @@
package ru.vtosters.lite.themes;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.loaders.ResourcesLoader;
import ru.vtosters.lite.themes.utils.ArscEditor;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.ThemesUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class ThemesManager {
private final static int[] ACCENT_COLORS = {
R.color.vk_sky_300,
R.color.sky_300,
R.color.viewer_retry_button_text_color,
R.color.azure_300,
R.color.picker_camera_button,
R.color.music_check_button_bg_pressed,
R.color.notification_color,
R.color.music_text_counter,
R.color.music_follow_button_bg_pressed,
R.color.music_blue_button_normal,
R.color.music_check_button_bg_normal,
R.color.vk_terms_link,
R.color.vk_blue_200,
R.color.accent_blue,
R.color.azure_A400,
R.color.vk_azure_A400,
R.color.blue,
R.color.blue_200,
R.color.vk_blue_300,
R.color.vk_blue_400,
R.color.blue_300,
R.color.blue_400,
R.color.cool_blue,
R.color.cornflower_blue,
R.color.cornflower_blue_two,
R.color.dot_unread,
R.color.fave_promo_btn_pressed,
R.color.header_blue,
R.color.colorAccent,
R.color.live_emoji_butt_hide,
R.color.music_action_button_blue,
R.color.name,
R.color.picker_blue,
R.color.picker_blue_pressed,
R.color.picker_tab_bg_selected,
R.color.picker_tab_text_selected,
R.color.sharing_blue_btn_normal,
R.color.sharing_blue_btn_pressed,
R.color.vk_sky_300,
R.color.vk_blue_A400,
R.color.blue_A400,
R.color.sky_300,
R.color.text_blue,
R.color.tip_background,
R.color.tw__blue_default,
R.color.tw__blue_pressed,
R.color.vkim_msg_sending_ic,
R.color.vkim_playing_drawable_rect,
R.color.vk_white_blue32,
R.color.date_picker_selector,
R.color.post_suggest_blue,
R.color.list_dialog_blue,
R.color.music_tab_bg_normal,
R.color.toolbar_blue_background,
R.color.toolbar_blue_statusBarColorBack,
R.color.blue_200_muted,
R.color.vk_blue_200_muted,
R.color.white_blue32,
R.color.vk_white_blue32,
R.color.header_blue_opacity16,
R.color.music_check_button_bg_checked,
R.color.attachpicker_item_background,
R.color.music_placeholder_artist_bg,
R.color.azure_100_muted,
R.color.vk_azure_100_muted,
R.color.muted_blue,
R.color.blue_600,
R.color.light_blue,
R.color.vk_blue_600,
R.color.muted_blue_opacity40,
R.color.muted_blue_old,
R.color.darker_blue,
R.color.header_blue_opacity40
};
private final static String[] NAMES = {
"resources.arsc",
"res",
"assets"
};
// Directories
private static File mainDir;
private static File binDir;
private static File palettesDir;
// Files
// Temp archive with resources.arsc and res.zip content for loading resources
private static File resTmpZipFile = null;
// Archive with assets folder content for loading resources
private static File assetsZipFile = null;
// Archive with res folder content
private static File resZipFile = null;
// Resources table file
private static File resourcesArscFile = null;
public static void init(Application app) {
try {
Log.d("ThemesManager", "init paths");
initPaths(app);
if (hasTmpArchive() && !ThemesUtils.isMonetTheme()) {
ResourcesLoader.init(app);
ResourcesLoader.load(app, resTmpZipFile.getAbsolutePath(), assetsZipFile.getAbsolutePath(), false);
}
} catch (Exception e) {
Log.e("ThemesManager", String.valueOf(e));
}
}
private static void initPaths(Context context) {
mainDir = new File(context.getFilesDir(), "VTLThemes");
if (!mainDir.exists()) mainDir.mkdir();
binDir = new File(mainDir, "bin");
if (!binDir.exists()) mainDir.mkdir();
palettesDir = new File(mainDir, "palettes");
if (!palettesDir.exists()) palettesDir.mkdir();
assetsZipFile = new File(binDir, "assets.zip");
resZipFile = new File(binDir, "res.zip");
resourcesArscFile = new File(binDir, "resources.arsc");
resTmpZipFile = new File(binDir, "res.tmp.zip");
}
public static void generateBins(Context context)
throws IOException {
try (final var apk = new ZipFile(context.getApplicationInfo().sourceDir)) {
for (var filename : NAMES) {
final var bin = new File(binDir, filename);
if (!bin.exists())
if (filename.contains(".")) extractFile(apk, filename, bin);
else extractAndCompressDirectory(apk, filename, new File(binDir, filename + ".zip"));
}
}
}
private static void extractFile(ZipFile apk, String name, File out)
throws IOException {
final var is = apk.getInputStream(apk.getEntry(name));
if (!out.exists()) out.getParentFile().mkdirs();
IOUtils.copy(is, out);
}
private static void extractAndCompressDirectory(ZipFile apk, String dirName, File out)
throws IOException {
final var zos = new ZipOutputStream(new FileOutputStream(out));
try (zos) {
final var entries = apk.entries();
while (entries.hasMoreElements()) {
final var entry = entries.nextElement();
if (!entry.getName().startsWith(dirName + "/")) continue;
zos.putNextEntry(new ZipEntry(entry.getName()));
final var zis = apk.getInputStream(entry);
IOUtils.copy(zis, zos);
zis.close();
zos.closeEntry();
}
}
}
public static boolean hasBins() {
return assetsZipFile.exists() && resZipFile.exists() && resourcesArscFile.exists();
}
public static boolean hasTmpArchive() {
return resTmpZipFile.exists();
}
public static void generateTempResArchive(int accentColor)
throws IOException {
final var resZip = new ZipFile(resZipFile);
final var zos = new ZipOutputStream(new FileOutputStream(resTmpZipFile));
try (zos) {
final var entries = resZip.entries();
while (entries.hasMoreElements()) {
final var entry = entries.nextElement();
final var is = resZip.getInputStream(entry);
zos.putNextEntry(new ZipEntry(entry.getName()));
IOUtils.copy(is, zos);
is.close();
zos.closeEntry();
}
final var arsc = new BinaryResourceFile(IOUtils.readFully(resourcesArscFile));
if (!ArscEditor.changeColors(arsc, ACCENT_COLORS, accentColor)) {
resZip.close();
resTmpZipFile.delete();
throw new IOException("Error while generating res.tmp.res");
}
zos.putNextEntry(new ZipEntry("resources.arsc"));
IOUtils.copy(new ByteArrayInputStream(arsc.toByteArray()), zos);
}
}
public static void deleteBins() {
assetsZipFile.delete();
resZipFile.delete();
resourcesArscFile.delete();
}
public static void deleteTmpArchive() {
resTmpZipFile.delete();
}
}
@@ -7,21 +7,17 @@ import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import ru.vtosters.lite.utils.ThemesUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import ru.vtosters.lite.utils.ThemesUtils;
public class VTLResources extends Resources {
private static final String TAG = "VTLResources";
private final Context context;
private static Field typedArrayField;
private static final List<Integer> attributesToTheme = new ArrayList<>();
private static Field typedArrayField;
static {
try {
@@ -36,12 +32,28 @@ public class VTLResources extends Resources {
attributesToTheme.add(16843173);
}
private final Context context;
public VTLResources(Context context, Resources parent) {
super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration());
this.context = context;
// Log.d(TAG, "VTLResources: init");
}
private static boolean isAttrThemeable(int attrID) {
return !ThemesUtils.isDarkTheme() && attrID == com.vtosters.lite.R.attr.button_primary_background;
}
private static int[] getArrayData(TypedArray arr) {
try {
return (int[]) typedArrayField.get(arr);
} catch (IllegalAccessException e) {
Log.e(TAG, "getArrayData: ", e);
e.printStackTrace();
return new int[0];
}
}
@Override
public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
var typedArray = super.obtainAttributes(set, attrs);
@@ -50,13 +62,13 @@ public class VTLResources extends Resources {
try {
int attrID = attrs[i];
if (attributesToTheme.contains(attrID)) {
int type = data[i*6];
int cnt = data[(i*6)+1];
int type = data[i * 6];
int cnt = data[(i * 6) + 1];
if (type == TypedValue.TYPE_ATTRIBUTE && isAttrThemeable(cnt)) {
data[i*6] = TypedValue.TYPE_INT_COLOR_RGB8;
data[(i*6)+1] = ThemesUtils.getAccentColor();
data[(i*6)+2] = 0; // clear reference content
data[i * 6] = TypedValue.TYPE_INT_COLOR_RGB8;
data[(i * 6) + 1] = ThemesUtils.getAccentColor();
data[(i * 6) + 2] = 0; // clear reference content
}
}
} catch (Exception e) {
@@ -75,18 +87,4 @@ public class VTLResources extends Resources {
}
return drawable;
}
private static boolean isAttrThemeable(int attrID) {
return !ThemesUtils.isDarkTheme() && attrID == com.vtosters.lite.R.attr.button_primary_background;
}
private static int[] getArrayData(TypedArray arr) {
try {
return (int[]) typedArrayField.get(arr);
} catch (IllegalAccessException e) {
Log.e(TAG, "getArrayData: ", e);
e.printStackTrace();
return new int[0];
}
}
}
@@ -1,43 +0,0 @@
package ru.vtosters.lite.themes;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import ru.vtosters.lite.themes.hooks.BaseHook;
import ru.vtosters.lite.themes.hooks.ButtonHook;
import ru.vtosters.lite.themes.hooks.ImageViewHook;
import ru.vtosters.lite.themes.hooks.ProgressWheelHook;
import ru.vtosters.lite.themes.hooks.TabLayoutHook;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.themes.hooks.ToolbarHook;
import ru.vtosters.lite.themes.hooks.VKImageViewHook;
public class ViewInjector {
public static ArrayList<BaseHook> hooks = new ArrayList<>();
static {
hooks.add(new ButtonHook());
hooks.add(new ImageViewHook());
hooks.add(new TabLayoutHook());
hooks.add(new TextViewHook());
hooks.add(new ToolbarHook());
hooks.add(new VKImageViewHook());
hooks.add(new ProgressWheelHook());
}
public static View inject(View view, int i, boolean z) {
for (BaseHook hook : hooks) {
hook.inject(view, i, z);
}
if (view instanceof ViewGroup) {
var viewGroup = (ViewGroup) view;
for (int i2 = 0; i2 < viewGroup.getChildCount(); i2++) {
inject(viewGroup.getChildAt(i2), i, false);
}
}
return view;
}
}
@@ -1,7 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
public interface BaseHook {
void inject(View view, int i, boolean z);
}
@@ -1,29 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import android.widget.Button;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.utils.ThemesUtils;
public class ButtonHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof Button) {
var button = (Button) view;
if (button.getBackgroundTintList() != null && ColorReferences.isMutedAccentedColor(button.getBackgroundTintList())) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
if (button.getBackgroundTintList() != null && ColorReferences.isAccentedColor(button.getBackgroundTintList())) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (TextViewHook.isPositiveButton(button) || TextViewHook.isVkUiButton(view)) {
button.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
}
@@ -1,24 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import android.widget.ImageView;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.utils.ThemesUtils;
public class ImageViewHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof ImageView) {
if (ThemesCore.isCachedAccents()) {
var imageView = (ImageView) view;
if (imageView.getImageTintList() == null) return;
if (ColorReferences.isCslNeedToBeThemed(imageView.getImageTintList())) {
imageView.setImageTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
}
}
@@ -1,21 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
import com.pnikosis.materialishprogress.ProgressWheel;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.utils.ThemesUtils;
public class ProgressWheelHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof ProgressWheel) {
if (ThemesCore.isCachedAccents()) {
var progressWheel = (ProgressWheel) view;
progressWheel.setBarColor(ThemesUtils.getAccentColor());
}
}
}
}
@@ -1,21 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.content.res.ColorStateList;
import android.view.View;
import com.vk.core.view.VKTabLayout;
import ru.vtosters.lite.utils.ThemesUtils;
public class TabLayoutHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof VKTabLayout) {
var tabLayout = (VKTabLayout) view;
tabLayout.setSelectedTabIndicatorColor(ThemesUtils.getAccentColor());
tabLayout.setTabRippleColor(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
}
}
@@ -1,136 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.os.Build;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.widget.AppCompatCheckedTextView;
import com.vk.core.extensions.EditTextExt;
import com.vk.core.view.OverlayTextView;
import com.vk.core.view.TintTextView;
import com.vk.core.view.links.LinkedTextView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.themes.ThemesHacks;
import ru.vtosters.lite.themes.utils.VkUiThemer;
import ru.vtosters.lite.utils.ThemesUtils;
public class TextViewHook implements BaseHook {
private static final String TAG = "TextViewHook";
@SuppressLint("RestrictedApi")
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof TextView) {
var textView = (TextView) view;
if (ColorReferences.isAccentedColor(textView.getCurrentTextColor())) {
textView.setTextColor(ThemesUtils.getAccentColor());
}
if (ColorReferences.isMutedAccentedColor(textView.getCurrentTextColor())) {
textView.setTextColor(ThemesUtils.getMutedAccentColor());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && textView.getCompoundDrawableTintList() != null) {
if (ColorReferences.isAccentedColor(textView.getCompoundDrawableTintList())) {
textView.setCompoundDrawableTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (ColorReferences.isMutedAccentedColor(textView.getCompoundDrawableTintList())) {
textView.setCompoundDrawableTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
}
if (textView.getLinkTextColors() != null && ColorReferences.isAccentedColor(textView.getLinkTextColors())) {
textView.setLinkTextColor(ThemesUtils.getAccentColor());
}
if (textView.getLinkTextColors() != null && ColorReferences.isMutedAccentedColor(textView.getLinkTextColors())) {
textView.setLinkTextColor(ThemesUtils.getMutedAccentColor());
}
if (isPositiveButton(textView) || isVkUiButton(textView)) {
view.setBackgroundTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (view instanceof OverlayTextView) {
if (view.getId() == android.R.id.button1) {
view.setBackground(ThemesUtils.recolorDrawable(view.getBackground()));
}
if (ColorReferences.isAccentedColor(((OverlayTextView) view).getTextColors().getDefaultColor())) {
((OverlayTextView) view).setTextColor(ThemesUtils.getAccentColor());
}
}
if (view instanceof TintTextView) {
if (eligibleForStartTinting((TintTextView) view)) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getAccentColor());
}
if (eligibleForStartTintingMuted((TintTextView) view)) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getMutedAccentColor());
}
if (eligibleForTinting(view.getId())) {
((TintTextView) view).setDrawableTint(ThemesUtils.getAccentColor());
}
if (eligibleForTinting(view.getId())) {
((TintTextView) view).setDrawableStartTint(ThemesUtils.getAccentColor());
}
}
if (view instanceof LinkedTextView) {
((LinkedTextView) view).setLinkTextColor(ThemesUtils.getAccentColor());
}
if (view instanceof AppCompatCheckedTextView) {
var check = (AppCompatCheckedTextView) view;
if (ColorReferences.isAccentedColor(check.getCheckMarkTintList())) {
check.setCheckMarkTintList(ColorStateList.valueOf(ThemesUtils.getAccentColor()));
}
if (ColorReferences.isMutedAccentedColor(check.getCheckMarkTintList())) {
check.setCheckMarkTintList(ColorStateList.valueOf(ThemesUtils.getMutedAccentColor()));
}
}
if (view instanceof EditText) {
ThemesUtils.setCursorColor((EditText) view);
}
ThemesUtils.colorHandles(textView);
VkUiThemer.autoThemeVkuiButtons(textView);
ThemesHacks.fixProfileSelector(textView);
ThemesHacks.fixThemedFeed(textView, i);
}
}
public static boolean eligibleForStartTinting(TintTextView view) {
return ColorReferences.isAccentedColor(view.getTextColors()) || view.getTextColors().getDefaultColor() == ThemesUtils.getAccentColor();
}
public static boolean eligibleForStartTintingMuted(TintTextView view) {
return ColorReferences.isMutedAccentedColor(view.getTextColors()) || view.getTextColors().getDefaultColor() == ThemesUtils.getMutedAccentColor();
}
public static boolean eligibleForTinting(int id) {
return !ThemesUtils.isDarkTheme() && (id == R.id.attach_title || id == R.id.post_retweet_name);
}
public static boolean isPositiveButton(View view) {
return !ThemesUtils.isDarkTheme() && (view.getId() == R.id.positive || view.getId() == R.id.positive_button);
}
public static boolean isVkUiButton(View view) {
return !ThemesUtils.isDarkTheme() && (view.getId() == R.id.empty_button || view.getId() == R.id.button);
}
}
@@ -1,29 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import com.vk.core.drawable.RecoloredDrawable;
import ru.vtosters.lite.utils.ThemesUtils;
public class ToolbarHook implements BaseHook {
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof Toolbar) {
var color = (ThemesUtils.isMilkshake() && !ThemesUtils.isDarkTheme()) ? ThemesUtils.getAccentColor() : ThemesUtils.getHeaderText();
var menu = ((Toolbar) view).getMenu();
for (int j = 0; j < menu.size(); ++j) {
var item = menu.getItem(j);
var icon = item.getIcon();
if (icon != null) {
if (icon instanceof RecoloredDrawable)
((RecoloredDrawable) icon).a(color);
else
icon = new RecoloredDrawable(icon, color);
item.setIcon(icon);
}
}
}
}
}
@@ -1,30 +0,0 @@
package ru.vtosters.lite.themes.hooks;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.view.View;
import com.vk.imageloader.view.VKImageView;
import ru.vtosters.lite.themes.utils.RecolorUtils;
import ru.vtosters.lite.utils.ThemesUtils;
public class VKImageViewHook implements BaseHook {
private static Paint paint = new Paint();
static {
paint.setColorFilter(new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN));
}
@Override
public void inject(View view, int i, boolean z) {
if (view instanceof VKImageView) {
var imageView = (VKImageView) view;
if (imageView.getImageTintList() != null) {
imageView.setImageTintList(RecolorUtils.recolorCSL(imageView.getImageTintList()));
imageView.setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN);
}
}
}
}
@@ -10,6 +10,7 @@ public class VTLColor {
this.id = id;
this.color = color;
}
public VTLColor(String resName, int id, int color) {
this.resName = resName;
this.id = id;
@@ -1,7 +1,6 @@
package ru.vtosters.lite.themes.items;
import android.graphics.Color;
import org.json.JSONException;
import org.json.JSONObject;
@@ -25,38 +24,6 @@ public class VTLPalette {
this.colors = colors;
}
public VTLPalette setId(String id) {
this.id = id;
return this;
}
public VTLPalette setName(String name) {
this.name = name;
return this;
}
public VTLPalette setAuthor(String author) {
this.author = author;
return this;
}
public VTLPalette setColors(List<VTLColor> colors) {
this.colors = colors;
return this;
}
public VTLPalette setColor(String name, int color) {
this.colors.add(new VTLColor(name, 0, color));
return this;
}
public int getColor(String name) {
for (var item : colors)
if (name.equals(item.resName))
return item.color;
return -1;
}
public static VTLPalette fromJson(JSONObject json) throws JSONException {
var palette = new VTLPalette()
.setId(json.optString("palette_id"))
@@ -91,4 +58,36 @@ public class VTLPalette {
.setAuthor(author)
.setColors(colors);
}
public VTLPalette setId(String id) {
this.id = id;
return this;
}
public VTLPalette setName(String name) {
this.name = name;
return this;
}
public VTLPalette setAuthor(String author) {
this.author = author;
return this;
}
public VTLPalette setColors(List<VTLColor> colors) {
this.colors = colors;
return this;
}
public VTLPalette setColor(String name, int color) {
this.colors.add(new VTLColor(name, 0, color));
return this;
}
public int getColor(String name) {
for (var item : colors)
if (name.equals(item.resName))
return item.color;
return -1;
}
}
@@ -0,0 +1,59 @@
package ru.vtosters.lite.themes.loaders;
import android.content.res.AssetManager;
import ru.vtosters.lite.utils.ReflectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class AssetManagerHelper {
// methods
private final static Method getAssetPathMtd;
private final static Method getApkAssetsMtd;
private final static Method addAssetPathMtd;
private final static Method addAssetPathAsSharedLibraryMtd;
private final static Method addOverlayPathMtd;
static {
try {
getAssetPathMtd = ReflectionUtils.findMethod(Class.forName("android.content.res.ApkAssets"), "getAssetPath");
getApkAssetsMtd = ReflectionUtils.findMethod(AssetManager.class, "getApkAssets");
addAssetPathMtd = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathAsSharedLibraryMtd = AssetManager.class.getMethod("addAssetPathAsSharedLibrary", String.class);
addOverlayPathMtd = AssetManager.class.getMethod("addOverlayPath", String.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
throw new RuntimeException();
}
}
public static String getApkPath(Object assetManager, String packagename)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object[] apkAssetsArr = (Object[]) getAssetPathMtd.invoke(assetManager);
// By default, base apk is the last element of array if is not, for execution speed, we should iterate array from the end to the beginning
for (int i = apkAssetsArr.length - 1; i >= 0; i--) {
final String pathname = (String) getApkAssetsMtd.invoke(apkAssetsArr[i]);
if (pathname.contains(packagename)) return pathname;
}
throw new NullPointerException();
}
//region Uses for adding own assets or getting existing cookies
public static int addAssetPath(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addAssetPathMtd.invoke(manager, path);
}
public static int addAssetPathAsSharedLibrary(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addAssetPathAsSharedLibraryMtd.invoke(manager, path);
}
public static int addOverlayPath(AssetManager manager, String path)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return (int) addOverlayPathMtd.invoke(manager, path);
}
//endregion
}
@@ -0,0 +1,376 @@
package ru.vtosters.lite.themes.loaders;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;
import ru.vtosters.lite.utils.ReflectionUtils;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class ResourcesLoader {
private static Collection<WeakReference<Resources>> sResourceReferences = null;
private static Object sCurrentActivityThread = null;
private static AssetManager sAssetManager = null;
// methods
private static Method ensureStringBlocksMtd = null;
// fields
private static Field assetsFld = null;
private static Field resourcesImplFld = null;
private static Field resDirFld = null;
private static Field packagesFld = null;
private static Field resourcePackagesFld = null;
private static Field publicSourceDirField = null;
private static Field stringBlocksFld = null;
private static long storedPatchedResModifiedTime = 0L;
static {
try {
// API version 8 has PackageInfo, 10 has LoadedApk. Needs to check 9
final Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
Class<?> loadedApkClz;
try {
loadedApkClz = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException ignored) {
loadedApkClz = Class.forName("android.app.ActivityThread$PackagenInfo");
}
resDirFld = ReflectionUtils.findField(loadedApkClz, "mResDir");
packagesFld = ReflectionUtils.findField(activityThreadClz, "mPackages");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1)
resourcePackagesFld = ReflectionUtils.findField(activityThreadClz, "mResourcePackages");
// fix jianGuo pro has private field 'mAssets' with Resource
// try use mResourcesImpl first
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
try {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFld = ReflectionUtils.findField(Resources.class, "mResourcesImpl");
} catch (Throwable ignored) {
// for safety
assetsFld = ReflectionUtils.findField(Resources.class, "mAssets");
}
else
assetsFld = ReflectionUtils.findField(Resources.class, "mAssets");
try {
publicSourceDirField = ReflectionUtils.findField(ApplicationInfo.class, "publicSourceDir");
} catch (NoSuchFieldException ignored) {
// Ignored.
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
try {
stringBlocksFld = ReflectionUtils.findField(AssetManager.class, "mStringBlocks");
ensureStringBlocksMtd = ReflectionUtils.findMethod(AssetManager.class, "ensureStringBlocks");
} catch (Throwable ignored) {
// Ignored.
}
} catch (Throwable e) {
Log.d("WTF", String.valueOf(e));
}
}
public static void init(Context context)
throws Exception {
sCurrentActivityThread = ReflectionUtils.getActivityThread(context, null);
//pre-N
// Find the singleton instance of ResourcesManager
final Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
final Method mGetInstance = ReflectionUtils.findMethod(resourcesManagerClass, "getInstance");
final Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = ReflectionUtils.findField(resourcesManagerClass, "mActiveResources");
final ArrayMap<?, WeakReference<Resources>> activeResources19 =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
sResourceReferences = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
final Field mResourceReferences = ReflectionUtils.findField(resourcesManagerClass, "mResourceReferences");
sResourceReferences = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
}
public static void load(Context context, String resPath, String assetsPath, boolean reinject)
throws Exception {
final ApplicationInfo appInfo = context.getApplicationInfo();
final Field[] packagesFlds = Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1
? new Field[]{packagesFld, resourcePackagesFld}
: new Field[]{packagesFld};
for (var fld : packagesFlds) {
final var value = fld.get(sCurrentActivityThread);
for (var entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
final var loadedApk = entry.getValue().get();
if (loadedApk == null) continue;
final var resDirPath = (String) resDirFld.get(loadedApk);
if (appInfo.sourceDir.equals(resDirPath))
resDirFld.set(loadedApk, resPath);
}
}
if (reinject) {
Log.i("ResourcesLoader", "Re-injecting, skip rest logic.");
return;
}
sAssetManager = AssetManager.class.newInstance();
// Create a new AssetManager instance and point it to the resources installed under
if (AssetManagerHelper.addAssetPath(sAssetManager, resPath) == 0
|| AssetManagerHelper.addAssetPath(sAssetManager, assetsPath) == 0)
throw new IllegalStateException("Could not create new AssetManager");
// Add SharedLibraries to AssetManager for resolve system resources not found issue
// This influence SharedLibrary Package ID
if (shouldAddSharedLibraryAssets(appInfo)) {
for (var sharedLibrary : appInfo.sharedLibraryFiles) {
if (sharedLibrary.endsWith(".apk")) continue;
if (AssetManagerHelper.addAssetPathAsSharedLibrary(sAssetManager, sharedLibrary) == 0)
throw new IllegalStateException("AssetManager add SharedLibrary Fail");
Log.i("ResourcesLoader", "addAssetPathAsSharedLibrary " + sharedLibrary);
}
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
if (stringBlocksFld != null && ensureStringBlocksMtd != null) {
stringBlocksFld.set(sAssetManager, null);
ensureStringBlocksMtd.invoke(sAssetManager);
}
for (var wr : sResourceReferences) {
final var resources = wr.get();
if (resources == null) continue;
// Set the AssetManager of the Resources instance to our brand new one
try {
//pre-N
assetsFld.set(resources, sAssetManager);
} catch (Throwable ignore) {
// N
final var resourceImpl = resourcesImplFld.get(resources);
// for Huawei HwResourcesImpl
final Field implAssets = ReflectionUtils.findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, sAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// Handle issues caused by WebView on Android N.
// Issue: On Android N, if an activity contains a webview, when screen rotates
// our resource patch may lost effects.
// for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
publicSourceDirField.set(context.getApplicationInfo(), resPath);
}
} catch (Throwable ignore) {
// Ignored.
}
}
installResourceInsuranceHacks(context, resPath, assetsPath);
}
private static void installResourceInsuranceHacks(Context context, String patchedResApkPath, String patchedAssetsApkPath) {
try {
final Object activityThread = ReflectionUtils.getActivityThread(context, null);
final Field mHField = ReflectionUtils.findField(activityThread, "mH");
final Handler mH = (Handler) mHField.get(activityThread);
final Field mCallbackField = ReflectionUtils.findField(Handler.class, "mCallback");
final Handler.Callback originCallback = (Handler.Callback) mCallbackField.get(mH);
if (!(originCallback instanceof ResourceInsuranceHandlerCallback)) {
final ResourceInsuranceHandlerCallback hackCallback = new ResourceInsuranceHandlerCallback(
context, patchedResApkPath, patchedAssetsApkPath, originCallback, mH.getClass());
mCallbackField.set(mH, hackCallback);
} else {
Log.w("ResourcesLoader", "installResourceInsuranceHacks: already installed, skip rest logic.");
}
} catch (Throwable thr) {
thr.printStackTrace();
}
}
private static boolean isPatchedResModifiedAfterLastLoad(String patchedResPath) {
long patchedResModifiedTime;
try {
patchedResModifiedTime = new File(patchedResPath).lastModified();
} catch (Throwable thr) {
thr.printStackTrace();
patchedResModifiedTime = 0L;
}
if (patchedResModifiedTime == 0) {
return false;
}
return patchedResModifiedTime != storedPatchedResModifiedTime;
}
private static void recordCurrentPatchedResModifiedTime(String patchedResPath) {
try {
storedPatchedResModifiedTime = new File(patchedResPath).lastModified();
} catch (Throwable thr) {
thr.printStackTrace();
storedPatchedResModifiedTime = 0L;
}
}
/**
* Why must I do these?
* Resource has mTypedArrayPool field, which just like Message Poll to reduce gc
* MiuiResource change TypedArray to MiuiTypedArray, but it get string block from offset instead of assetManager
*/
private static void clearPreloadTypedArrayIssue(Resources resources) {
// Perform this trick not only in Miui system since we can't predict if any other
// manufacturer would do the same modification to Android.
// if (!isMiuiSystem) {
// return;
// }
Log.w("ResourcesLoader", "try to clear typedArray cache!");
// Clear typedArray cache.
try {
final Field typedArrayPoolField = ReflectionUtils.findField(Resources.class, "mTypedArrayPool");
final Object origTypedArrayPool = typedArrayPoolField.get(resources);
final Method acquireMethod = ReflectionUtils.findMethod(origTypedArrayPool, "acquire");
while (true) {
if (acquireMethod.invoke(origTypedArrayPool) == null) {
break;
}
}
} catch (Throwable ignored) {
Log.e("ResourcesLoader", "clearPreloadTypedArrayIssue failed, ignore error: " + ignored);
}
}
private static boolean shouldAddSharedLibraryAssets(ApplicationInfo applicationInfo) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && applicationInfo != null &&
applicationInfo.sharedLibraryFiles != null;
}
private static final class ResourceInsuranceHandlerCallback implements Handler.Callback {
private static final String LAUNCH_ACTIVITY_LIFECYCLE_ITEM_CLASSNAME = "android.app.servertransaction.LaunchActivityItem";
private final Context mContext;
private final String mPatchResApkPath;
private final String mPatchedAssetsApkPath;
private final Handler.Callback mOriginalCallback;
private final int LAUNCH_ACTIVITY;
private final int RELAUNCH_ACTIVITY;
private final int EXECUTE_TRANSACTION;
private Method mGetCallbacksMethod = null;
private boolean mSkipInterceptExecuteTransaction = false;
ResourceInsuranceHandlerCallback(Context context, String patchResApkPath, String patchedAssetsApkPath, Handler.Callback original, Class<?> hClazz) {
Context appContext = context.getApplicationContext();
mContext = (appContext != null ? appContext : context);
mPatchResApkPath = patchResApkPath;
mPatchedAssetsApkPath = patchedAssetsApkPath;
mOriginalCallback = original;
LAUNCH_ACTIVITY = fetchMessageId(hClazz, "LAUNCH_ACTIVITY", 100);
RELAUNCH_ACTIVITY = fetchMessageId(hClazz, "RELAUNCH_ACTIVITY", 126);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
EXECUTE_TRANSACTION = fetchMessageId(hClazz, "EXECUTE_TRANSACTION ", 159);
} else {
EXECUTE_TRANSACTION = -1;
}
}
private int fetchMessageId(Class<?> hClazz, String name, int defVal) {
int value;
try {
value = ReflectionUtils.findField(hClazz, name).getInt(null);
} catch (Throwable e) {
value = defVal;
}
return value;
}
@Override
public boolean handleMessage(Message msg) {
boolean consume = false;
if (hackMessage(msg)) {
consume = true;
} else if (mOriginalCallback != null) {
consume = mOriginalCallback.handleMessage(msg);
}
return consume;
}
@SuppressWarnings("unchecked")
private boolean hackMessage(Message msg) {
boolean shouldReInjectPatchedResources = false;
if (!isPatchedResModifiedAfterLastLoad(mPatchResApkPath)) {
shouldReInjectPatchedResources = false;
} else {
if (msg.what == LAUNCH_ACTIVITY || msg.what == RELAUNCH_ACTIVITY) {
shouldReInjectPatchedResources = true;
} else if (msg.what == EXECUTE_TRANSACTION) {
do {
if (mSkipInterceptExecuteTransaction) {
break;
}
final Object transaction = msg.obj;
if (transaction == null) {
Log.w("ResourcesLoader", "transaction is null, skip rest insurance logic.");
break;
}
if (mGetCallbacksMethod == null) {
try {
mGetCallbacksMethod = ReflectionUtils.findMethod(transaction, "getCallbacks");
} catch (Throwable ignored) {
// Ignored.
}
}
if (mGetCallbacksMethod == null) {
Log.e("ResourcesLoader", "fail to find getLifecycleStateRequest method, skip rest insurance logic.");
mSkipInterceptExecuteTransaction = true;
break;
}
try {
final List<Object> req = (List<Object>) mGetCallbacksMethod.invoke(transaction);
if (req != null && req.size() > 0) {
final Object cb = req.get(0);
shouldReInjectPatchedResources = cb != null && cb.getClass().getName().equals(LAUNCH_ACTIVITY_LIFECYCLE_ITEM_CLASSNAME);
}
} catch (Throwable ignored) {
Log.e("ResourcesLoader", "fail to call getLifecycleStateRequest method, skip rest insurance logic.");
}
} while (false);
}
}
if (shouldReInjectPatchedResources) {
try {
load(mContext, mPatchResApkPath, mPatchedAssetsApkPath, true);
} catch (Throwable thr) {
thr.printStackTrace();
}
}
return false;
}
}
}
@@ -30,16 +30,16 @@ public class PalettesManager {
private final List<VTLPalette> mPalettes = new ArrayList<>();
public PalettesManager() {
load();
}
public static PalettesManager getInstance() {
if (sInstance == null)
sInstance = new PalettesManager();
return sInstance;
}
public PalettesManager() {
load();
}
public void load() {
mPalettes.clear();
if (!PALETTES_DIR.exists())
@@ -64,7 +64,7 @@ public class PalettesManager {
if (assets == null || assets.length == 0) return false;
for (var asset : assets) {
try {
IOUtils.copyFile(manager.open(ASSETS_DIR + asset), new File(PALETTES_DIR, asset));
IOUtils.copy(manager.open(ASSETS_DIR + asset), new File(PALETTES_DIR, asset));
} catch (IOException e) {
e.printStackTrace();
}
@@ -0,0 +1,37 @@
package ru.vtosters.lite.themes.utils;
import com.google.devrel.gmscore.tools.apk.arsc.*;
public class ArscEditor {
public static boolean changeColors(BinaryResourceFile arsc, int[] colorIds, int color /*int[] colors*/) {
int q = 0;
for (var chunk : arsc.getChunks()) {
final var resourceTableChunk = (ResourceTableChunk) chunk;
for (PackageChunk packageChunk : resourceTableChunk.getPackages()) {
for (TypeSpecChunk typeSpecChunk : packageChunk.getTypeSpecChunks()) {
for (TypeChunk typeChunk : packageChunk.getTypeChunks(typeSpecChunk.getId())) {
if (!"color".equals(typeChunk.getTypeName())) continue;
for (var entry : typeChunk.getEntries().entrySet()) {
if (entry.getValue().isComplex()) continue;
final var id = BinaryResourceIdentifier.create(packageChunk.getId(), typeSpecChunk.getId(), entry.getKey());
for (int i = 0; i < colorIds.length; ++i) {
if (colorIds[i] != id.resId()) continue;
final var value = entry.getValue();
value.updateValue(new BinaryResourceValue(8, value.value().type(), color /*colors[i]*/));
++q;
}
}
}
}
}
}
return q == colorIds.length;
}
}
@@ -25,7 +25,8 @@ public class RecolorUtils {
return new RecoloredDrawable(res, ThemesUtils.getAccentColor());
} // Get res drawable via id and coloring to accent
public static int recolorHexColor(int i){
public static int recolorHexColor(int i) {
if (!ThemesUtils.isCustomAccentEnabled()) return i;
var accented = ColorReferences.isAccentedColor(i);
var mutedaccented = ColorReferences.isMutedAccentedColor(i);
return (accented || mutedaccented) ? (accented ? ThemesUtils.getAccentColor() : ThemesUtils.getMutedAccentColor()) : i;
@@ -39,6 +40,14 @@ public class RecolorUtils {
@SuppressLint("UseCompatLoadingForColorStateLists")
public static ColorStateList themeCSL(Context context, int color) {
if (!ThemesUtils.isCustomAccentEnabled()) {
if (Build.VERSION.SDK_INT >= 23) {
return context.getColorStateList(color);
} else {
return context.getResources().getColorStateList(color);
}
}
if (ColorReferences.isColorRefAccented(color)) {
return ColorStateList.valueOf(ThemesUtils.getAccentColor());
} else if (ColorReferences.isColorRefMutedAccented(color)) {
@@ -56,7 +65,9 @@ public class RecolorUtils {
return themeCSL(csl);
} // Recolor ColorStateList
public static ColorStateList themeCSL(ColorStateList csl){
public static ColorStateList themeCSL(ColorStateList csl) {
if (!ThemesUtils.isCustomAccentEnabled()) return csl;
try {
int unsel = csl.getColorForState(new int[]{-android.R.attr.state_selected}, Color.BLACK);
int sel = csl.getColorForState(new int[]{android.R.attr.state_selected}, Color.BLACK);
@@ -1,85 +0,0 @@
package ru.vtosters.lite.themes.utils;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.widget.TextView;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.utils.ThemesUtils;
public class VkUiThemer {
public static void autoThemeVkuiButtons(TextView view) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
if (ThemesUtils.isDarkTheme())
return; // no need to theme b/w buttons, well it should skip, but I do it for extra performance
try {
if (!(view.getBackground() instanceof StateListDrawable)) return;
StateListDrawable states = ((StateListDrawable) view.getBackground());
if (states.getConstantState() == null) return;
DrawableContainer.DrawableContainerState dStates = (DrawableContainer.DrawableContainerState) states.getConstantState();
if (dStates.getChildCount() == 2) {
Drawable target = dStates.getChildren()[0];
if (!(target instanceof RippleDrawable)) return;
RippleDrawable ripple = (RippleDrawable) target;
for (int i = 0; i < ripple.getNumberOfLayers(); i++) {
if (ripple.getId(i) == android.R.id.mask) continue;
Drawable currentTarget = ripple.getDrawable(i);
if (!(currentTarget instanceof GradientDrawable)) continue;
//if (TSColorRefs.TS_DEBUG_MODE) Log.d("TSCore", "[tsh] finally, obamium = ["+currentTarget.toString()+"]");
GradientDrawable spd = (GradientDrawable) currentTarget;
if (spd.getShape() != GradientDrawable.RECTANGLE) continue;
// Log.d("TSCore", "[tsh] got a color!");
ColorStateList csl = spd.getColor();
// if (csl != null) {
// Log.d("TSCore", "[tsh] csl2 <def: " + csl.getDefaultColor() + " = " + ThemesUtils.hex(csl.getDefaultColor()) + "> | " + csl);
// }
if (ColorReferences.isAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh] theming CSL");
spd.setTint(ThemesUtils.getAccentColor());
}
}
Drawable draw2 = dStates.getChildren()[0];
if (!(draw2 instanceof GradientDrawable)) return;
GradientDrawable spd = (GradientDrawable) draw2;
if (spd.getShape() != GradientDrawable.RECTANGLE) return;
// Log.d("TSCore", "[tsh:disabled] got a color!");
ColorStateList csl = spd.getColor();
// if (csl != null) {
// Log.d("TSCore", "[tsh:disabled] csl2 <def: " + csl.getDefaultColor() + " = " + ThemesUtils.hex(csl.getDefaultColor()) + "> | " + csl);
// }
if (ColorReferences.isAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh:disabled] theming CSL");
spd.setTint(ThemesUtils.getAccentColor());
}
if (ColorReferences.isMutedAccentedColor(spd.getColor())) {
// Log.d("TSCore", "[tsh:disabled] theming CSL");
spd.setTint(ThemesUtils.getMutedAccentColor());
}
}
} catch (Exception e) {
// This must go wrong, so we ignore any exceptions that may occur
e.printStackTrace(); // REMOVE THIS!
}
}
}
@@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.ui.adapters.CategorizedAdapter;
import ru.vtosters.lite.ui.components.DockBarEditorManager;
import ru.vtosters.lite.ui.components.ItemMovingCallback;
@@ -58,8 +57,6 @@ public class DockBarEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(save, 0, false);
var saveParams = LayoutUtils.createLinear(0, -2);
saveParams.weight = 1.0f;
buttonsContainer.addView(save, saveParams);
@@ -74,8 +71,6 @@ public class DockBarEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(reset, 0, false);
var resetParams = LayoutUtils.createLinear(0, -2);
resetParams.weight = 1.0f;
buttonsContainer.addView(reset, resetParams);
@@ -15,7 +15,6 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.vtosters.lite.R;
import ru.vtosters.lite.themes.hooks.TextViewHook;
import ru.vtosters.lite.ui.adapters.CategorizedAdapter;
import ru.vtosters.lite.ui.components.ItemMovingCallback;
import ru.vtosters.lite.ui.components.SuperAppEditorManager;
@@ -67,8 +66,6 @@ public class SuperAppEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(save, 0, false);
var saveParams = LayoutUtils.createLinear(0, -2);
saveParams.weight = 1.0f;
buttonsContainer.addView(save, saveParams);
@@ -83,8 +80,6 @@ public class SuperAppEditorFragment extends BaseToolbarFragment {
restartApplication();
});
new TextViewHook().inject(reset, 0, false);
var resetParams = LayoutUtils.createLinear(0, -2);
resetParams.weight = 1.0f;
buttonsContainer.addView(reset, resetParams);
@@ -1,18 +1,17 @@
package ru.vtosters.lite.ui.fragments;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import androidx.preference.PreferenceCategory;
import b.h.g.k.VKProgressDialog;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.core.fragments.FragmentImpl;
import com.vk.navigation.Navigator;
import com.vtosters.lite.R;
import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.themes.palettes.PalettesManager;
import ru.vtosters.lite.ui.components.DockBarEditorManager;
import ru.vtosters.lite.ui.dialogs.PalettesBottomSheetDialog;
@@ -20,14 +19,22 @@ import ru.vtosters.lite.ui.views.rarepebble.ColorPickerView;
import ru.vtosters.lite.ui.wallpapers.WallpaperMenuFragment;
import ru.vtosters.lite.utils.*;
import java.io.IOException;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.lite.utils.ThemesUtils.recolorDrawable;
public class ThemesFragment extends MaterialPreferenceToolbarFragment {
private VKProgressDialog mProgressDialog;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
mProgressDialog = new VKProgressDialog(requireContext());
mProgressDialog.setCancelable(false);
addPreferencesFromResource(R.xml.preferences_themes);
initPreferences();
}
@@ -40,6 +47,8 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
return true;
});
findPreference("systememoji").setSummary(getGlobalContext().getString(R.string.systememojisum) + " \uD83D\uDE00\uD83D\uDE01\uD83E\uDD11\uD83E\uDD75\uD83D\uDC4D");
var dockbarEditor = findPreference("dockbareditor");
dockbarEditor.setSummary(AndroidUtils.getString(R.string.vtldocksumm) + ": " + DockBarEditorManager.getInstance().getSelectedTabs().size());
dockbarEditor.setOnPreferenceClickListener(preference -> {
@@ -47,6 +56,22 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
return true;
});
var invalidateThemeCache = findPreference("invalidate_theme_cache");
invalidateThemeCache.setOnPreferenceClickListener(preference -> {
mProgressDialog.setMessage("Инвалидация кэша...");
mProgressDialog.show();
VTExecutors.getIoExecutor().execute(() -> {
ThemesManager.deleteBins();
try {
ThemesManager.generateBins(requireContext());
requireActivity().runOnUiThread(this::restart);
} catch(IOException e) {
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при инвалидации кэша"));
}
});
return true;
});
var navBarPreference = findPreference("navbar");
navBarPreference.setOnPreferenceClickListener(preference -> {
restart();
@@ -104,7 +129,7 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
newsfeedNotificationsPreference.setVisible(Preferences.milkshake());
findPreference("accentprefs").setVisible(!ThemesUtils.isMonetTheme() && ThemesUtils.isMilkshake());
findPreference("accentprefs").setVisible(!ThemesUtils.isMonetTheme());
var wp = findPreference("wallpapers");
wp.setIcon(recolorDrawable(getGlobalContext().getDrawable(R.drawable.ic_media_outline_28)));
@@ -147,7 +172,7 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
}
})
.setNegativeButton(R.string.reset, (dialog, which) -> {
ThemesUtils.setCustomAccentColor(0, false);
ThemesManager.deleteTmpArchive();
LifecycleUtils.restartApplicationWithTimer();
})
.setPositiveButton(R.string.cancel, null)
@@ -193,9 +218,47 @@ public class ThemesFragment extends MaterialPreferenceToolbarFragment {
}
void setAccentColor(int color) {
ThemesUtils.setCustomAccentColor(color, false);
ThemesCore.setThemedColors(color);
restart();
String message;
Runnable task;
if(!ThemesManager.hasBins()) {
message = "Генерация файлов...";
task = () -> {
try {
ThemesManager.generateBins(requireContext());
requireActivity().runOnUiThread(() -> setAccentColor(color));
} catch(IOException e) {
Log.e("ThemesFragment", e+"");
ThemesManager.deleteBins();
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при генерации файлов"));
}
};
} else {
message = "Применение акцента...";
task = () -> {
try {
ThemesManager.generateTempResArchive(color);
requireActivity().runOnUiThread(this::restart);
} catch(IOException e) {
Log.e("ThemesFragment", e+"");
ThemesManager.deleteTmpArchive();
requireActivity().runOnUiThread(() -> showErrorDialog("Ошибка при применении акцента"));
}
};
}
mProgressDialog.setMessage(message);
mProgressDialog.show();
VTExecutors.getIoExecutor().execute(task);
}
void showErrorDialog(String msg) {
mProgressDialog.dismiss();
new VkAlertDialog.Builder(requireContext())
.setTitle("Ошибка")
.setMessage(msg)
.setPositiveButton("OK", null)
.show();
}
void restart() {
@@ -70,7 +70,7 @@ public class StickersFragment extends BaseToolbarFragment {
@Override
protected void onCreateMenu(Menu menu) {
var item = menu.add(0, 0, 0, "");
item.setIcon(ThemesUtils.recolorDrawable(AndroidUtils.getGlobalContext().getDrawable(R.drawable.ic_settings_24)))
item.setIcon(ThemesUtils.recolorDrawable(AndroidUtils.getGlobalContext().getDrawable(R.drawable.ic_settings_outline_28)))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
super.onCreateMenu(menu);
@@ -21,6 +21,7 @@ import com.vk.core.util.ToastUtils;
import com.vtosters.lite.general.fragments.WebViewFragment;
import ru.vtosters.lite.hooks.DateHook;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
@@ -56,7 +57,8 @@ public class AndroidUtils {
@NonNull
public static Context getGlobalContext() {
try {
return ReflectionUtils.invokeStaticMethod("android.app.AppGlobals", "getInitialApplication");
final Method getInitialApplicationMtd = ReflectionUtils.findMethod(Class.forName("android.app.AppGlobals"), "getInitialApplication");
return (Context) getInitialApplicationMtd.invoke(null);
} catch (Exception e) {
Log.d("GlobalContext", "Error while fetching context via refl");
}
@@ -1,11 +1,6 @@
package ru.vtosters.lite.utils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
@@ -24,12 +19,14 @@ import javax.crypto.spec.SecretKeySpec;
public class IOUtils {
public static final int BUFFER_SIZE = 0x800;
public static byte[] decodeStream(InputStream encIs, String keyURL) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
public static byte[] decodeStream(InputStream encIs, String keyURL)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
CipherInputStream cip = new CipherInputStream(encIs, getCipher(keyURL));
return readAllBytes(cip);
return readFully(cip);
}
public static Cipher getCipher(String key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
public static Cipher getCipher(String key)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key keySpec = new SecretKeySpec(key.getBytes(), "AES");
// bypassing ecb
@@ -40,66 +37,72 @@ public class IOUtils {
return cipher;
}
public static InputStream openStream(String url) throws IOException {
public static InputStream openStream(String url)
throws IOException {
URLConnection connection = new URL(url).openConnection();
return connection.getInputStream();
}
public static String readAllLines(File file) throws IOException {
public static String readAllLines(File file)
throws IOException {
return readAllLines(new FileInputStream(file));
}
public static String readAllLines(InputStream is) throws IOException {
return new String(readAllBytes(is), "utf-8");
public static String readAllLines(InputStream is)
throws IOException {
return new String(readFully(is), StandardCharsets.UTF_8);
}
public static byte[] readAllBytes(File file) throws IOException {
return readAllBytes(new FileInputStream(file));
public static byte[] readFully(final InputStream is)
throws IOException {
final var bos = new ByteArrayOutputStream();
try(bos) {
final byte[] buff = new byte[BUFFER_SIZE];
int len;
while((len = is.read(buff)) > 0)
bos.write(buff, 0, len);
return bos.toByteArray();
}
}
public static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = is.read(buffer)) > 0)
bos.write(buffer, 0, len);
is.close();
bos.close();
return bos.toByteArray();
public static byte[] readFully(final File in)
throws IOException {
return readFully(new FileInputStream(in));
}
public static void writeToFile(File file, String content) throws IOException {
public static void writeToFile(File file, String content)
throws IOException {
writeToFile(file, content.getBytes(StandardCharsets.UTF_8));
}
public static void writeToFile(File file, byte[] content) throws IOException {
if (!file.exists())
file.createNewFile();
public static void writeToFile(File file, byte[] content)
throws IOException {
var fos = new FileOutputStream(file);
fos.write(content);
fos.close();
}
public static void copyFileToDirectory(File dest, File targetDir) throws IOException {
if (!targetDir.exists())
targetDir.mkdirs();
var path = dest.getAbsolutePath();
var target = new File(targetDir, path.substring(path.lastIndexOf("/")));
copyFile(dest, target);
public static void copy(final InputStream is, final OutputStream os)
throws IOException {
final byte[] buff = new byte[BUFFER_SIZE];
int len;
while((len = is.read(buff)) > 0)
os.write(buff, 0, len);
}
public static void copyFile(File dest, File target) throws IOException {
var parent = target.getParentFile();
if (!parent.exists())
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(dest));
public static void copy(final byte[] buffer, final File out)
throws IOException {
copy(new ByteArrayInputStream(buffer), new FileOutputStream(out));
}
public static void copyFile(InputStream is, File target) throws IOException {
var parent = target.getParentFile();
if (!(parent != null ? parent.exists() : false))
parent.getParentFile().mkdirs();
writeToFile(target, readAllBytes(is));
public static void copy(final InputStream is, final File out)
throws IOException {
copy(is, new FileOutputStream(out));
}
public static void copy(final File in, final File out)
throws IOException {
copy(new FileInputStream(in), new FileOutputStream(out));
}
public static void deleteRecursive(File f) {
@@ -1,9 +1,9 @@
package ru.vtosters.lite.utils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import android.content.Context;
import com.vk.im.ui.views.Corners;
import java.lang.reflect.*;
public class ReflectionUtils {
@@ -11,244 +11,91 @@ public class ReflectionUtils {
* Gets a class with a specific name.
* Supports smali type (Lpackage/Class;)
*/
public static Class<?> getClass(String name)
public static Class<?> findClass(String name)
throws ClassNotFoundException {
if (name.contains(";"))
if (name.endsWith(";"))
name = name.substring(1, name.length() - 1).replace("/", ".");
return Class.forName(name);
}
public static Field getDeclaredField(Class<?> clz, String fieldName)
public static Field findField(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = clz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
public static Field getDeclaredField(String className, String fieldName)
throws NoSuchFieldException, ClassNotFoundException {
return getDeclaredField(getClass(className), fieldName);
}
public static Field getDeclaredFieldRecursive(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = null;
Class<?> clazz = clz;
while (clazz != null) {
NoSuchFieldException ex = null;
Field fld = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
field = getDeclaredField(clazz, fieldName);
fld = clz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clz.getSuperclass();
} catch(NoSuchFieldException e) {
ex = e;
}
}
if (field != null)
return field;
throw new NoSuchFieldException(fieldName);
if(fld == null) throw ex;
fld.setAccessible(true);
return fld;
}
public static Field getDeclaredFieldRecursive(String className, String fieldName)
throws ClassNotFoundException, NoSuchFieldException {
return getDeclaredFieldRecursive(getClass(className), fieldName);
public static Field findField(Object instance, String fieldName)
throws NoSuchFieldException {
return findField(instance.getClass(), fieldName);
}
public static Field getField(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = clz.getField(fieldName);
field.setAccessible(true);
return field;
}
public static Field getField(String className, String fieldName)
throws NoSuchFieldException, ClassNotFoundException {
return getField(getClass(className), fieldName);
}
public static Field getFieldRecursive(Class<?> clz, String fieldName)
throws NoSuchFieldException {
Field field = null;
Class<?> clazz = clz;
while (clazz != null) {
public static Constructor<?> findConstructor(Class<?> clz, Class<?>... params)
throws NoSuchMethodException {
Constructor<?> cctor = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
field = getField(clazz, fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clz.getSuperclass();
cctor = clz.getDeclaredConstructor(params);
} catch(NoSuchMethodException ignored) {
}
}
if (field != null)
return field;
throw new NoSuchFieldException(fieldName);
if(cctor == null) throw new NoSuchMethodException("");
cctor.setAccessible(true);
return cctor;
}
public static Field getFieldRecursive(String className, String fieldName)
throws ClassNotFoundException, NoSuchFieldException {
return getFieldRecursive(getClass(className), fieldName);
public static Constructor<?> findConstructor(Object instance, Class<?>... params)
throws NoSuchMethodException {
return findConstructor(instance.getClass(), params);
}
public static <T> T getObjectField(Class<?> clz, String fieldName, Object instance)
throws NoSuchFieldException, IllegalAccessException {
Field field;
public static Method findMethod(Class<?> clz, String methodName, Class<?>... params)
throws NoSuchMethodException {
Method mtd = null;
for(; clz != null; clz = clz.getSuperclass()) {
try {
mtd = clz.getDeclaredMethod(methodName, params);
} catch(NoSuchMethodException ignored) {
}
}
if(mtd == null) throw new NoSuchMethodException("methodName == " + methodName);
mtd.setAccessible(true);
return mtd;
}
public static Method findMethod(Object instance, String methodName, Class<?>... params)
throws NoSuchMethodException {
return findMethod(instance.getClass(), methodName, params);
}
public static Object getActivityThread(Context context, Class<?> clz)
throws InvocationTargetException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
try {
field = getField(clz, fieldName);
} catch (NoSuchFieldException e) {
field = getDeclaredField(clz, fieldName);
if(clz == null) clz = findClass("android.app.ActivityThread");
final Method currentActivityThreadMtd = findMethod(clz, "currentActivityThread");
Object currentActivityThread = currentActivityThreadMtd.invoke(null);
if (currentActivityThread == null && context != null) {
// In older versions of Android (prior to frameworks/base 66a017b63461a22842)
// the currentActivityThread was built on thread locals, so we'll need to try
// even harder
final Field loadedApkFld = findField(context, "mLoadedApk");
final Object apk = loadedApkFld.get(context);
final Field activityThreadFld = findField(apk, "mActivityThread");
currentActivityThread = activityThreadFld.get(apk);
}
return currentActivityThread;
} catch(Throwable th) {
return null;
}
return (T) field.get(instance);
}
public static <T> T getObjectField(String className, String fieldName, Object instance)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getObjectField(getClass(className), fieldName, instance);
}
public static void setObjectField(Class<?> clz, String fieldName, Object instance, Object value)
throws NoSuchFieldException, IllegalAccessException {
Field field;
try {
field = getField(clz, fieldName);
} catch (NoSuchFieldException e) {
field = getDeclaredField(clz, fieldName);
}
unfinalField(field);
field.set(instance, value);
}
public static void setObjectField(String className, String fieldName, Object instance, Object value)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
setObjectField(getClass(className), fieldName, instance, value);
}
public static <T> T getObjectStaticField(Class<?> clz, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
return getObjectField(clz, fieldName, null);
}
public static <T> T getObjectStaticField(String className, String fieldName)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getObjectField(getClass(className), fieldName, null);
}
public static void setObjectStaticField(Class<?> clz, String fieldName, Object value)
throws NoSuchFieldException, IllegalAccessException {
setObjectField(clz, fieldName, null, value);
}
public static void setObjectStaticField(String className, String fieldName, Object value)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
setObjectStaticField(getClass(className), fieldName, value);
}
public static void unfinalField(Field field) throws NoSuchFieldException {
if (!Modifier.isFinal(field.getModifiers())) return;
Field accessFlagsFld = getDeclaredField(Field.class, "accessFlags");
try {
int accessFlags = accessFlagsFld.getInt(field);
accessFlagsFld.setInt(field, ~Modifier.FINAL & accessFlags);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static Method getMethod(Class<?> clz, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = clz.getMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}
public static Method getMethod(String className, String methodName, Class<?>... parameterTypes)
throws ClassNotFoundException, NoSuchMethodException {
return getMethod(getClass(className), methodName, parameterTypes);
}
public static Method getMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return getMethod(clz, methodName, parameterTypes);
}
public static Method getMethod(String className, String methodName, Object... parameters)
throws ClassNotFoundException, NoSuchMethodException {
return getMethod(getClass(className), methodName, parameters);
}
public static Method getDeclaredMethod(Class<?> clz, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = clz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}
public static Method getDeclaredMethod(String className, String methodName, Class<?>... parameterTypes)
throws ClassNotFoundException, NoSuchMethodException {
return getDeclaredMethod(getClass(className), methodName, parameterTypes);
}
public static Method getDeclaredMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return getDeclaredMethod(clz, methodName, parameterTypes);
}
public static Method getDeclaredMethod(String className, String methodName, Object... parameters)
throws ClassNotFoundException, NoSuchMethodException {
return getDeclaredMethod(getClass(className), methodName, parameters);
}
public static <T> T invokeMethod(Class<?> clz, String methodName, Object instance, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method;
try {
method = getMethod(clz, methodName, parameterTypes);
} catch (NoSuchMethodException e) {
method = getDeclaredMethod(clz, methodName, parameters);
}
return (T) method.invoke(instance, parameters);
}
public static <T> T invokeMethod(String className, String methodName, Object instance, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, instance, parameterTypes, parameters);
}
public static <T> T invokeMethod(Class<?> clz, String methodName, Object instance, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?>[] parameterTypes = new Class<?>[parameters.length];
for (int i = 0; i < parameters.length; i++) {
parameterTypes[i] = parameters[i].getClass();
}
return invokeMethod(clz, methodName, instance, parameterTypes, parameters);
}
public static <T> T invokeMethod(String className, String methodName, Object instance, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, instance, parameters);
}
public static <T> T invokeStaticMethod(Class<?> clz, String methodName, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return invokeMethod(clz, methodName, null, parameterTypes, parameters);
}
public static <T> T invokeStaticMethod(String className, String methodName, Class<?>[] parameterTypes, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeStaticMethod(getClass(className), methodName, parameterTypes, parameters);
}
public static <T> T invokeStaticMethod(Class<?> clz, String methodName, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return invokeMethod(clz, methodName, null, parameters);
}
public static <T> T invokeStaticMethod(String className, String methodName, Object... parameters)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
return invokeMethod(getClass(className), methodName, null, parameters);
}
}
@@ -30,6 +30,7 @@ import com.vtosters.lite.data.ThemeTracker;
import ru.vtosters.lite.deviceinfo.OEMDetector;
import ru.vtosters.lite.hooks.VKUIHook;
import ru.vtosters.lite.themes.ThemesHacks;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.ui.wallpapers.WallpapersHooks;
import java.lang.reflect.Field;
@@ -46,6 +47,10 @@ public class ThemesUtils {
}
} // Apply VKTheme and ImTheme (hard applying without dynamic theme changing)
public static boolean isCustomAccentEnabled() {
return ThemesUtils.isMonetTheme() || ThemesManager.hasTmpArchive();
}
public static ColorStateList getAccenedColorStateList() {
return ColorStateList.valueOf(getAccentColor());
}
@@ -93,8 +98,7 @@ public class ThemesUtils {
}
public static int getAccentColor() {
var accent = AndroidUtils.getPreferences().getInt("accent_color", getColorFromAttr(R.attr.accent));
return (accent == 0 || !isMilkshake()) || isMonetTheme() ? getColorFromAttr(R.attr.accent) : accent;
return getColorFromAttr(R.attr.accent);
} // Color accent
public static int getMutedAccentColor() {
@@ -296,6 +300,7 @@ public class ThemesUtils {
} // Recolor drawable to accent color
public static Drawable recolorToolbarDrawable(Drawable drawable) {
if (!ThemesUtils.isCustomAccentEnabled()) return drawable;
if (drawable == null) return null;
return new RecoloredDrawable(drawable, (ThemesUtils.isMilkshake() && !ThemesUtils.isDarkTheme()) ? ThemesUtils.getAccentColor() : ThemesUtils.getHeaderText());
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#ff518bcc" android:state_checked="true" />
<item android:color="@color/music_action_button_blue" android:state_checked="true" />
<item android:color="#ffc4c8cc" />
</selector>
+1 -1
View File
@@ -10,7 +10,7 @@
android:strokeWidth="1.0"
android:strokeColor="#00000000" />
<path
android:fillColor="#ff518bcc"
android:fillColor="@color/music_action_button_blue"
android:pathData="M13,27.25L13,31L16.75,31L27.81,19.94L24.06,16.19L13,27.25L13,27.25ZM30.71,17.04C31.1,16.65 31.1,16.02 30.71,15.63L28.37,13.29C27.98,12.9 27.35,12.9 26.96,13.29L25.13,15.12L28.88,18.87L30.71,17.04L30.71,17.04Z"
android:strokeWidth="1.0"
android:strokeColor="#00000000" />
+1 -1
View File
@@ -10,7 +10,7 @@
android:strokeWidth="1.0"
android:strokeColor="#00000000" />
<path
android:fillColor="#ff518bcc"
android:fillColor="@color/music_action_button_blue"
android:pathData="M14.8,31C13.81,31 13,29.99 13,28.75L13,15.25C13,14.01 13.81,13 14.8,13L29.2,13C30.19,13 31,14.01 31,15.25L31,28.75C31,29.99 30.19,31 29.2,31L14.8,31ZM17,22C17,24.76 19.24,27 22,27C24.76,27 27,24.76 27,22C27,19.24 24.76,17 22,17C19.24,17 17,19.24 17,22ZM18.8,22C18.8,20.23 20.23,18.8 22,18.8C23.77,18.8 25.2,20.23 25.2,22C25.2,23.77 23.77,25.2 22,25.2C20.23,25.2 18.8,23.77 18.8,22Z"
android:strokeWidth="1.0"
android:strokeColor="#00000000" />
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5696,7 +5696,7 @@ QR-код"</string>
<string name="stickersproxy1">Наладзьце IP-проксі!</string>
<string name="stories_vtl">Гісторыі</string>
<string name="systememoji">Выкарыстоўваць сістэмныя эмодзі</string>
<string name="systememojisum">Вашы сістэмныя эмодзі - 😀😁🤑🥵👍</string>
<string name="systememojisum">Вашы сістэмныя эмодзі - </string>
<string name="tokenwarning">Нікому не перадавайце гэты токен, калі вы не ведаеце, што рабіць!</string>
<string name="useproxy">Варыянт проксі</string>
<string name="useproxyinfo">Інфармацыя аб выкарыстанні проксі</string>
@@ -5865,7 +5865,7 @@ QR-код"</string>
<string name="vtl_crash_upload">Загрузіць логі</string>
<string name="vtl_crash_save">Захаваць у файл</string>
<string name="batteryissuetitle">Адключыце аптымізацыю батарэі!</string>
<string name="batteryissuesumm">У сувязі з тым, што вы карыстаецеся прашыўку іншых вытворцаў з прыведзенага ніжэй спісу - апавяшчэнні могуць прыходзіць з затрымкай або не прыходзіць зусім. \n Націсніце на кнопку \"Адключыць аптымізацыю\" каб выправіць праблему. \n\nПабочныя прашыўкі на якіх назіраюцца праблемы з пушамі: \nFlyme, EMUI, MIUI, ColorOS, Samsung Experience або TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuesumm">У сувязі з тым, што вы карыстаецеся прашыўку іншых вытворцаў з прыведзенага ніжэй спісу - апавяшчэнні могуць прыходзіць з затрымкай або не прыходзіць зусім. \nНацісніце на кнопку \"Адключыць аптымізацыю\" каб выправіць праблему. \n\nПабочныя прашыўкі на якіх назіраюцца праблемы з пушамі: \nFlyme, EMUI, MIUI, ColorOS, Samsung Experience або TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuebtn1">Адключыць аптымізацыю</string>
<string name="batteryissuebtn2">Ігнараваць</string>
<string name="gmsissuesumm">На вашай прыладзе адсутнічае кампанент Google Play Services (GMS), які неабходны для карэктнай працы VTosters Lite</string>
+3 -3
View File
@@ -5692,7 +5692,7 @@
<string name="stickersproxy1">Настройте IP-прокси!</string>
<string name="stories_vtl">Истории</string>
<string name="systememoji">Использовать системные эмодзи</string>
<string name="systememojisum">Ваши системные эмодзи - 😀😁🤑🥵👍</string>
<string name="systememojisum">Ваши системные эмодзи - </string>
<string name="tokenwarning">Никому не передавайте этот токен, если вы не знаете что делать!</string>
<string name="useproxy">Вариант прокси</string>
<string name="useproxyinfo">Информация об использовании прокси</string>
@@ -5895,7 +5895,7 @@
<string name="vtl_crash_save">Сохранить в файл</string>
<string name="batteryissuetitle">Отключите оптимизацию батареи!</string>
<string name="batteryissuesumm">В связи с тем, что вы используете прошивку сторонних производителей из приведенного ниже списка - уведомления могут приходить с задержкой или не приходить вовсе. \n Нажмите на кнопку \"Отключить оптимизацию\" чтобы исправить проблему. \n\nСторонние прошивки на которых наблюдаются проблемы с пушами:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience или TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuesumm">В связи с тем, что вы используете прошивку сторонних производителей из приведенного ниже списка - уведомления могут приходить с задержкой или не приходить вовсе. \nНажмите на кнопку \"Отключить оптимизацию\" чтобы исправить проблему. \n\nСторонние прошивки на которых наблюдаются проблемы с пушами:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience или TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuebtn1">Отключить оптимизацию</string>
<string name="batteryissuebtn2">Игнорировать</string>
@@ -5904,7 +5904,7 @@
<string name="gmsissuebtn2">Установить GMS</string>
<string name="startsumm1">Модификация VTosters Lite не имеет никакого отношения к VK Group!\nРаспространяется полностью бесплатно!</string>
<string name="startsumm2">\n\nПриложение может некорректно работать в связи с тем, что вы, скорее всего, используете оболочку из списка ниже. Все проблемы с отображением и работой приложения связаны только с вашей прошивкой и только с ней. Все багрепорты, присланные из-за проблем на этой прошивке, не рассматриваются разработчиками и не будут исправлены.\n\nНажимая на кнопку \"Продолжить\", вы соглашаетесь с тем, что возможные баги приложения могут произойти из-за специфики вашей прошивки. Рекомендуем использовать прошивки на базе AOSP для получения максимальной стабильности.\n\n Прошивки на которых наблюдаются проблемы с приложениями:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience или TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="startsumm2">\n\nПриложение может некорректно работать в связи с тем, что вы, скорее всего, используете оболочку из списка ниже. Все проблемы с отображением и работой приложения связаны только с вашей прошивкой и только с ней. Все багрепорты, присланные из-за проблем на этой прошивке, не рассматриваются разработчиками и не будут исправлены.\n\nНажимая на кнопку \"Продолжить\", вы соглашаетесь с тем, что возможные баги приложения могут произойти из-за специфики вашей прошивки. Рекомендуем использовать прошивки на базе AOSP для получения максимальной стабильности.\n\nПрошивки на которых наблюдаются проблемы с приложениями:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience или TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="startbtn1">Новости</string>
<string name="startbtn2">Продолжить</string>
+2 -2
View File
@@ -5619,7 +5619,7 @@
<string name="stickersproxy1">Налаштуйте IP-проксі!</string>
<string name="stories_vtl">Історії</string>
<string name="systememoji">Використовувати системні емодзі</string>
<string name="systememojisum">Ваші системні емодзі - 😀😁🤑🥵👍</string>
<string name="systememojisum">Ваші системні емодзі - </string>
<string name="tokenwarning">Нікому не передавайте цей токен, якщо ви не знаєте, що робити!</string>
<string name="useproxy">Варіант проксі</string>
<string name="useproxyinfo">Інформація про використання проксі</string>
@@ -5821,7 +5821,7 @@
<string name="vtl_crash_save">Зберегти у файл</string>
<string name="batteryissuetitle">Відключіть оптимізацію батареї!</string>
<string name="batteryissuesumm">"У зв'язку з тим, що ви використовуєте прошивку сторонніх виробників із наведеного нижче списку - повідомлення можуть надходити із затримкою або не приходити зовсім. \n Натисніть кнопку "Вимкнути оптимізацію", щоб виправити проблему. \n\nСторінні прошивки на яких спостерігаються проблеми з пушами:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience або TouchWiz, Magic UI, ZenUI, Funtouch OS"</string>
<string name="batteryissuesumm">"У зв'язку з тим, що ви використовуєте прошивку сторонніх виробників із наведеного нижче списку - повідомлення можуть надходити із затримкою або не приходити зовсім. \nНатисніть кнопку "Вимкнути оптимізацію", щоб виправити проблему. \n\nСторінні прошивки на яких спостерігаються проблеми з пушами:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience або TouchWiz, Magic UI, ZenUI, Funtouch OS"</string>
<string name="batteryissuebtn1">Вимкнути оптимізацію</string>
<string name="batteryissuebtn2">Ігнорувати</string>
+3 -3
View File
@@ -5826,7 +5826,7 @@ You need to restart the application to apply proxy settings"</string>
<string name="stickersproxy1">Configure the IP proxy!</string>
<string name="stories_vtl">Stories</string>
<string name="systememoji">Use system emojis</string>
<string name="systememojisum">Your system emojis - 😀😁🤑🥵👍</string>
<string name="systememojisum">Your system emojis - </string>
<string name="tokenwarning">"Don't give this token to anyone if you don't know what to do!"</string>
<string name="useproxy">Proxy variant</string>
<string name="useproxyinfo">Information about using a proxy</string>
@@ -6033,7 +6033,7 @@ May look like a fake leaving the online"</string>
<string name="vtl_crash_save">Save to local file</string>
<string name="batteryissuetitle">Disable battery optimization!</string>
<string name="batteryissuesumm">Due to the fact that you are using a third-party firmware from the list below, notifications may come with a delay or not at all. \n Click on the \"Disable optimization\" button to fix this problem. \n\nThird-party firmware on which problems with push are observed:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience or TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuesumm">Due to the fact that you are using a third-party firmware from the list below, notifications may come with a delay or not at all. \nClick on the \"Disable optimization\" button to fix this problem. \n\nThird-party firmware on which problems with push are observed:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience or TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="batteryissuebtn1">Disable optimization</string>
<string name="batteryissuebtn2">Ignore</string>
@@ -6042,7 +6042,7 @@ May look like a fake leaving the online"</string>
<string name="gmsissuebtn2">Install GMS</string>
<string name="startsumm1">The VTosters Lite modification has nothing with VK Group!\nDistributed completely free of fee!</string>
<string name="startsumm2">\n\nThe application may not work correctly due to the fact that you are most likely using the shell from the list below. All problems with the display and work of the application are related only to your firmware and only with it. All bug reports sent due to problems on this firmware are not considered by the developers and will not be fixed.\n\n Pressing the \"Continue\" button, you agree that possible application bugs may occur due to the specifics of your firmware. We recommend using AOSP-based firmware for maximum stability.\n\n Firmware on which there are problems with applications:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience or TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="startsumm2">\n\nThe application may not work correctly due to the fact that you are most likely using the shell from the list below. All problems with the display and work of the application are related only to your firmware and only with it. All bug reports sent due to problems on this firmware are not considered by the developers and will not be fixed.\n\nPressing the \"Continue\" button, you agree that possible application bugs may occur due to the specifics of your firmware. We recommend using AOSP-based firmware for maximum stability.\n\nFirmware on which there are problems with applications:\nFlyme, EMUI, MIUI, ColorOS, Samsung Experience or TouchWiz, Magic UI, ZenUI, Funtouch OS</string>
<string name="startbtn1">News</string>
<string name="startbtn2">Continue</string>
+5
View File
@@ -22,6 +22,11 @@
android:key="calls"
android:summary="@string/messages_interface_calls_summary"
android:title="@string/vtlcalls"/>
<com.vtosters.lite.ui.MaterialSwitchPreference
android:defaultValue="false"
android:key="useGenius"
android:summary="Получать тексты с сервиса Genius"
android:title="Тексты с Genius"/>
<!-- <com.vtosters.lite.ui.SummaryListPreference
android:defaultValue="system"
android:dialogTitle="@string/debug_experimental_language_summary"
+4 -1
View File
@@ -7,6 +7,10 @@
android:summary="@string/accent_color_summ"
android:icon="@drawable/bg_accent_circle"
android:title="@string/accent_color_title"/>
<Preference
android:key="invalidate_theme_cache"
android:title="Инвалидация кэша темы" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/vtlthemes">
<com.vtosters.lite.ui.MaterialSwitchPreference
@@ -39,7 +43,6 @@
<com.vtosters.lite.ui.MaterialSwitchPreference
android:defaultValue="false"
android:key="systememoji"
android:summary="@string/systememojisum"
android:title="@string/systememoji"/>
</PreferenceCategory>
<PreferenceCategory
@@ -1,29 +0,0 @@
.class public Lcom/vk/api/audio/AudioGetLyrics$a;
.super Ljava/lang/Object;
.source "AudioGetLyrics.java"
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/vk/api/audio/AudioGetLyrics;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x9
name = "a"
.end annotation
# instance fields
.field public a:Ljava/lang/String;
# direct methods
.method public constructor <init>()V
.locals 0
.line 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
@@ -1,123 +0,0 @@
.class public Lcom/vk/api/audio/AudioGetLyrics;
.super Lcom/vk/api/base/ApiRequest;
.source "AudioGetLyrics.java"
# annotations
.annotation system Ldalvik/annotation/MemberClasses;
value = {
Lcom/vk/api/audio/AudioGetLyrics$a;
}
.end annotation
.annotation system Ldalvik/annotation/Signature;
value = {
"Lcom/vk/api/base/ApiRequest<",
"Lcom/vk/api/audio/AudioGetLyrics$a;",
">;"
}
.end annotation
# instance fields
.field private F:I
# direct methods
.method public constructor <init>(I)V
.locals 1
const-string v0, "audio.getLyrics"
.line 1
invoke-direct {p0, v0}, Lcom/vk/api/base/ApiRequest;-><init>(Ljava/lang/String;)V
const-string v0, "lyrics_id"
.line 2
invoke-virtual {p0, v0, p1}, Lcom/vk/api/base/ApiRequest;->b(Ljava/lang/String;I)Lcom/vk/api/base/ApiRequest;
.line 3
iput p1, p0, Lcom/vk/api/audio/AudioGetLyrics;->F:I
return-void
.end method
# virtual methods
.method public a(Lorg/json/JSONObject;)Lcom/vk/api/audio/AudioGetLyrics$a;
.locals 3
.param p1 # Lorg/json/JSONObject;
.annotation build Landroidx/annotation/NonNull;
.end annotation
.end param
.line 2
:try_start_0
new-instance v0, Lcom/vk/api/audio/AudioGetLyrics$a;
invoke-direct {v0}, Lcom/vk/api/audio/AudioGetLyrics$a;-><init>()V
const-string v1, "response"
.line 3
invoke-virtual {p1, v1}, Lorg/json/JSONObject;->getJSONObject(Ljava/lang/String;)Lorg/json/JSONObject;
move-result-object p1
const-string v1, "text"
invoke-virtual {p1, v1}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
iput-object p1, v0, Lcom/vk/api/audio/AudioGetLyrics$a;->a:Ljava/lang/String;
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
return-object v0
:catch_0
move-exception p1
const/4 v0, 0x2
new-array v0, v0, [Ljava/lang/Object;
const/4 v1, 0x0
const-string v2, "vk"
aput-object v2, v0, v1
const/4 v1, 0x1
aput-object p1, v0, v1
.line 4
invoke-static {v0}, Lcom/vk/log/L;->e([Ljava/lang/Object;)V
const/4 p1, 0x0
return-object p1
.end method
.method public bridge synthetic a(Lorg/json/JSONObject;)Ljava/lang/Object;
.locals 0
.param p1 # Lorg/json/JSONObject;
.annotation build Landroidx/annotation/NonNull;
.end annotation
.end param
.annotation system Ldalvik/annotation/Throws;
value = {
Ljava/lang/Exception;
}
.end annotation
.line 1
invoke-virtual {p0, p1}, Lcom/vk/api/audio/AudioGetLyrics;->a(Lorg/json/JSONObject;)Lcom/vk/api/audio/AudioGetLyrics$a;
move-result-object p1
return-object p1
.end method
@@ -274,9 +274,6 @@
invoke-virtual {v2, v3}, Landroidx/appcompat/widget/Toolbar;->setOnMenuItemClickListener(Landroidx/appcompat/widget/Toolbar$OnMenuItemClickListener;)V
const v3, 0x0
invoke-static {v2, v3, v3}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
const-string v3, "this"
.line 12
@@ -343,12 +343,6 @@
invoke-super {p0, p1, p2}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;)Landroid/view/View;
move-result-object p2
const/4 v0, 0x1
invoke-static {p2, p1, v0}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
move-result-object p1
return-object p1
@@ -359,10 +353,6 @@
invoke-super {p0, p1, p2, p3}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;
move-result-object p2
invoke-static {p2, p1, p3}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
move-result-object p1
return-object p1
@@ -375,14 +365,6 @@
move-result-object p1
const/4 p2, -0x1
const/4 v0, 0x1
invoke-static {p1, p2, v0}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
move-result-object p1
return-object p1
.end method
@@ -393,11 +375,5 @@
move-result-object p1
const/4 p2, -0x1
invoke-static {p1, p2, p3}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
move-result-object p1
return-object p1
.end method
@@ -157,6 +157,19 @@
.method public getResources()Landroid/content/res/Resources;
.registers 3
invoke-static {}, Lru/vtosters/lite/utils/ThemesUtils;->isMonetTheme()Z
move-result v1
if-nez v1, :cond_0
invoke-static {}, Lru/vtosters/lite/utils/AndroidUtils;->getResources()Landroid/content/res/Resources;
move-result-object v0
return-object v0
:cond_0
iget-object v0, p0, Lcom/vk/core/ui/themes/VKThemeHelper$b;->resources:Landroid/content/res/Resources;
if-eqz v0, :cond_7
@@ -1281,10 +1281,6 @@
invoke-virtual {p0, p1}, Landroidx/appcompat/widget/Toolbar;->setOnMenuItemClickListener(Landroidx/appcompat/widget/Toolbar$OnMenuItemClickListener;)V
const v1, 0x0
invoke-static {p0, v1, v1}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
return-void
.end method
@@ -241,9 +241,6 @@
iput-object p2, p0, Lcom/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent;->B:Lcom/vk/im/ui/components/dialogs_header/vc/DialogsHeaderVc;
const v0, 0x0
invoke-static {p3, v0, v0}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
.line 3
iget-object p1, p0, Lcom/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent;->B:Lcom/vk/im/ui/components/dialogs_header/vc/DialogsHeaderVc;
@@ -1,47 +0,0 @@
.class Lcom/vk/music/view/x/LyricsHolder$a;
.super Ljava/lang/Object;
.source "LyricsHolder.java"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/vk/music/view/x/LyricsHolder;-><init>(Landroid/view/ViewGroup;Lcom/vk/music/player/PlayerModel;)V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x0
name = null
.end annotation
# instance fields
.field final synthetic a:Lcom/vk/music/view/x/LyricsHolder;
# direct methods
.method constructor <init>(Lcom/vk/music/view/x/LyricsHolder;)V
.locals 0
.line 1
iput-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$a;->a:Lcom/vk/music/view/x/LyricsHolder;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public onClick(Landroid/view/View;)V
.locals 0
.line 1
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$a;->a:Lcom/vk/music/view/x/LyricsHolder;
invoke-virtual {p1}, Lcom/vk/music/view/x/LyricsHolder;->g0()V
return-void
.end method
@@ -1,173 +0,0 @@
.class Lcom/vk/music/view/x/LyricsHolder$b;
.super Ljava/lang/Object;
.source "LyricsHolder.java"
# interfaces
.implements Lcom/vk/api/base/ApiCallback;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/vk/music/view/x/LyricsHolder;->g0()V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x0
name = null
.end annotation
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/lang/Object;",
"Lcom/vk/api/base/ApiCallback<",
"Lcom/vk/api/audio/AudioGetLyrics$a;",
">;"
}
.end annotation
# instance fields
.field final synthetic a:Lcom/vk/dto/music/MusicTrack;
.field final synthetic b:Lcom/vk/music/view/x/LyricsHolder;
# direct methods
.method constructor <init>(Lcom/vk/music/view/x/LyricsHolder;Lcom/vk/dto/music/MusicTrack;)V
.locals 0
.line 1
iput-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
iput-object p2, p0, Lcom/vk/music/view/x/LyricsHolder$b;->a:Lcom/vk/dto/music/MusicTrack;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public a(Lcom/vk/api/audio/AudioGetLyrics$a;)V
.locals 1
.line 2
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder$b;->a:Lcom/vk/dto/music/MusicTrack;
iget-object p1, p1, Lcom/vk/api/audio/AudioGetLyrics$a;->a:Ljava/lang/String;
iput-object p1, v0, Lcom/vk/dto/music/MusicTrack;->H:Ljava/lang/String;
.line 3
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->a(Lcom/vk/music/view/x/LyricsHolder;)Lcom/vtosters/lite/ui/MusicErrorViewHelper;
move-result-object p1
const/16 v0, 0x8
invoke-virtual {p1, v0}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->b(I)V
.line 4
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->b(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
move-result-object p1
invoke-static {p1, v0}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
.line 5
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->c(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
move-result-object p1
const/4 v0, 0x0
invoke-static {p1, v0}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
.line 6
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-virtual {p1}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->f0()V
.line 7
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->d(Lcom/vk/music/view/x/LyricsHolder;)Ljava/util/concurrent/atomic/AtomicBoolean;
move-result-object p1
invoke-virtual {p1, v0}, Ljava/util/concurrent/atomic/AtomicBoolean;->set(Z)V
return-void
.end method
.method public a(Lcom/vk/api/sdk/exceptions/VKApiExecutionException;)V
.locals 2
.line 8
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {v0}, Lcom/vk/music/view/x/LyricsHolder;->d(Lcom/vk/music/view/x/LyricsHolder;)Ljava/util/concurrent/atomic/AtomicBoolean;
move-result-object v0
const/4 v1, 0x0
invoke-virtual {v0, v1}, Ljava/util/concurrent/atomic/AtomicBoolean;->set(Z)V
.line 9
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {v0}, Lcom/vk/music/view/x/LyricsHolder;->a(Lcom/vk/music/view/x/LyricsHolder;)Lcom/vtosters/lite/ui/MusicErrorViewHelper;
move-result-object v0
invoke-virtual {v0, p1}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->a(Lcom/vk/api/sdk/exceptions/VKApiExecutionException;)V
.line 10
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->a(Lcom/vk/music/view/x/LyricsHolder;)Lcom/vtosters/lite/ui/MusicErrorViewHelper;
move-result-object p1
invoke-virtual {p1, v1}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->b(I)V
.line 11
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->b(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
move-result-object p1
const/16 v0, 0x8
invoke-static {p1, v0}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
.line 12
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder$b;->b:Lcom/vk/music/view/x/LyricsHolder;
invoke-static {p1}, Lcom/vk/music/view/x/LyricsHolder;->c(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
move-result-object p1
invoke-static {p1, v0}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
return-void
.end method
.method public bridge synthetic a(Ljava/lang/Object;)V
.locals 0
.line 1
check-cast p1, Lcom/vk/api/audio/AudioGetLyrics$a;
invoke-virtual {p0, p1}, Lcom/vk/music/view/x/LyricsHolder$b;->a(Lcom/vk/api/audio/AudioGetLyrics$a;)V
return-void
.end method
@@ -1,539 +0,0 @@
.class public Lcom/vk/music/view/x/LyricsHolder;
.super Lcom/vtosters/lite/ui/holder/RecyclerHolder;
.source "LyricsHolder.java"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/Signature;
value = {
"Lcom/vtosters/lite/ui/holder/RecyclerHolder<",
"Lcom/vk/music/dto/PlayerInfo;",
">;",
"Landroid/view/View$OnClickListener;"
}
.end annotation
# instance fields
.field private B:Ljava/util/concurrent/atomic/AtomicBoolean;
.field private final C:Lcom/vk/music/common/MusicPlaybackLaunchContext;
.field private final D:Lcom/vk/common/k/PodcastTimeCodeClickListener;
.field private final E:Lcom/vtosters/lite/LinkParserParams;
.field private final c:Landroid/widget/TextView;
.field private final d:Landroid/widget/TextView;
.field private final e:Landroid/widget/TextView;
.field private final f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
.field private final g:Landroid/view/View;
.field private final h:Landroid/view/View;
# direct methods
.method public constructor <init>(Landroid/view/ViewGroup;Lcom/vk/music/player/PlayerModel;)V
.locals 2
const v0, 0x7f0d037c
.line 1
invoke-direct {p0, v0, p1}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;-><init>(ILandroid/view/ViewGroup;)V
.line 2
new-instance v0, Ljava/util/concurrent/atomic/AtomicBoolean;
const/4 v1, 0x0
invoke-direct {v0, v1}, Ljava/util/concurrent/atomic/AtomicBoolean;-><init>(Z)V
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->B:Ljava/util/concurrent/atomic/AtomicBoolean;
.line 3
sget-object v0, Lcom/vk/music/common/MusicPlaybackLaunchContext;->p0:Lcom/vk/music/common/MusicPlaybackLaunchContext;
const/16 v1, 0x80
invoke-virtual {v0, v1}, Lcom/vk/music/common/MusicPlaybackLaunchContext;->h(I)Lcom/vk/music/common/MusicPlaybackLaunchContext;
move-result-object v0
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->C:Lcom/vk/music/common/MusicPlaybackLaunchContext;
.line 4
new-instance v0, Lcom/vtosters/lite/LinkParserParams;
invoke-direct {v0}, Lcom/vtosters/lite/LinkParserParams;-><init>()V
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->E:Lcom/vtosters/lite/LinkParserParams;
const v0, 0x7f0a0d80
.line 5
invoke-virtual {p0, v0}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v0
check-cast v0, Landroid/widget/TextView;
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->c:Landroid/widget/TextView;
const v0, 0x7f0a00ba
.line 6
invoke-virtual {p0, v0}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v0
check-cast v0, Landroid/widget/TextView;
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->d:Landroid/widget/TextView;
.line 7
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->d:Landroid/widget/TextView;
invoke-virtual {v0, p0}, Landroid/widget/TextView;->setOnClickListener(Landroid/view/View$OnClickListener;)V
const v0, 0x7f0a0795
.line 8
invoke-virtual {p0, v0}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v0
check-cast v0, Landroid/widget/TextView;
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->e:Landroid/widget/TextView;
const v0, 0x7f0a0af0
.line 9
invoke-virtual {p0, v0}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v0
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->g:Landroid/view/View;
const v0, 0x7f0a02d0
.line 10
invoke-virtual {p0, v0}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v0
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->h:Landroid/view/View;
.line 11
new-instance v0, Lcom/vtosters/lite/ui/MusicErrorViewHelper$b;
const v1, 0x7f0a03c1
invoke-virtual {p0, v1}, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->i(I)Landroid/view/View;
move-result-object v1
invoke-direct {v0, v1}, Lcom/vtosters/lite/ui/MusicErrorViewHelper$b;-><init>(Landroid/view/View;)V
invoke-virtual {v0}, Lcom/vtosters/lite/ui/MusicErrorViewHelper$b;->a()Lcom/vtosters/lite/ui/MusicErrorViewHelper;
move-result-object v0
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
.line 12
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
new-instance v1, Lcom/vk/music/view/x/LyricsHolder$a;
invoke-direct {v1, p0}, Lcom/vk/music/view/x/LyricsHolder$a;-><init>(Lcom/vk/music/view/x/LyricsHolder;)V
invoke-virtual {v0, v1}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->a(Landroid/view/View$OnClickListener;)V
.line 13
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
const/16 v1, 0x8
invoke-virtual {v0, v1}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->b(I)V
.line 14
new-instance v0, Lcom/vk/common/k/PodcastTimeCodeClickListener;
invoke-virtual {p1}, Landroid/view/ViewGroup;->getContext()Landroid/content/Context;
move-result-object p1
invoke-direct {v0, p1, p2}, Lcom/vk/common/k/PodcastTimeCodeClickListener;-><init>(Landroid/content/Context;Lcom/vk/music/player/PlayerModel;)V
iput-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->D:Lcom/vk/common/k/PodcastTimeCodeClickListener;
.line 15
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder;->E:Lcom/vtosters/lite/LinkParserParams;
const/16 p2, 0x38b
invoke-virtual {p1, p2}, Lcom/vtosters/lite/LinkParserParams;->b(I)V
.line 16
iget-object p1, p0, Lcom/vk/music/view/x/LyricsHolder;->E:Lcom/vtosters/lite/LinkParserParams;
iget-object p2, p0, Lcom/vk/music/view/x/LyricsHolder;->D:Lcom/vk/common/k/PodcastTimeCodeClickListener;
invoke-virtual {p1, p2}, Lcom/vtosters/lite/LinkParserParams;->a(Lcom/vk/common/k/TimeCodeClickListener;)V
return-void
.end method
.method static synthetic a(Lcom/vk/music/view/x/LyricsHolder;)Lcom/vtosters/lite/ui/MusicErrorViewHelper;
.locals 0
.line 1
iget-object p0, p0, Lcom/vk/music/view/x/LyricsHolder;->f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
return-object p0
.end method
.method static synthetic b(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
.locals 0
.line 1
iget-object p0, p0, Lcom/vk/music/view/x/LyricsHolder;->g:Landroid/view/View;
return-object p0
.end method
.method static synthetic c(Lcom/vk/music/view/x/LyricsHolder;)Landroid/view/View;
.locals 0
.line 1
iget-object p0, p0, Lcom/vk/music/view/x/LyricsHolder;->h:Landroid/view/View;
return-object p0
.end method
.method static synthetic d(Lcom/vk/music/view/x/LyricsHolder;)Ljava/util/concurrent/atomic/AtomicBoolean;
.locals 0
.line 1
iget-object p0, p0, Lcom/vk/music/view/x/LyricsHolder;->B:Ljava/util/concurrent/atomic/AtomicBoolean;
return-object p0
.end method
# virtual methods
.method public a(Lcom/vk/music/dto/PlayerInfo;)V
.locals 2
.line 2
iget-object p1, p1, Lcom/vk/music/dto/PlayerInfo;->a:Lcom/vk/music/player/TrackInfo;
if-eqz p1, :cond_2
.line 3
invoke-virtual {p1}, Lcom/vk/music/player/TrackInfo;->n()Z
move-result v0
if-eqz v0, :cond_2
invoke-virtual {p1}, Lcom/vk/music/player/TrackInfo;->e()Lcom/vk/dto/music/MusicTrack;
move-result-object v0
iget v0, v0, Lcom/vk/dto/music/MusicTrack;->G:I
if-eqz v0, :cond_2
.line 4
invoke-virtual {p1}, Lcom/vk/music/player/TrackInfo;->e()Lcom/vk/dto/music/MusicTrack;
move-result-object p1
.line 5
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->c:Landroid/widget/TextView;
iget-object v1, p1, Lcom/vk/dto/music/MusicTrack;->f:Ljava/lang/String;
invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
.line 6
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->c:Landroid/widget/TextView;
const/4 v1, 0x1
invoke-virtual {v0, v1}, Landroid/widget/TextView;->setSelected(Z)V
.line 7
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->d:Landroid/widget/TextView;
iget-object v1, p1, Lcom/vk/dto/music/MusicTrack;->C:Ljava/lang/String;
invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
.line 8
invoke-virtual {p1}, Lcom/vk/dto/music/MusicTrack;->D1()Z
move-result v0
if-eqz v0, :cond_0
.line 9
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->E:Lcom/vtosters/lite/LinkParserParams;
iget v1, p1, Lcom/vk/dto/music/MusicTrack;->h:I
invoke-virtual {v0, v1}, Lcom/vtosters/lite/LinkParserParams;->a(I)V
.line 10
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->D:Lcom/vk/common/k/PodcastTimeCodeClickListener;
iget-object v1, p0, Lcom/vk/music/view/x/LyricsHolder;->C:Lcom/vk/music/common/MusicPlaybackLaunchContext;
invoke-virtual {v0, p1, v1}, Lcom/vk/common/k/PodcastTimeCodeClickListener;->a(Lcom/vk/dto/music/MusicTrack;Lcom/vk/music/common/MusicPlaybackLaunchContext;)Lcom/vk/common/k/PodcastTimeCodeClickListener;
.line 11
iget-object p1, p1, Lcom/vk/dto/music/MusicTrack;->H:Ljava/lang/String;
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->E:Lcom/vtosters/lite/LinkParserParams;
invoke-static {p1, v0}, Lcom/vk/common/links/LinkParser;->a(Ljava/lang/CharSequence;Lcom/vtosters/lite/LinkParserParams;)Ljava/lang/CharSequence;
move-result-object p1
goto :goto_0
.line 12
:cond_0
iget-object p1, p1, Lcom/vk/dto/music/MusicTrack;->H:Ljava/lang/String;
.line 13
:goto_0
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->e:Landroid/widget/TextView;
invoke-virtual {v0}, Landroid/widget/TextView;->getText()Ljava/lang/CharSequence;
move-result-object v0
invoke-static {v0, p1}, Ljava/util/Objects;->equals(Ljava/lang/Object;Ljava/lang/Object;)Z
move-result v0
if-nez v0, :cond_1
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->h:Landroid/view/View;
const/4 v1, 0x0
invoke-virtual {v0, v1}, Landroid/view/View;->setScrollY(I)V
.line 14
:cond_1
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->e:Landroid/widget/TextView;
invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
.line 15
invoke-virtual {p0}, Lcom/vk/music/view/x/LyricsHolder;->g0()V
:cond_2
return-void
.end method
.method public bridge synthetic b(Ljava/lang/Object;)V
.locals 0
.line 2
check-cast p1, Lcom/vk/music/dto/PlayerInfo;
invoke-virtual {p0, p1}, Lcom/vk/music/view/x/LyricsHolder;->a(Lcom/vk/music/dto/PlayerInfo;)V
return-void
.end method
.method public g0()V
.locals 4
.line 1
iget-object v0, p0, Lcom/vk/music/view/x/LyricsHolder;->B:Ljava/util/concurrent/atomic/AtomicBoolean;
invoke-virtual {v0}, Ljava/util/concurrent/atomic/AtomicBoolean;->get()Z
move-result v0
if-nez v0, :cond_0
.line 2
iget-object v0, p0, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->b:Ljava/lang/Object;
check-cast v0, Lcom/vk/music/dto/PlayerInfo;
iget-object v0, v0, Lcom/vk/music/dto/PlayerInfo;->a:Lcom/vk/music/player/TrackInfo;
if-eqz v0, :cond_0
.line 3
invoke-virtual {v0}, Lcom/vk/music/player/TrackInfo;->n()Z
move-result v1
if-eqz v1, :cond_0
invoke-virtual {v0}, Lcom/vk/music/player/TrackInfo;->e()Lcom/vk/dto/music/MusicTrack;
move-result-object v1
iget v1, v1, Lcom/vk/dto/music/MusicTrack;->G:I
if-eqz v1, :cond_0
.line 4
invoke-virtual {v0}, Lcom/vk/music/player/TrackInfo;->e()Lcom/vk/dto/music/MusicTrack;
move-result-object v0
.line 5
iget v1, v0, Lcom/vk/dto/music/MusicTrack;->G:I
if-lez v1, :cond_0
iget-object v1, v0, Lcom/vk/dto/music/MusicTrack;->H:Ljava/lang/String;
invoke-static {v1}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z
move-result v1
if-eqz v1, :cond_0
.line 6
iget-object v1, p0, Lcom/vk/music/view/x/LyricsHolder;->B:Ljava/util/concurrent/atomic/AtomicBoolean;
const/4 v2, 0x1
invoke-virtual {v1, v2}, Ljava/util/concurrent/atomic/AtomicBoolean;->set(Z)V
.line 7
iget-object v1, p0, Lcom/vk/music/view/x/LyricsHolder;->f:Lcom/vtosters/lite/ui/MusicErrorViewHelper;
const/16 v2, 0x8
invoke-virtual {v1, v2}, Lcom/vtosters/lite/ui/MusicErrorViewHelper;->b(I)V
.line 8
iget-object v1, p0, Lcom/vk/music/view/x/LyricsHolder;->g:Landroid/view/View;
const/4 v3, 0x0
invoke-static {v1, v3}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
.line 9
iget-object v1, p0, Lcom/vk/music/view/x/LyricsHolder;->h:Landroid/view/View;
invoke-static {v1, v2}, Lcom/vtosters/lite/ViewUtils;->a(Landroid/view/View;I)V
.line 10
new-instance v1, Lcom/vk/api/audio/AudioGetLyrics;
iget v2, v0, Lcom/vk/dto/music/MusicTrack;->G:I
invoke-direct {v1, v2}, Lcom/vk/api/audio/AudioGetLyrics;-><init>(I)V
new-instance v2, Lcom/vk/music/view/x/LyricsHolder$b;
invoke-direct {v2, p0, v0}, Lcom/vk/music/view/x/LyricsHolder$b;-><init>(Lcom/vk/music/view/x/LyricsHolder;Lcom/vk/dto/music/MusicTrack;)V
invoke-virtual {v1, v2}, Lcom/vk/api/base/ApiRequest;->a(Lcom/vk/api/base/ApiCallback;)Lcom/vk/api/base/ApiCallbackDisposable;
move-result-object v0
.line 11
invoke-virtual {v0}, Lcom/vk/api/base/ApiCallbackDisposable;->a()Lio/reactivex/disposables/Disposable;
:cond_0
return-void
.end method
.method public onClick(Landroid/view/View;)V
.locals 2
.line 1
iget-object v0, p0, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->b:Ljava/lang/Object;
move-object v1, v0
check-cast v1, Lcom/vk/music/dto/PlayerInfo;
iget-object v1, v1, Lcom/vk/music/dto/PlayerInfo;->a:Lcom/vk/music/player/TrackInfo;
if-eqz v1, :cond_0
check-cast v0, Lcom/vk/music/dto/PlayerInfo;
iget-object v0, v0, Lcom/vk/music/dto/PlayerInfo;->a:Lcom/vk/music/player/TrackInfo;
invoke-virtual {v0}, Lcom/vk/music/player/TrackInfo;->e()Lcom/vk/dto/music/MusicTrack;
move-result-object v0
goto :goto_0
:cond_0
const/4 v0, 0x0
.line 2
:goto_0
invoke-virtual {p1}, Landroid/view/View;->getContext()Landroid/content/Context;
move-result-object p1
invoke-static {p1}, Lcom/vk/core/util/ContextExtKt;->e(Landroid/content/Context;)Landroid/app/Activity;
move-result-object p1
if-eqz v0, :cond_3
if-nez p1, :cond_1
goto :goto_1
.line 3
:cond_1
invoke-virtual {v0}, Lcom/vk/dto/music/MusicTrack;->D1()Z
move-result v1
if-eqz v1, :cond_2
.line 4
new-instance v1, Lcom/vk/profile/ui/BaseProfileFragment$z;
iget v0, v0, Lcom/vk/dto/music/MusicTrack;->e:I
invoke-direct {v1, v0}, Lcom/vk/profile/ui/BaseProfileFragment$z;-><init>(I)V
invoke-virtual {v1, p1}, Lcom/vk/navigation/Navigator;->a(Landroid/content/Context;)V
goto :goto_1
.line 5
:cond_2
iget-object v1, p0, Lcom/vtosters/lite/ui/holder/RecyclerHolder;->b:Ljava/lang/Object;
check-cast v1, Lcom/vk/music/dto/PlayerInfo;
iget-object v1, v1, Lcom/vk/music/dto/PlayerInfo;->d:Lcom/vk/music/common/MusicPlaybackLaunchContext;
invoke-static {p1, v0, v1}, Lcom/vk/music/artists/chooser/MusicArtistSelector;->a(Landroid/app/Activity;Lcom/vk/dto/music/MusicTrack;Lcom/vk/music/stats/MusicStatsRefer;)V
:cond_3
:goto_1
return-void
.end method
@@ -413,10 +413,6 @@
invoke-virtual {p2, v1}, Landroidx/appcompat/widget/Toolbar;->setOnMenuItemClickListener(Landroidx/appcompat/widget/Toolbar$OnMenuItemClickListener;)V
const v1, 0x0
invoke-static {p2, v1, v1}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
.line 6
new-instance p2, Lcom/vk/superapp/SuperAppAdapter;
@@ -2524,6 +2524,8 @@
.line 2
invoke-super {p0}, Landroid/app/Application;->onCreate()V
invoke-static/range {p0 .. p0}, Lru/vtosters/lite/themes/ThemesManager;->init(Landroid/app/Application;)V
return-void
.line 3
@@ -2535,6 +2537,8 @@
.line 4
invoke-super {p0}, Landroid/app/Application;->onCreate()V
invoke-static/range {p0 .. p0}, Lru/vtosters/lite/themes/ThemesManager;->init(Landroid/app/Application;)V
.line 5
invoke-direct {p0}, Lcom/vtosters/lite/VKApplication;->D()V
@@ -2544,6 +2548,8 @@
:cond_1
invoke-super {p0}, Landroid/app/Application;->onCreate()V
invoke-static/range {p0 .. p0}, Lru/vtosters/lite/themes/ThemesManager;->init(Landroid/app/Application;)V
.line 8
new-instance v0, Lcom/vk/core/util/TimeLogger;
@@ -990,10 +990,6 @@
invoke-virtual {p0, p1}, Landroidx/appcompat/widget/Toolbar;->setNavigationContentDescription(I)V
:cond_1
const v0, 0x0
invoke-static {p0, v0, v0}, Lru/vtosters/lite/themes/ViewInjector;->inject(Landroid/view/View;IZ)Landroid/view/View;
return-void
.end method
@@ -2161,7 +2161,7 @@
const v0, 0x7f0600f7
invoke-static {v0}, Lru/vtosters/lite/utils/ThemesUtils;->getColor2(I)I
invoke-virtual {p2, v0}, Landroid/content/res/Resources;->getColor(I)I
move-result p2