From 314e4de27d9c66cb4f4818117d0ed9b19b4614e9 Mon Sep 17 00:00:00 2001 From: Vologhat <47892605+Vologhat@users.noreply.github.com> Date: Sun, 19 Feb 2023 00:37:48 +0300 Subject: [PATCH] new way of color modification --- app/build.gradle | 1 + .../gmscore/tools/apk/arsc/ArscBlamer.java | 277 +++++++ .../apk/arsc/BinaryResourceConfiguration.java | 745 ++++++++++++++++++ .../tools/apk/arsc/BinaryResourceFile.java | 74 ++ .../apk/arsc/BinaryResourceIdentifier.java | 99 +++ .../tools/apk/arsc/BinaryResourceString.java | 164 ++++ .../tools/apk/arsc/BinaryResourceValue.java | 151 ++++ .../devrel/gmscore/tools/apk/arsc/Chunk.java | 301 +++++++ .../tools/apk/arsc/ChunkWithChunks.java | 73 ++ .../gmscore/tools/apk/arsc/LibraryChunk.java | 136 ++++ .../gmscore/tools/apk/arsc/PackageChunk.java | 239 ++++++ .../gmscore/tools/apk/arsc/PackageUtils.java | 67 ++ .../apk/arsc/ResourceEntryStatsCollector.java | 280 +++++++ .../tools/apk/arsc/ResourceTableChunk.java | 92 +++ .../tools/apk/arsc/SerializableResource.java | 41 + .../tools/apk/arsc/StringPoolChunk.java | 423 ++++++++++ .../gmscore/tools/apk/arsc/TypeChunk.java | 449 +++++++++++ .../gmscore/tools/apk/arsc/TypeSpecChunk.java | 101 +++ .../gmscore/tools/apk/arsc/UnknownChunk.java | 61 ++ .../gmscore/tools/apk/arsc/XmlAttribute.java | 148 ++++ .../gmscore/tools/apk/arsc/XmlCdataChunk.java | 73 ++ .../gmscore/tools/apk/arsc/XmlChunk.java | 48 ++ .../tools/apk/arsc/XmlEndElementChunk.java | 75 ++ .../tools/apk/arsc/XmlNamespaceChunk.java | 70 ++ .../tools/apk/arsc/XmlNamespaceEndChunk.java | 33 + .../apk/arsc/XmlNamespaceStartChunk.java | 33 + .../gmscore/tools/apk/arsc/XmlNodeChunk.java | 96 +++ .../tools/apk/arsc/XmlResourceMapChunk.java | 86 ++ .../tools/apk/arsc/XmlStartElementChunk.java | 145 ++++ .../DialogGetMentionSuggestionCmd.java | 239 ++++++ .../com/vk/im/engine/models/MemberType.java | 32 + .../vk/im/ui/q/h/e/MentionsController.java | 166 ++++ .../vtosters/lite/deviceinfo/OEMDetector.java | 4 +- .../vtosters/lite/hooks/DockBarInjector.java | 3 +- .../lite/hooks/MainActivityInjector.java | 13 +- .../ru/vtosters/lite/music/M3UDownloader.java | 2 +- .../lite/music/converter/ts/TSMerger.java | 2 +- .../music/downloader/ThumbnailDownloader.java | 2 +- .../ru/vtosters/lite/themes/ThemesCore.java | 33 - .../ru/vtosters/lite/themes/ThemesHacks.java | 54 -- .../vtosters/lite/themes/ThemesManager.java | 241 ++++++ .../ru/vtosters/lite/themes/VTLResources.java | 92 --- .../ru/vtosters/lite/themes/ViewInjector.java | 43 - .../vtosters/lite/themes/hooks/BaseHook.java | 7 - .../lite/themes/hooks/ButtonHook.java | 29 - .../lite/themes/hooks/ImageViewHook.java | 24 - .../lite/themes/hooks/ProgressWheelHook.java | 21 - .../lite/themes/hooks/TabLayoutHook.java | 21 - .../lite/themes/hooks/TextViewHook.java | 136 ---- .../lite/themes/hooks/ToolbarHook.java | 29 - .../lite/themes/hooks/VKImageViewHook.java | 30 - .../themes/loaders/AssetManagerHelper.java | 58 ++ .../lite/themes/loaders/ResourcesLoader.java | 380 +++++++++ .../lite/themes/palettes/PalettesManager.java | 2 +- .../lite/themes/utils/ArscEditor.java | 37 + .../lite/themes/utils/RecolorUtils.java | 18 - .../lite/themes/utils/VkUiThemer.java | 85 -- .../ui/fragments/DockBarEditorFragment.java | 5 - .../ui/fragments/SuperAppEditorFragment.java | 5 - .../lite/ui/fragments/ThemesFragment.java | 79 +- .../ru/vtosters/lite/utils/AndroidUtils.java | 4 +- .../java/ru/vtosters/lite/utils/IOUtils.java | 91 +-- .../vtosters/lite/utils/ReflectionUtils.java | 287 ++----- .../ru/vtosters/lite/utils/ThemesUtils.java | 133 +--- smali/res/xml/preferences_themes.xml | 4 + .../appcompat/app/AppCompatDelegateImpl.smali | 2 +- .../appcompat/widget/ResourcesWrapper.smali | 4 +- .../androidx/cardview/widget/CardView.smali | 4 +- .../widget/RoundRectDrawableWithShadow.smali | 4 +- .../androidx/core/content/ContextCompat.smali | 53 +- .../core/content/res/ResourcesCompat.smali | 18 +- .../graphics/drawable/DrawableCompat.smali | 4 - .../com/vk/articles/ArticleFragment.smali | 4 +- .../holders/stickers/StickerCatalogVh.smali | 3 - .../com/vk/core/ui/VKLayoutInflater.smali | 24 - .../vk/core/ui/themes/VKThemeHelper$b.smali | 30 +- .../com/vk/core/util/ContextExtKt.smali | 13 - .../VerifyInfoHelper$colorLightBlue$2.smali | 6 +- .../com/vk/extensions/ViewExtKt.smali | 4 - .../impl/vk/VkDialogsHeaderComponent.smali | 3 - .../w/DefaultMentionSpanProvider.smali | 6 +- .../com/vk/superapp/SuperAppFragment.smali | 4 - .../com/vtosters/lite/VKApplication.smali | 6 + .../com/vtosters/lite/ViewUtils.smali | 4 - .../fragments/photos/PhotoListFragment.smali | 2 +- .../lite/ui/widget/WaveRecordCircleView.smali | 2 +- 86 files changed, 6007 insertions(+), 1185 deletions(-) create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ArscBlamer.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceConfiguration.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceFile.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceIdentifier.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceString.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceValue.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/Chunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ChunkWithChunks.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/LibraryChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageUtils.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceEntryStatsCollector.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceTableChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/SerializableResource.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/StringPoolChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeSpecChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/UnknownChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlAttribute.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlCdataChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlEndElementChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceEndChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceStartChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNodeChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlResourceMapChunk.java create mode 100644 app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlStartElementChunk.java create mode 100644 app/src/main/java/com/vk/im/engine/commands/dialogs/DialogGetMentionSuggestionCmd.java create mode 100644 app/src/main/java/com/vk/im/engine/models/MemberType.java create mode 100644 app/src/main/java/com/vk/im/ui/q/h/e/MentionsController.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/ThemesHacks.java create mode 100644 app/src/main/java/ru/vtosters/lite/themes/ThemesManager.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/VTLResources.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/ViewInjector.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/BaseHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/ButtonHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/ImageViewHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/ProgressWheelHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/TabLayoutHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/TextViewHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/ToolbarHook.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/hooks/VKImageViewHook.java create mode 100644 app/src/main/java/ru/vtosters/lite/themes/loaders/AssetManagerHelper.java create mode 100644 app/src/main/java/ru/vtosters/lite/themes/loaders/ResourcesLoader.java create mode 100644 app/src/main/java/ru/vtosters/lite/themes/utils/ArscEditor.java delete mode 100644 app/src/main/java/ru/vtosters/lite/themes/utils/VkUiThemer.java diff --git a/app/build.gradle b/app/build.gradle index d8a4d13b8..f7e6ee2cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) { diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ArscBlamer.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ArscBlamer.java new file mode 100644 index 000000000..d6423103e --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ArscBlamer.java @@ -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: + * + * + */ +public class ArscBlamer { + + /** Maps package key pool indices to blamed resources. */ + private final Map[]> keyToBlame = new HashMap<>(); + + /** Maps types to blamed resources. */ + private final Map[]> typeToBlame = new HashMap<>(); + + /** Maps package to blamed resources. */ + private final Multimap packageToBlame = HashMultimap.create(); + + /** Maps string indices to blamed resources. */ + private final List[] stringToBlame; + + /** Maps type chunk entries to blamed resources. */ + private final Multimap typeEntryToBlame = HashMultimap.create(); + + /** Maps resources to the type chunk entries they reference. */ + private Multimap resourceEntries; + + /** Maps resources which have no base config to the type chunk entries they reference. */ + private Multimap baselessKeys; + + /** Contains all of the type chunks in {@link #resourceTable}. */ + private List 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 entries = getResourceEntries(); + for (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[]> 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 getAllResourceValues(TypeChunk.Entry entry) { + Set values = new HashSet(); + 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[]> getKeyToBlamedResources() { + return Collections.unmodifiableMap(keyToBlame); + } + + /** Must first call {@link #blame}. */ + public Map[]> getTypeToBlamedResources() { + return Collections.unmodifiableMap(typeToBlame); + } + + /** Must first call {@link #blame}. */ + public Multimap getPackageToBlamedResources() { + return Multimaps.unmodifiableMultimap(packageToBlame); + } + + /** Must first call {@link #blame}. */ + public List[] getStringToBlamedResources() { + return stringToBlame; + } + + /** Must first call {@link #blame}. */ + public Multimap getTypeEntryToBlamedResources() { + return Multimaps.unmodifiableMultimap(typeEntryToBlame); + } + + /** Returns a multimap of keys for which there is no default resource. */ + public Multimap getBaselessKeys() { + if (baselessKeys != null) { + return baselessKeys; + } + Multimap result = HashMultimap.create(); + for (Entry> entry + : getResourceEntries().asMap().entrySet()) { + Collection 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 getResourceEntries() { + if (resourceEntries != null) { + return resourceEntries; + } + Multimap 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 getTypeChunks() { + if (typeChunks != null) { + return typeChunks; + } + List result = new ArrayList<>(); + for (PackageChunk packageChunk : resourceTable.getPackages()) { + for (TypeChunk typeChunk : packageChunk.getTypeChunks()) { + result.add(typeChunk); + } + } + typeChunks = result; + return result; + } + + private boolean hasBaseConfiguration(Collection entries) { + for (TypeChunk.Entry entry : entries) { + if (entry.parent().getConfiguration().isDefault()) { + return true; + } + } + return false; + } + + private static List[] 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 { + + 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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceConfiguration.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceConfiguration.java new file mode 100644 index 000000000..922871833 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceConfiguration.java @@ -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 DENSITY_DPI_VALUES = + ImmutableMap.builder() + .put(DENSITY_DPI_UNDEFINED, "") + .put(DENSITY_DPI_LDPI, "ldpi") + .put(DENSITY_DPI_MDPI, "mdpi") + .put(DENSITY_DPI_TVDPI, "tvdpi") + .put(DENSITY_DPI_HDPI, "hdpi") + .put(DENSITY_DPI_XHDPI, "xhdpi") + .put(DENSITY_DPI_XXHDPI, "xxhdpi") + .put(DENSITY_DPI_XXXHDPI, "xxxhdpi") + .put(DENSITY_DPI_ANY, "anydpi") + .put(DENSITY_DPI_NONE, "nodpi") + .build(); + + private static final int KEYBOARD_NOKEYS = 1; + private static final int KEYBOARD_QWERTY = 2; + private static final int KEYBOARD_12KEY = 3; + private static final Map KEYBOARD_VALUES = ImmutableMap.of( + KEYBOARD_NOKEYS, "nokeys", + KEYBOARD_QWERTY, "qwerty", + KEYBOARD_12KEY, "12key"); + + private static final int KEYBOARDHIDDEN_MASK = 0x03; + private static final int KEYBOARDHIDDEN_NO = 1; + private static final int KEYBOARDHIDDEN_YES = 2; + private static final int KEYBOARDHIDDEN_SOFT = 3; + private static final Map KEYBOARDHIDDEN_VALUES = ImmutableMap.of( + KEYBOARDHIDDEN_NO, "keysexposed", + KEYBOARDHIDDEN_YES, "keyshidden", + KEYBOARDHIDDEN_SOFT, "keyssoft"); + + private static final int NAVIGATION_NONAV = 1; + private static final int NAVIGATION_DPAD = 2; + private static final int NAVIGATION_TRACKBALL = 3; + private static final int NAVIGATION_WHEEL = 4; + private static final Map NAVIGATION_VALUES = ImmutableMap.of( + NAVIGATION_NONAV, "nonav", + NAVIGATION_DPAD, "dpad", + NAVIGATION_TRACKBALL, "trackball", + NAVIGATION_WHEEL, "wheel"); + + private static final int NAVIGATIONHIDDEN_MASK = 0x0C; + private static final int NAVIGATIONHIDDEN_NO = 0x04; + private static final int NAVIGATIONHIDDEN_YES = 0x08; + private static final Map NAVIGATIONHIDDEN_VALUES = ImmutableMap.of( + NAVIGATIONHIDDEN_NO, "navexposed", + NAVIGATIONHIDDEN_YES, "navhidden"); + + private static final int ORIENTATION_PORTRAIT = 0x01; + private static final int ORIENTATION_LANDSCAPE = 0x02; + private static final Map ORIENTATION_VALUES = ImmutableMap.of( + ORIENTATION_PORTRAIT, "port", + ORIENTATION_LANDSCAPE, "land"); + + private static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0xC0; + private static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; + private static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; + private static final Map SCREENLAYOUT_LAYOUTDIR_VALUES = ImmutableMap.of( + SCREENLAYOUT_LAYOUTDIR_LTR, "ldltr", + SCREENLAYOUT_LAYOUTDIR_RTL, "ldrtl"); + + private static final int SCREENLAYOUT_LONG_MASK = 0x30; + private static final int SCREENLAYOUT_LONG_NO = 0x10; + private static final int SCREENLAYOUT_LONG_YES = 0x20; + private static final Map SCREENLAYOUT_LONG_VALUES = ImmutableMap.of( + SCREENLAYOUT_LONG_NO, "notlong", + SCREENLAYOUT_LONG_YES, "long"); + + private static final int SCREENLAYOUT_ROUND_MASK = 0x03; + private static final int SCREENLAYOUT_ROUND_NO = 0x01; + private static final int SCREENLAYOUT_ROUND_YES = 0x02; + private static final Map SCREENLAYOUT_ROUND_VALUES = ImmutableMap.of( + SCREENLAYOUT_ROUND_NO, "notround", + SCREENLAYOUT_ROUND_YES, "round"); + + private static final int SCREENLAYOUT_SIZE_MASK = 0x0F; + private static final int SCREENLAYOUT_SIZE_SMALL = 0x01; + private static final int SCREENLAYOUT_SIZE_NORMAL = 0x02; + private static final int SCREENLAYOUT_SIZE_LARGE = 0x03; + private static final int SCREENLAYOUT_SIZE_XLARGE = 0x04; + private static final Map SCREENLAYOUT_SIZE_VALUES = ImmutableMap.of( + SCREENLAYOUT_SIZE_SMALL, "small", + SCREENLAYOUT_SIZE_NORMAL, "normal", + SCREENLAYOUT_SIZE_LARGE, "large", + SCREENLAYOUT_SIZE_XLARGE, "xlarge"); + + private static final int TOUCHSCREEN_NOTOUCH = 1; + private static final int TOUCHSCREEN_FINGER = 3; + private static final Map TOUCHSCREEN_VALUES = ImmutableMap.of( + TOUCHSCREEN_NOTOUCH, "notouch", + TOUCHSCREEN_FINGER, "finger"); + + private static final int UI_MODE_NIGHT_MASK = 0x30; + private static final int UI_MODE_NIGHT_NO = 0x10; + private static final int UI_MODE_NIGHT_YES = 0x20; + private static final Map UI_MODE_NIGHT_VALUES = ImmutableMap.of( + UI_MODE_NIGHT_NO, "notnight", + UI_MODE_NIGHT_YES, "night"); + + private static final int UI_MODE_TYPE_MASK = 0x0F; + private static final int UI_MODE_TYPE_DESK = 0x02; + private static final int UI_MODE_TYPE_CAR = 0x03; + private static final int UI_MODE_TYPE_TELEVISION = 0x04; + private static final int UI_MODE_TYPE_APPLIANCE = 0x05; + private static final int UI_MODE_TYPE_WATCH = 0x06; + private static final 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 UI_MODE_TYPE_VALUES = + ImmutableMap.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 parts = toStringParts().values(); + parts.removeAll(Collections.singleton("")); + return Joiner.on('-').join(parts); + } + + /** + * Returns a map of the configuration parts for {@link #toString}. + * + *

If a configuration part is not defined for this {@link BinaryResourceConfiguration}, its value + * will be the empty string. + */ + public final Map toStringParts() { + Map result = new LinkedHashMap<>(); // Preserve order for #toString(). + result.put(Type.MCC, mcc() != 0 ? "mcc" + mcc() : ""); + result.put(Type.MNC, mnc() != 0 ? "mnc" + mnc() : ""); + result.put(Type.LANGUAGE_STRING, !languageString().isEmpty() ? "" + languageString() : ""); + result.put(Type.REGION_STRING, !regionString().isEmpty() ? "r" + regionString() : ""); + result.put(Type.SCREEN_LAYOUT_DIRECTION, + getOrDefault(SCREENLAYOUT_LAYOUTDIR_VALUES, screenLayoutDirection(), "")); + result.put(Type.SMALLEST_SCREEN_WIDTH_DP, + smallestScreenWidthDp() != 0 ? "sw" + smallestScreenWidthDp() + "dp" : ""); + result.put(Type.SCREEN_WIDTH_DP, screenWidthDp() != 0 ? "w" + screenWidthDp() + "dp" : ""); + result.put(Type.SCREEN_HEIGHT_DP, screenHeightDp() != 0 ? "h" + screenHeightDp() + "dp" : ""); + result.put(Type.SCREEN_LAYOUT_SIZE, + getOrDefault(SCREENLAYOUT_SIZE_VALUES, screenLayoutSize(), "")); + result.put(Type.SCREEN_LAYOUT_LONG, + getOrDefault(SCREENLAYOUT_LONG_VALUES, screenLayoutLong(), "")); + result.put(Type.SCREEN_LAYOUT_ROUND, + getOrDefault(SCREENLAYOUT_ROUND_VALUES, screenLayoutRound(), "")); + result.put(Type.ORIENTATION, getOrDefault(ORIENTATION_VALUES, orientation(), "")); + result.put(Type.UI_MODE_TYPE, getOrDefault(UI_MODE_TYPE_VALUES, uiModeType(), "")); + result.put(Type.UI_MODE_NIGHT, getOrDefault(UI_MODE_NIGHT_VALUES, uiModeNight(), "")); + result.put(Type.DENSITY_DPI, getOrDefault(DENSITY_DPI_VALUES, density(), density() + "dpi")); + result.put(Type.TOUCHSCREEN, getOrDefault(TOUCHSCREEN_VALUES, touchscreen(), "")); + result.put(Type.KEYBOARD_HIDDEN, getOrDefault(KEYBOARDHIDDEN_VALUES, keyboardHidden(), "")); + result.put(Type.KEYBOARD, getOrDefault(KEYBOARD_VALUES, keyboard(), "")); + result.put(Type.NAVIGATION_HIDDEN, + getOrDefault(NAVIGATIONHIDDEN_VALUES, navigationHidden(), "")); + result.put(Type.NAVIGATION, getOrDefault(NAVIGATION_VALUES, navigation(), "")); + result.put(Type.SDK_VERSION, sdkVersion() != 0 ? "v" + sdkVersion() : ""); + return result; + } + + private V getOrDefault(Map map, K key, V defaultValue) { + // TODO(acornwall): Remove this when Java 8's Map#getOrDefault is available. + // Null is not returned, even if the map contains a key whose value is null. This is intended. + V value = map.get(key); + return value != null ? value : defaultValue; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceFile.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceFile.java new file mode 100644 index 000000000..96f98784f --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceFile.java @@ -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 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 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(); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceIdentifier.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceIdentifier.java new file mode 100644 index 000000000..a3b80ab77 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceIdentifier.java @@ -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); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceString.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceString.java new file mode 100644 index 000000000..4da4e2f51 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceString.java @@ -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. + * + *

Here's an example UTF-8-encoded string of ab©: + *

+   * 03 04 61 62 C2 A9 00
+   * ^ Offset should be here
+   * 
+ * + * @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). + * + *

Here's an example UTF-8-encoded string of ab©: + *

03 04 61 62 C2 A9 00
+ * + * @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; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceValue.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceValue.java new file mode 100644 index 000000000..b66a53157 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/BinaryResourceValue.java @@ -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 FROM_BYTE; + + static { + Builder 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); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/Chunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/Chunk.java new file mode 100644 index 000000000..0d2661486 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/Chunk.java @@ -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 FROM_SHORT; + + static { + Builder 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). + * + *

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; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ChunkWithChunks.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ChunkWithChunks.java new file mode 100644 index 000000000..11ba3a41f --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ChunkWithChunks.java @@ -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 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 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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/LibraryChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/LibraryChunk.java new file mode 100644 index 000000000..38236de8b --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/LibraryChunk.java @@ -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 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 enumerateEntries(ByteBuffer buffer) { + List 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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageChunk.java new file mode 100644 index 000000000..cc8c2e32f --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageChunk.java @@ -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 typeSpecs = new HashMap<>(); + + /** Contains a mapping of a type index to all of the {@link TypeChunk} with that index. */ + private final Multimap types = ArrayListMultimap.create(); + + /** May contain a library chunk for mapping dynamic references to resolved references. */ + private Optional 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 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 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 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 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); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageUtils.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageUtils.java new file mode 100644 index 000000000..5077ca6f0 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/PackageUtils.java @@ -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]); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceEntryStatsCollector.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceEntryStatsCollector.java new file mode 100644 index 000000000..5891f0567 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceEntryStatsCollector.java @@ -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 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 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[]> entry + : blamer.getTypeToBlamedResources().entrySet()) { + computePoolSizes(entry.getKey().getTypeStringPool(), entry.getValue()); + } + } + + private void computeKeyPoolSizes() throws IOException { + for (Entry[]> entry + : blamer.getKeyToBlamedResources().entrySet()) { + computePoolSizes(entry.getKey().getKeyStringPool(), entry.getValue()); + } + } + + private void computeTypeSpecSizes() { + for (Entry[]> entry + : blamer.getTypeToBlamedResources().entrySet()) { + computeTypeSpecSizes(entry.getKey(), entry.getValue()); + } + } + + private void computeTypeChunkSizes() { + for (Entry> 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> 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[] 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[] 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 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; + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceTableChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceTableChunk.java new file mode 100644 index 000000000..8752b9054 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/ResourceTableChunk.java @@ -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: + * + *

    + *
  • A {@link StringPoolChunk} containing all string values in the entire resource table. It does + * not, however, contain the names of entries or type identifiers. + *
  • One or more {@link PackageChunk}. + *
+ */ +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 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 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()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/SerializableResource.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/SerializableResource.java new file mode 100644 index 000000000..b9c62a1d5 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/SerializableResource.java @@ -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; +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/StringPoolChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/StringPoolChunk.java new file mode 100644 index 000000000..c0c73efaa --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/StringPoolChunk.java @@ -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 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 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 readStrings(ByteBuffer buffer, int offset, int count) { + List 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 readStyles(ByteBuffer buffer, int offset, int count) { + List 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 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 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 spans; + + static StringPoolStyle create(ByteBuffer buffer, int offset, StringPoolChunk parent) { + Builder 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 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: + * + *
"StringPoolStyle{spans=[StringPoolSpan{foo, start=0, stop=5}, ...]}"
+ */ + @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: + * + *
"StringPoolSpan{foo, start=0, stop=5}"
+ */ + @Override + public String toString() { + return String.format("StringPoolSpan{%s, start=%d, stop=%d}", + parent.getString(nameIndex), start, stop); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeChunk.java new file mode 100644 index 000000000..9a98ed54c --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeChunk.java @@ -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}. + * + *

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 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 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 entries) { + for (Map.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 values; + private final int parentEntry; + private final TypeChunk parent; + + private Entry(int headerSize, + int flags, + int keyIndex, + BinaryResourceValue value, + Map 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 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. + * + *

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 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 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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeSpecChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeSpecChunk.java new file mode 100644 index 000000000..691d9a38a --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/TypeSpecChunk.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/UnknownChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/UnknownChunk.java new file mode 100644 index 000000000..7117e63f7 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/UnknownChunk.java @@ -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; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlAttribute.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlAttribute.java new file mode 100644 index 000000000..1cb2977b3 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlAttribute.java @@ -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: + * + *

"XmlAttribute{namespace=foo, name=bar, value=1234}"
+ */ + @Override + public String toString() { + return String.format("XmlAttribute{namespace=%s, name=%s, value=%s}", + namespace(), name(), rawValue()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlCdataChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlCdataChunk.java new file mode 100644 index 000000000..44bc66bef --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlCdataChunk.java @@ -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: + * + *
"XmlCdataChunk{line=1234, comment=My awesome comment., value=1234}"
+ */ + @Override + public String toString() { + return String.format("XmlCdataChunk{line=%d, comment=%s, value=%s}", + getLineNumber(), getComment(), getRawValue()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlChunk.java new file mode 100644 index 000000000..08aba6d3a --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlChunk.java @@ -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. + * + *

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."); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlEndElementChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlEndElementChunk.java new file mode 100644 index 000000000..c336c68b9 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlEndElementChunk.java @@ -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: + * + *

+   * "XmlEndElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar}"
+   * 
+ */ + @Override + public String toString() { + return String.format("XmlEndElementChunk{line=%d, comment=%s, namespace=%s, name=%s}", + getLineNumber(), getComment(), getNamespace(), getName()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceChunk.java new file mode 100644 index 000000000..dce571456 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceChunk.java @@ -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: + * + *
+   * "XmlNamespaceChunk{line=1234, comment=My awesome comment., prefix=foo, uri=com.google.foo}"
+   * 
+ */ + @Override + public String toString() { + return String.format("XmlNamespaceChunk{line=%d, comment=%s, prefix=%s, uri=%s}", + getLineNumber(), getComment(), getPrefix(), getUri()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceEndChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceEndChunk.java new file mode 100644 index 000000000..6cce6e9fd --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceEndChunk.java @@ -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; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceStartChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceStartChunk.java new file mode 100644 index 000000000..3cb3daff9 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNamespaceStartChunk.java @@ -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; + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNodeChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNodeChunk.java new file mode 100644 index 000000000..3f293c0a9 --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlNodeChunk.java @@ -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: + * + *
"XmlNodeChunk{line=1234, comment=My awesome comment.}"
+ */ + @Override + public String toString() { + return String.format("XmlNodeChunk{line=%d, comment=%s}", getLineNumber(), getComment()); + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlResourceMapChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlResourceMapChunk.java new file mode 100644 index 000000000..85d389f0e --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlResourceMapChunk.java @@ -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. + * + *

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 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 enumerateResources(ByteBuffer buffer) { + int resourceCount = (getOriginalChunkSize() - getHeaderSize()) / RESOURCE_SIZE; + List 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); + } + } +} diff --git a/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlStartElementChunk.java b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlStartElementChunk.java new file mode 100644 index 000000000..523dcaf1a --- /dev/null +++ b/app/src/main/java/com/google/devrel/gmscore/tools/apk/arsc/XmlStartElementChunk.java @@ -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 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 enumerateAttributes(ByteBuffer buffer) { + List 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 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: + * + *

+   * "XmlStartElementChunk{line=1234, comment=My awesome comment., namespace=foo, name=bar, ...}"
+   * 
+ */ + @Override + public String toString() { + return String.format( + "XmlStartElementChunk{line=%d, comment=%s, namespace=%s, name=%s, attributes=%s}", + getLineNumber(), getComment(), getNamespace(), getName(), attributes.toString()); + } +} diff --git a/app/src/main/java/com/vk/im/engine/commands/dialogs/DialogGetMentionSuggestionCmd.java b/app/src/main/java/com/vk/im/engine/commands/dialogs/DialogGetMentionSuggestionCmd.java new file mode 100644 index 000000000..251e4a757 --- /dev/null +++ b/app/src/main/java/com/vk/im/engine/commands/dialogs/DialogGetMentionSuggestionCmd.java @@ -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 { + + 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 implements Comparator { + 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 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 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 a(ImEnvironment imEnvironment, int i, String str) { + if (a(str).length() == 0) { + return l.a(); + } + List 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 a(List 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 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 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 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() { + + @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 + ")"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vk/im/engine/models/MemberType.java b/app/src/main/java/com/vk/im/engine/models/MemberType.java new file mode 100644 index 000000000..d816fe085 --- /dev/null +++ b/app/src/main/java/com/vk/im/engine/models/MemberType.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vk/im/ui/q/h/e/MentionsController.java b/app/src/main/java/com/vk/im/ui/q/h/e/MentionsController.java new file mode 100644 index 000000000..4ef6a7831 --- /dev/null +++ b/app/src/main/java/com/vk/im/ui/q/h/e/MentionsController.java @@ -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 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) ((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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/deviceinfo/OEMDetector.java b/app/src/main/java/ru/vtosters/lite/deviceinfo/OEMDetector.java index 42d71fb47..667474e4b 100644 --- a/app/src/main/java/ru/vtosters/lite/deviceinfo/OEMDetector.java +++ b/app/src/main/java/ru/vtosters/lite/deviceinfo/OEMDetector.java @@ -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(); } diff --git a/app/src/main/java/ru/vtosters/lite/hooks/DockBarInjector.java b/app/src/main/java/ru/vtosters/lite/hooks/DockBarInjector.java index 6bb75f308..ee4344c06 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/DockBarInjector.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/DockBarInjector.java @@ -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); } diff --git a/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java b/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java index d47bc289e..7e9831f8a 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java @@ -1,14 +1,7 @@ package ru.vtosters.lite.hooks; -import static ru.vtosters.lite.ui.dialogs.ServerDialog.sendRequest; -import static ru.vtosters.lite.utils.CacheUtils.getInstance; -import static ru.vtosters.lite.utils.NewsFeedFiltersUtils.setupFilters; -import static ru.vtosters.lite.utils.Preferences.checkupdates; -import static ru.vtosters.lite.utils.ThemesUtils.setNeededTheme; - import android.app.Activity; import android.os.Build; - import ru.vtosters.lite.concurrent.VTExecutors; import ru.vtosters.lite.downloaders.notifications.NotificationChannels; import ru.vtosters.lite.ssfs.UsersList; @@ -18,6 +11,12 @@ import ru.vtosters.lite.ui.dialogs.OTADialog; import ru.vtosters.lite.ui.dialogs.Start; import ru.vtosters.lite.utils.DeletedMessagesHandler; +import static ru.vtosters.lite.ui.dialogs.ServerDialog.sendRequest; +import static ru.vtosters.lite.utils.CacheUtils.getInstance; +import static ru.vtosters.lite.utils.NewsFeedFiltersUtils.setupFilters; +import static ru.vtosters.lite.utils.Preferences.checkupdates; +import static ru.vtosters.lite.utils.ThemesUtils.setNeededTheme; + public class MainActivityInjector { public static void inject(Activity activity) { setNeededTheme(activity); diff --git a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java index a6d1c1655..150422ea3 100644 --- a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java @@ -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); diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java index 4dc0e74d0..06d415c06 100644 --- a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java @@ -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; diff --git a/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java index 6ece5e7cd..7f8719cd4 100644 --- a/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/downloader/ThumbnailDownloader.java @@ -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); } } diff --git a/app/src/main/java/ru/vtosters/lite/themes/ThemesCore.java b/app/src/main/java/ru/vtosters/lite/themes/ThemesCore.java index 5b625aa35..2252dff61 100644 --- a/app/src/main/java/ru/vtosters/lite/themes/ThemesCore.java +++ b/app/src/main/java/ru/vtosters/lite/themes/ThemesCore.java @@ -152,39 +152,6 @@ public class ThemesCore { themedColors.put(R.attr.im_bubble_outgoing_highlighted, accentColor); } - @SuppressLint("NonConstantResourceId") - public static int getThemedAttr(Context context, @IdRes int attr) { - return switch (attr) { - case R.attr.im_bubble_wallpaper_outgoing, R.attr.im_bubble_outgoing -> outgoinging_msg; - case R.attr.im_bubble_outgoing_highlighted -> outgoinging_msg_highlight; - case R.attr.newsfeed_post_title_color -> darken_color; - default -> themedColors.get(attr); - }; - } - - public static boolean hasThemedAttr(Context context, int attrID) { -// try { -// Log.d(TAG, "Requesting color by attr " + context.getResources().getResourceName(attrID)); -// } 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))); - } 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; - } - } - public static void clear() { cachedAccents = false; themedColors.clear(); diff --git a/app/src/main/java/ru/vtosters/lite/themes/ThemesHacks.java b/app/src/main/java/ru/vtosters/lite/themes/ThemesHacks.java deleted file mode 100644 index ed60ec21a..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/ThemesHacks.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.vtosters.lite.themes; - -import android.content.Context; -import android.graphics.drawable.*; -import android.os.Build; -import android.view.View; -import android.widget.TextView; -import androidx.annotation.ColorRes; -import androidx.annotation.NonNull; -import com.vtosters.lite.R; -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; - ((DrawableContainer.DrawableContainerState) stateListDrawable.getConstantState()).getChildren()[0].setTint(ThemesUtils.getAccentColor()); - ((DrawableContainer.DrawableContainerState) stateListDrawable.getConstantState()).getChildren()[1].setTint(ThemesUtils.halfAlpha(ThemesUtils.getAccentColor())); - } - } - - 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(); - } - - return Build.VERSION.SDK_INT > 23 ? context.getColor(color) : context.getResources().getColor(color); - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/themes/ThemesManager.java b/app/src/main/java/ru/vtosters/lite/themes/ThemesManager.java new file mode 100644 index 000000000..fffbd7985 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/themes/ThemesManager.java @@ -0,0 +1,241 @@ +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 java.io.*; +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.black_blue45_alpha10, + R.color.music_check_button_bg_checked, + R.color.vk_black_blue45_alpha10, + R.color.black_blue30_alpha66, + R.color.vk_black_blue30_alpha66, + R.color.attachpicker_item_background, + R.color.black_blue24_alpha8, + R.color.music_placeholder_artist_bg, + R.color.black_blue24_alpha24, + R.color.vk_black_blue24_alpha24, + 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()) { + ResourcesLoader.init(app); + ResourcesLoader.load(app, resTmpZipFile.getAbsolutePath(), assetsZipFile.getAbsolutePath(), false); + } + } catch(Exception e) { + Log.e("ThemesManager", 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(); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/themes/VTLResources.java b/app/src/main/java/ru/vtosters/lite/themes/VTLResources.java deleted file mode 100644 index 32ddc7b48..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/VTLResources.java +++ /dev/null @@ -1,92 +0,0 @@ -package ru.vtosters.lite.themes; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; - -import androidx.annotation.Nullable; - -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 attributesToTheme = new ArrayList<>(); - - static { - try { - typedArrayField = TypedArray.class.getDeclaredField("mData"); - typedArrayField.setAccessible(true); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } - - attributesToTheme.add(16843041); - attributesToTheme.add(android.R.attr.backgroundTint); - attributesToTheme.add(16843173); - } - - public VTLResources(Context context, Resources parent) { - super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration()); - this.context = context; -// Log.d(TAG, "VTLResources: init"); - } - - @Override - public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { - var typedArray = super.obtainAttributes(set, attrs); - var data = getArrayData(typedArray); - for (int i = 0; i < attrs.length; i++) { - try { - int attrID = attrs[i]; - if (attributesToTheme.contains(attrID)) { - 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 - } - } - } catch (Exception e) { - Log.e(TAG, "TAVzlom failed! (obtainAttributes)"); - e.printStackTrace(); - } - } - return typedArray; - } - - @Override - public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException { - var drawable = super.getDrawable(id, theme); - if (id == com.vtosters.lite.R.drawable.newsfeed_tab_dropdown_16) { - ThemesHacks.fixDropdown(drawable); - } - 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]; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/themes/ViewInjector.java b/app/src/main/java/ru/vtosters/lite/themes/ViewInjector.java deleted file mode 100644 index f3c2ae118..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/ViewInjector.java +++ /dev/null @@ -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 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; - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/BaseHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/BaseHook.java deleted file mode 100644 index d56a91414..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/BaseHook.java +++ /dev/null @@ -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); -} diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/ButtonHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/ButtonHook.java deleted file mode 100644 index 1dec07232..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/ButtonHook.java +++ /dev/null @@ -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())); - } - } - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/ImageViewHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/ImageViewHook.java deleted file mode 100644 index b9b88ef8a..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/ImageViewHook.java +++ /dev/null @@ -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())); - } - } - } - } -} diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/ProgressWheelHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/ProgressWheelHook.java deleted file mode 100644 index 01da24ac2..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/ProgressWheelHook.java +++ /dev/null @@ -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()); - } - } - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/TabLayoutHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/TabLayoutHook.java deleted file mode 100644 index c85402c26..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/TabLayoutHook.java +++ /dev/null @@ -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())); - } - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/TextViewHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/TextViewHook.java deleted file mode 100644 index da8e65948..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/TextViewHook.java +++ /dev/null @@ -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); - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/ToolbarHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/ToolbarHook.java deleted file mode 100644 index 38c5097da..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/ToolbarHook.java +++ /dev/null @@ -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); - } - } - - } - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/hooks/VKImageViewHook.java b/app/src/main/java/ru/vtosters/lite/themes/hooks/VKImageViewHook.java deleted file mode 100644 index eb3133a7e..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/hooks/VKImageViewHook.java +++ /dev/null @@ -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); - } - } - } -} - diff --git a/app/src/main/java/ru/vtosters/lite/themes/loaders/AssetManagerHelper.java b/app/src/main/java/ru/vtosters/lite/themes/loaders/AssetManagerHelper.java new file mode 100644 index 000000000..7ff60dc71 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/themes/loaders/AssetManagerHelper.java @@ -0,0 +1,58 @@ +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 +} diff --git a/app/src/main/java/ru/vtosters/lite/themes/loaders/ResourcesLoader.java b/app/src/main/java/ru/vtosters/lite/themes/loaders/ResourcesLoader.java new file mode 100644 index 000000000..6a9f9934e --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/themes/loaders/ResourcesLoader.java @@ -0,0 +1,380 @@ +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.Constructor; +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> 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", 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> activeResources19 = + (ArrayMap>) fMActiveResources.get(resourcesManager); + sResourceReferences = activeResources19.values(); + } catch (NoSuchFieldException ignore) { + // N moved the resources to mResourceReferences + final Field mResourceReferences = ReflectionUtils.findField(resourcesManagerClass, "mResourceReferences"); + sResourceReferences = (Collection>) 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>) 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 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 req = (List) 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; + } + } + + 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; + } + if (patchedResModifiedTime == storedPatchedResModifiedTime) { + return false; + } + return true; + } + + 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; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/themes/palettes/PalettesManager.java b/app/src/main/java/ru/vtosters/lite/themes/palettes/PalettesManager.java index f8971e69f..fc8666b0e 100644 --- a/app/src/main/java/ru/vtosters/lite/themes/palettes/PalettesManager.java +++ b/app/src/main/java/ru/vtosters/lite/themes/palettes/PalettesManager.java @@ -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(); } diff --git a/app/src/main/java/ru/vtosters/lite/themes/utils/ArscEditor.java b/app/src/main/java/ru/vtosters/lite/themes/utils/ArscEditor.java new file mode 100644 index 000000000..26528dc39 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/themes/utils/ArscEditor.java @@ -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; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/themes/utils/RecolorUtils.java b/app/src/main/java/ru/vtosters/lite/themes/utils/RecolorUtils.java index f0236b9d4..c5c2cbc93 100644 --- a/app/src/main/java/ru/vtosters/lite/themes/utils/RecolorUtils.java +++ b/app/src/main/java/ru/vtosters/lite/themes/utils/RecolorUtils.java @@ -12,18 +12,6 @@ import ru.vtosters.lite.utils.AndroidUtils; import ru.vtosters.lite.utils.ThemesUtils; public class RecolorUtils { - public static Drawable recolorDrawableToolbar(Drawable drawable) { - if (drawable == null) return null; - - return new RecoloredDrawable(drawable, ThemesUtils.getHeaderText()); - } // Recolor toolbar drawable to accent color - - @SuppressLint("UseCompatLoadingForDrawables") - public static Drawable recolorDrawableInt(int drawable) { - - @SuppressLint("UseCompatLoadingForDrawables") Drawable res = AndroidUtils.getResources().getDrawable(drawable); - return new RecoloredDrawable(res, ThemesUtils.getAccentColor()); - } // Get res drawable via id and coloring to accent public static int recolorHexColor(int i){ var accented = ColorReferences.isAccentedColor(i); @@ -31,12 +19,6 @@ public class RecolorUtils { return (accented || mutedaccented) ? (accented ? ThemesUtils.getAccentColor() : ThemesUtils.getMutedAccentColor()) : i; } - public static ColorStateList recolorCSL(ColorStateList colorStateList) { - if (colorStateList == null) return null; - - return ColorStateList.valueOf(ThemesUtils.getAccentColor()); - } // Recolor ColorStateList to accent color - @SuppressLint("UseCompatLoadingForColorStateLists") public static ColorStateList themeCSL(Context context, int color) { if (ColorReferences.isColorRefAccented(color)) { diff --git a/app/src/main/java/ru/vtosters/lite/themes/utils/VkUiThemer.java b/app/src/main/java/ru/vtosters/lite/themes/utils/VkUiThemer.java deleted file mode 100644 index d471fe84a..000000000 --- a/app/src/main/java/ru/vtosters/lite/themes/utils/VkUiThemer.java +++ /dev/null @@ -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 | " + 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 | " + 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! - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/DockBarEditorFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/DockBarEditorFragment.java index d372d9f6b..eb6ed817c 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/DockBarEditorFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/DockBarEditorFragment.java @@ -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); diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/SuperAppEditorFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/SuperAppEditorFragment.java index d22bf8662..2ab68e1b3 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/SuperAppEditorFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/SuperAppEditorFragment.java @@ -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); diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/ThemesFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/ThemesFragment.java index eab1fafe5..8b9b79a89 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/ThemesFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/ThemesFragment.java @@ -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(); } @@ -47,6 +54,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 false; + }); + var navBarPreference = findPreference("navbar"); navBarPreference.setOnPreferenceClickListener(preference -> { restart(); @@ -147,7 +170,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 +216,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() { diff --git a/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java b/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java index 617c9bd4a..2bf8985d6 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/AndroidUtils.java @@ -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"); } diff --git a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java index 04fcaf8e6..afb0aad51 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/IOUtils.java @@ -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) { diff --git a/app/src/main/java/ru/vtosters/lite/utils/ReflectionUtils.java b/app/src/main/java/ru/vtosters/lite/utils/ReflectionUtils.java index 2971abfbe..dee5776b2 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/ReflectionUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/ReflectionUtils.java @@ -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 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 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 getObjectStaticField(Class clz, String fieldName) - throws NoSuchFieldException, IllegalAccessException { - return getObjectField(clz, fieldName, null); - } - - public static 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 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 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 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 invokeMethod(String className, String methodName, Object instance, Object... parameters) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException { - return invokeMethod(getClass(className), methodName, instance, parameters); - } - - public static T invokeStaticMethod(Class clz, String methodName, Class[] parameterTypes, Object... parameters) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - return invokeMethod(clz, methodName, null, parameterTypes, parameters); - } - - public static 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 invokeStaticMethod(Class clz, String methodName, Object... parameters) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - return invokeMethod(clz, methodName, null, parameters); - } - - public static T invokeStaticMethod(String className, String methodName, Object... parameters) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException { - return invokeMethod(getClass(className), methodName, null, parameters); } } \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java b/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java index d86e03c46..b6e84036f 100644 --- a/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java +++ b/app/src/main/java/ru/vtosters/lite/utils/ThemesUtils.java @@ -1,23 +1,17 @@ package ru.vtosters.lite.utils; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Build; -import android.util.Log; import android.view.View; import android.view.Window; import android.webkit.WebView; -import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import com.vk.articles.preload.WebCachePreloader; import com.vk.core.drawable.RecoloredDrawable; @@ -27,13 +21,9 @@ import com.vk.core.ui.themes.VKTheme; import com.vk.core.ui.themes.VKThemeHelper; import com.vtosters.lite.R; 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.ui.wallpapers.WallpapersHooks; -import java.lang.reflect.Field; - import static ru.vtosters.lite.utils.AndroidUtils.*; import static ru.vtosters.lite.utils.Preferences.*; @@ -50,14 +40,6 @@ public class ThemesUtils { return ColorStateList.valueOf(getAccentColor()); } - public static int getSystemModeTheme() { - return AppCompatDelegate.getDefaultNightMode(); - } - - public static void setSystemModeTheme(int theme) { - AppCompatDelegate.setDefaultNightMode(theme); - } - public static void setTheme(VKTheme theme, Activity activity) { if (activity == null) { activity = LifecycleUtils.getCurrentActivity(); @@ -69,17 +51,6 @@ public class ThemesUtils { WebCachePreloader.e(); } // apply changed theme - public static void setThemeFL(VKTheme theme, Activity activity, float[] fl) { - if (activity == null) { - activity = LifecycleUtils.getCurrentActivity(); - } - VKThemeHelper.theme(theme, activity, fl); - ThemeTracker.a(); - VKUIHook.isLoaded = false; - new WebView(activity).clearCache(true); - WebCachePreloader.e(); - } - public static boolean isDarkTheme() { return VKThemeHelper.r(); } @@ -93,111 +64,17 @@ 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() { return getMutedColor(getAccentColor()); } - @SuppressLint("DiscouragedPrivateApi") - public static void setCursorColor(EditText view) { - try { - view.setHighlightColor(ThemesUtils.getMutedAccentColor()); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - view.getTextCursorDrawable().setColorFilter(new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN)); - } else if (!OEMDetector.isMIUI()) { - Field field = TextView.class.getDeclaredField("mCursorDrawableRes"); - field.setAccessible(true); - int drawableResId = field.getInt(view); - - field = TextView.class.getDeclaredField("mEditor"); - field.setAccessible(true); - Object editor = field.get(view); - - if (editor != null) { - Drawable drawable = ContextCompat.getDrawable(view.getContext(), drawableResId); - drawable.setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN); - Drawable[] drawables = {drawable, drawable}; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - field = editor.getClass().getDeclaredField("mCursorDrawable"); - } else { - field = editor.getClass().getDeclaredField("mDrawableForCursor"); - } - field.setAccessible(true); - field.set(editor, drawables); - } - } - } catch (Exception e) { - Log.e("ThemesUtils", "setCursorColor: ", e); - } - } - @SuppressLint("DiscouragedPrivateApi") - public static void colorHandles(TextView view) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - PorterDuffColorFilter filter = new PorterDuffColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_IN); - view.getTextSelectHandle().setColorFilter(filter); - view.getTextSelectHandleRight().setColorFilter(filter); - view.getTextSelectHandleLeft().setColorFilter(filter); - } else if (!OEMDetector.isMIUI()) { - Field editorField = TextView.class.getDeclaredField("mEditor"); - - if (!editorField.isAccessible()) editorField.setAccessible(true); - - Object editor = editorField.get(view); - - if (editor != null) { - Class editorClass; - - editorClass = editor.getClass(); - - String[] handleNames = {"mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"}; - String[] resNames = {"mTextSelectHandleLeftRes", "mTextSelectHandleRightRes", "mTextSelectHandleRes"}; - - for (int i = 0; i < handleNames.length; i++) { - Field handleField = editorClass.getDeclaredField(handleNames[i]); - if (!handleField.isAccessible()) { - handleField.setAccessible(true); - } - - Drawable handleDrawable = (Drawable) handleField.get(editor); - - if (handleDrawable == null) { - Field resField = TextView.class.getDeclaredField(resNames[i]); - if (!resField.isAccessible()) { - resField.setAccessible(true); - } - int resId = resField.getInt(view); - handleDrawable = view.getResources().getDrawable(resId); - } - - if (handleDrawable != null) { - Drawable drawable = handleDrawable.mutate(); - drawable.setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN); - handleField.set(editor, drawable); - } - } - } - } - } catch (Exception e) { - Log.e("ThemesUtils", "colorHandles: ", e); - } - } - public static int getMutedColor(int color) { return ColorUtils.blendARGB(color, (isDarkTheme() ? Color.BLACK : Color.WHITE), (isMilkshake() ? 0.5f : isDarkTheme() ? 0.32f : 0.1f)); } - public static void setCustomAccentColor(int newColor, boolean async) { - var editor = AndroidUtils.edit().putInt("accent_color", newColor); - if (async) editor.apply(); - else editor.commit(); - } - public static int getTextAttr() { return getColorFromAttr(R.attr.text_primary); } // Text Primary color @@ -304,10 +181,6 @@ public class ThemesUtils { tw.getBackground().setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_OVER); } - public static void recolorBGView(View view) { - view.getBackground().setColorFilter(ThemesUtils.getAccentColor(), PorterDuff.Mode.SRC_OVER); - } - public static int darken(int color, float by) { float[] hsl = new float[3]; ColorUtils.colorToHSL(color, hsl); @@ -335,8 +208,8 @@ public class ThemesUtils { return ColorUtils.setAlphaComponent(src, 169); } - public static int getColor(int i) { - return ThemesHacks.getHackedColor(getGlobalContext(), i); + public static int getColor(int color) { + return Build.VERSION.SDK_INT > 23 ? getGlobalContext().getColor(color) : getGlobalContext().getResources().getColor(color); } // Android Support color injector + accent color checker public static int getAlertStyle() { diff --git a/smali/res/xml/preferences_themes.xml b/smali/res/xml/preferences_themes.xml index 1cc7a9286..e84bd2ebd 100644 --- a/smali/res/xml/preferences_themes.xml +++ b/smali/res/xml/preferences_themes.xml @@ -7,6 +7,10 @@ android:summary="@string/accent_color_summ" android:icon="@drawable/bg_accent_circle" android:title="@string/accent_color_title"/> + + abc_input_method_navigation_guard:I .line 19 - invoke-static {v5}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {v4, v5}, Landroid/content/res/Resources;->getColor(I)I move-result v4 diff --git a/smali/smali/androidx/appcompat/widget/ResourcesWrapper.smali b/smali/smali/androidx/appcompat/widget/ResourcesWrapper.smali index 5e342888d..99d4a4c39 100644 --- a/smali/smali/androidx/appcompat/widget/ResourcesWrapper.smali +++ b/smali/smali/androidx/appcompat/widget/ResourcesWrapper.smali @@ -79,7 +79,9 @@ .end annotation .line 1 - invoke-static {p1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + iget-object v0, p0, Landroidx/appcompat/widget/ResourcesWrapper;->mResources:Landroid/content/res/Resources; + + invoke-virtual {v0, p1}, Landroid/content/res/Resources;->getColor(I)I move-result p1 diff --git a/smali/smali/androidx/cardview/widget/CardView.smali b/smali/smali/androidx/cardview/widget/CardView.smali index 6b79426f9..5fefa8686 100644 --- a/smali/smali/androidx/cardview/widget/CardView.smali +++ b/smali/smali/androidx/cardview/widget/CardView.smali @@ -235,7 +235,7 @@ sget v1, Landroidx/cardview/R$color;->cardview_light_background:I - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {p3, v1}, Landroid/content/res/Resources;->getColor(I)I move-result p3 @@ -249,7 +249,7 @@ sget v1, Landroidx/cardview/R$color;->cardview_dark_background:I - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {p3, v1}, Landroid/content/res/Resources;->getColor(I)I move-result p3 diff --git a/smali/smali/androidx/cardview/widget/RoundRectDrawableWithShadow.smali b/smali/smali/androidx/cardview/widget/RoundRectDrawableWithShadow.smali index c811f8280..ff323b3aa 100644 --- a/smali/smali/androidx/cardview/widget/RoundRectDrawableWithShadow.smali +++ b/smali/smali/androidx/cardview/widget/RoundRectDrawableWithShadow.smali @@ -95,7 +95,7 @@ .line 5 sget v1, Landroidx/cardview/R$color;->cardview_shadow_start_color:I - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {p1, v1}, Landroid/content/res/Resources;->getColor(I)I move-result v1 @@ -104,7 +104,7 @@ .line 6 sget v1, Landroidx/cardview/R$color;->cardview_shadow_end_color:I - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {p1, v1}, Landroid/content/res/Resources;->getColor(I)I move-result v1 diff --git a/smali/smali/androidx/core/content/ContextCompat.smali b/smali/smali/androidx/core/content/ContextCompat.smali index 425d44f8a..d9fa6879e 100644 --- a/smali/smali/androidx/core/content/ContextCompat.smali +++ b/smali/smali/androidx/core/content/ContextCompat.smali @@ -245,13 +245,6 @@ .annotation build Landroidx/annotation/ColorInt; .end annotation - # ru.vtosters.lite.themes.ThemesHacks.getHackedColor(Landroid/content/Context;I)I - invoke-static {p0, p1}, Lru/vtosters/lite/themes/ThemesHacks;->getHackedColor(Landroid/content/Context;I)I - - move-result v0 - - return v0 - .line 1 sget v0, Landroid/os/Build$VERSION;->SDK_INT:I @@ -292,37 +285,31 @@ .annotation build Landroidx/annotation/Nullable; .end annotation - invoke-static {p0, p1}, Lru/vtosters/lite/themes/utils/RecolorUtils;->themeCSL(Landroid/content/Context;I)Landroid/content/res/ColorStateList; + .line 1 + sget v0, Landroid/os/Build$VERSION;->SDK_INT:I + + const/16 v1, 0x17 + + if-lt v0, v1, :cond_0 + + .line 2 + invoke-virtual {p0, p1}, Landroid/content/Context;->getColorStateList(I)Landroid/content/res/ColorStateList; move-result-object p0 return-object p0 -# .line 1 -# sget v0, Landroid/os/Build$VERSION;->SDK_INT:I -# -# const/16 v1, 0x17 -# -# if-lt v0, v1, :cond_0 -# -# .line 2 -# invoke-virtual {p0, p1}, Landroid/content/Context;->getColorStateList(I)Landroid/content/res/ColorStateList; -# -# move-result-object p0 -# -# return-object p0 -# -# .line 3 -# :cond_0 -# invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; -# -# move-result-object p0 -# -# invoke-virtual {p0, p1}, Landroid/content/res/Resources;->getColorStateList(I)Landroid/content/res/ColorStateList; -# -# move-result-object p0 -# -# return-object p0 + .line 3 + :cond_0 + invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; + + move-result-object p0 + + invoke-virtual {p0, p1}, Landroid/content/res/Resources;->getColorStateList(I)Landroid/content/res/ColorStateList; + + move-result-object p0 + + return-object p0 .end method .method public static getDataDir(Landroid/content/Context;)Ljava/io/File; diff --git a/smali/smali/androidx/core/content/res/ResourcesCompat.smali b/smali/smali/androidx/core/content/res/ResourcesCompat.smali index 19ce40c10..f8ec5912d 100644 --- a/smali/smali/androidx/core/content/res/ResourcesCompat.smali +++ b/smali/smali/androidx/core/content/res/ResourcesCompat.smali @@ -48,7 +48,23 @@ } .end annotation - invoke-static {p1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + .line 1 + sget v0, Landroid/os/Build$VERSION;->SDK_INT:I + + const/16 v1, 0x17 + + if-lt v0, v1, :cond_0 + + .line 2 + invoke-virtual {p0, p1, p2}, Landroid/content/res/Resources;->getColor(ILandroid/content/res/Resources$Theme;)I + + move-result p0 + + return p0 + + .line 3 + :cond_0 + invoke-virtual {p0, p1}, Landroid/content/res/Resources;->getColor(I)I move-result p0 diff --git a/smali/smali/androidx/core/graphics/drawable/DrawableCompat.smali b/smali/smali/androidx/core/graphics/drawable/DrawableCompat.smali index 8b710f823..69b709c1d 100644 --- a/smali/smali/androidx/core/graphics/drawable/DrawableCompat.smali +++ b/smali/smali/androidx/core/graphics/drawable/DrawableCompat.smali @@ -645,10 +645,6 @@ .end annotation .end param - invoke-static {p1}, Lru/vtosters/lite/themes/utils/RecolorUtils;->themeCSL(Landroid/content/res/ColorStateList;)Landroid/content/res/ColorStateList; - - move-result-object p1 - .line 1 sget v0, Landroid/os/Build$VERSION;->SDK_INT:I diff --git a/smali/smali_classes2/com/vk/articles/ArticleFragment.smali b/smali/smali_classes2/com/vk/articles/ArticleFragment.smali index c8ce08c34..3db2c4e99 100644 --- a/smali/smali_classes2/com/vk/articles/ArticleFragment.smali +++ b/smali/smali_classes2/com/vk/articles/ArticleFragment.smali @@ -3494,14 +3494,14 @@ const v1, 0x7f060018 - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-static {p1, v1}, Landroidx/core/content/ContextCompat;->getColor(Landroid/content/Context;I)I move-result v2 const v3, 0x7f060105 .line 20 - invoke-static {v3}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-static {p1, v3}, Landroidx/core/content/ContextCompat;->getColor(Landroid/content/Context;I)I move-result v4 diff --git a/smali/smali_classes2/com/vk/catalog2/core/holders/stickers/StickerCatalogVh.smali b/smali/smali_classes2/com/vk/catalog2/core/holders/stickers/StickerCatalogVh.smali index 3283aa89b..ae6d8f7dc 100644 --- a/smali/smali_classes2/com/vk/catalog2/core/holders/stickers/StickerCatalogVh.smali +++ b/smali/smali_classes2/com/vk/catalog2/core/holders/stickers/StickerCatalogVh.smali @@ -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 diff --git a/smali/smali_classes2/com/vk/core/ui/VKLayoutInflater.smali b/smali/smali_classes2/com/vk/core/ui/VKLayoutInflater.smali index 0caaa64cc..b0f962156 100644 --- a/smali/smali_classes2/com/vk/core/ui/VKLayoutInflater.smali +++ b/smali/smali_classes2/com/vk/core/ui/VKLayoutInflater.smali @@ -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 \ No newline at end of file diff --git a/smali/smali_classes2/com/vk/core/ui/themes/VKThemeHelper$b.smali b/smali/smali_classes2/com/vk/core/ui/themes/VKThemeHelper$b.smali index 1d4b829fe..7853c0137 100644 --- a/smali/smali_classes2/com/vk/core/ui/themes/VKThemeHelper$b.smali +++ b/smali/smali_classes2/com/vk/core/ui/themes/VKThemeHelper$b.smali @@ -27,8 +27,6 @@ .end annotation .end field -.field private resources:Landroid/content/res/Resources; - # direct methods .method public constructor (Landroid/content/Context;I)V @@ -152,30 +150,4 @@ move-result-object p1 return-object p1 -.end method - -.method public getResources()Landroid/content/res/Resources; - .registers 3 - - iget-object v0, p0, Lcom/vk/core/ui/themes/VKThemeHelper$b;->resources:Landroid/content/res/Resources; - - if-eqz v0, :cond_7 - - iget-object v0, p0, Lcom/vk/core/ui/themes/VKThemeHelper$b;->resources:Landroid/content/res/Resources; - - :goto_6 - return-object v0 - - :cond_7 - new-instance v0, Lru/vtosters/lite/themes/VTLResources; - - invoke-static {}, Lru/vtosters/lite/utils/AndroidUtils;->getResources()Landroid/content/res/Resources; - - move-result-object v1 - - invoke-direct {v0, p0, v1}, Lru/vtosters/lite/themes/VTLResources;->(Landroid/content/Context;Landroid/content/res/Resources;)V - - iput-object v0, p0, Lcom/vk/core/ui/themes/VKThemeHelper$b;->resources:Landroid/content/res/Resources; - - goto :goto_6 -.end method +.end method \ No newline at end of file diff --git a/smali/smali_classes2/com/vk/core/util/ContextExtKt.smali b/smali/smali_classes2/com/vk/core/util/ContextExtKt.smali index b86782ede..f536180e8 100644 --- a/smali/smali_classes2/com/vk/core/util/ContextExtKt.smali +++ b/smali/smali_classes2/com/vk/core/util/ContextExtKt.smali @@ -920,19 +920,6 @@ .end annotation .end param - invoke-static {p0, p1}, Lru/vtosters/lite/themes/ThemesCore;->hasThemedAttr(Landroid/content/Context;I)Z - - move-result v1 - - if-eqz v1, :cond_b - - invoke-static {p0, p1}, Lru/vtosters/lite/themes/ThemesCore;->getThemedAttr(Landroid/content/Context;I)I - - move-result v1 - - return v1 - :cond_b - .line 1 invoke-virtual {p0}, Landroid/content/Context;->getTheme()Landroid/content/res/Resources$Theme; diff --git a/smali/smali_classes2/com/vk/core/utils/VerifyInfoHelper$colorLightBlue$2.smali b/smali/smali_classes2/com/vk/core/utils/VerifyInfoHelper$colorLightBlue$2.smali index 190539f14..3bb0aca74 100644 --- a/smali/smali_classes2/com/vk/core/utils/VerifyInfoHelper$colorLightBlue$2.smali +++ b/smali/smali_classes2/com/vk/core/utils/VerifyInfoHelper$colorLightBlue$2.smali @@ -61,9 +61,13 @@ .line 2 sget-object v0, Lcom/vk/core/util/AppContextHolder;->a:Landroid/content/Context; + invoke-virtual {v0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; + + move-result-object v0 + sget v1, Lb/h/z/c;->light_blue:I - invoke-static {v1}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {v0, v1}, Landroid/content/res/Resources;->getColor(I)I move-result v0 diff --git a/smali/smali_classes2/com/vk/extensions/ViewExtKt.smali b/smali/smali_classes2/com/vk/extensions/ViewExtKt.smali index 25d433285..257c50f10 100644 --- a/smali/smali_classes2/com/vk/extensions/ViewExtKt.smali +++ b/smali/smali_classes2/com/vk/extensions/ViewExtKt.smali @@ -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 diff --git a/smali/smali_classes3/com/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent.smali b/smali/smali_classes3/com/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent.smali index cff5301b7..4f37ec72c 100644 --- a/smali/smali_classes3/com/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent.smali +++ b/smali/smali_classes3/com/vk/im/ui/components/dialogs_header/impl/vk/VkDialogsHeaderComponent.smali @@ -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; diff --git a/smali/smali_classes3/com/vk/mentions/w/DefaultMentionSpanProvider.smali b/smali/smali_classes3/com/vk/mentions/w/DefaultMentionSpanProvider.smali index 2b94edf24..c94fb1db7 100644 --- a/smali/smali_classes3/com/vk/mentions/w/DefaultMentionSpanProvider.smali +++ b/smali/smali_classes3/com/vk/mentions/w/DefaultMentionSpanProvider.smali @@ -27,7 +27,11 @@ .line 2 sget v0, Lcom/vk/mentions/p;->light_blue:I - invoke-static {v0}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual/range {p1 .. p1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; + + move-result-object p1 + + invoke-virtual {p1, v0}, Landroid/content/res/Resources;->getColor(I)I move-result p1 diff --git a/smali/smali_classes4/com/vk/superapp/SuperAppFragment.smali b/smali/smali_classes4/com/vk/superapp/SuperAppFragment.smali index 58cd8b164..457789709 100644 --- a/smali/smali_classes4/com/vk/superapp/SuperAppFragment.smali +++ b/smali/smali_classes4/com/vk/superapp/SuperAppFragment.smali @@ -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; diff --git a/smali/smali_classes4/com/vtosters/lite/VKApplication.smali b/smali/smali_classes4/com/vtosters/lite/VKApplication.smali index 2d313b5ac..59aa6da4e 100644 --- a/smali/smali_classes4/com/vtosters/lite/VKApplication.smali +++ b/smali/smali_classes4/com/vtosters/lite/VKApplication.smali @@ -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; diff --git a/smali/smali_classes4/com/vtosters/lite/ViewUtils.smali b/smali/smali_classes4/com/vtosters/lite/ViewUtils.smali index 203b7e27f..0a79a9fdf 100644 --- a/smali/smali_classes4/com/vtosters/lite/ViewUtils.smali +++ b/smali/smali_classes4/com/vtosters/lite/ViewUtils.smali @@ -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 diff --git a/smali/smali_classes5/com/vtosters/lite/fragments/photos/PhotoListFragment.smali b/smali/smali_classes5/com/vtosters/lite/fragments/photos/PhotoListFragment.smali index 57b853896..8e16258c0 100644 --- a/smali/smali_classes5/com/vtosters/lite/fragments/photos/PhotoListFragment.smali +++ b/smali/smali_classes5/com/vtosters/lite/fragments/photos/PhotoListFragment.smali @@ -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 diff --git a/smali/smali_classes5/com/vtosters/lite/ui/widget/WaveRecordCircleView.smali b/smali/smali_classes5/com/vtosters/lite/ui/widget/WaveRecordCircleView.smali index dcb1c5df1..cf1ec15e4 100644 --- a/smali/smali_classes5/com/vtosters/lite/ui/widget/WaveRecordCircleView.smali +++ b/smali/smali_classes5/com/vtosters/lite/ui/widget/WaveRecordCircleView.smali @@ -82,7 +82,7 @@ const v0, 0x7f0600f7 - invoke-static {v0}, Lru/vtosters/lite/utils/ThemesUtils;->getColor(I)I + invoke-virtual {p3, v0}, Landroid/content/res/Resources;->getColor(I)I move-result p3