diff --git a/docs/credits_acknowledgements.rst b/docs/credits_acknowledgements.rst
index f3d3d3776..9c5ba02c2 100644
--- a/docs/credits_acknowledgements.rst
+++ b/docs/credits_acknowledgements.rst
@@ -110,7 +110,7 @@ The following libraries and components are incorporated into RenderDoc, listed h
* `AOSP `_ - Copyright (c) 2006-2016, The Android Open Source Project, distributed under the Apache 2.0 License.
- Used to simplify Android workflows by distributing some tools from the android SDK.
+ Used to simplify Android workflows by distributing some tools from the android SDK, as well as patching android manifest files to enable debugging.
Thanks
------
diff --git a/renderdoc/3rdparty/android/android_manifest.h b/renderdoc/3rdparty/android/android_manifest.h
new file mode 100644
index 000000000..013ca68a6
--- /dev/null
+++ b/renderdoc/3rdparty/android/android_manifest.h
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2005 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include
+
+//////////////////////////////////////////////////////////////////////////////////
+//
+// These constants are taken from ResourceTypes.h:
+// https://android.googlesource.com/platform/frameworks/base/+/42ebcb80b50834a1ce4755cd4ca86918c96ca3c6/libs/androidfw/include/androidfw/ResourceTypes.h
+//
+// The ones we need are extracted here to not be dependent on the rest of the android framework.
+// There are some slight modifications for ease of integration, but this is still all under the
+// android license.
+
+enum class ResType : uint16_t
+{
+ Null = 0x0000,
+ StringPool = 0x0001,
+ XML = 0x0003,
+ NamespaceStart = 0x0100,
+ NamespaceEnd = 0x0101,
+ StartElement = 0x0102,
+ EndElement = 0x0103,
+ CDATA = 0x0104,
+ ResourceMap = 0x0180,
+};
+
+/**
+ * Header that appears at the front of every data chunk in a resource.
+ */
+struct ResChunk_header
+{
+ // Type identifier for this chunk. The meaning of this value depends
+ // on the containing chunk.
+ ResType type;
+
+ // Size of the chunk header (in bytes). Adding this value to
+ // the address of the chunk allows you to find its associated data
+ // (if any).
+ uint16_t headerSize;
+
+ // Total size of this chunk (in bytes). This is the chunkSize plus
+ // the size of any data associated with the chunk. Adding this value
+ // to the chunk allows you to completely skip its contents (including
+ // any child chunks). If this value is the same as chunkSize, there is
+ // no data associated with the chunk.
+ uint32_t size;
+};
+
+/**
+ * Reference to a string in a string pool.
+ */
+struct ResStringPool_ref
+{
+ // Index into the string pool table (uint32_t-offset from the indices
+ // immediately after ResStringPool_header) at which to find the location
+ // of the string data in the pool.
+ uint32_t index;
+};
+
+/**
+ * Representation of a value in a resource, supplying type
+ * information.
+ */
+struct Res_value
+{
+ // Number of bytes in this structure.
+ uint16_t size;
+
+ // Always set to 0.
+ uint8_t res0;
+
+ // Type of the data value.
+ enum class DataType : uint8_t
+ {
+ // The 'data' holds an index into the containing resource table's
+ // global value string pool.
+ String = 0x03,
+ // The 'data' is either 0 or 1, for input "false" or "true" respectively.
+ Boolean = 0x12,
+ } dataType;
+
+ // The data for this item, as interpreted according to dataType.
+ uint32_t data;
+};
+
+/**
+ * Definition for a pool of strings. The data of this chunk is an
+ * array of uint32_t providing indices into the pool, relative to
+ * stringsStart. At stringsStart are all of the UTF-16 strings
+ * concatenated together; each starts with a uint16_t of the string's
+ * length and each ends with a 0x0000 terminator. If a string is >
+ * 32767 characters, the high bit of the length is set meaning to take
+ * those 15 bits as a high word and it will be followed by another
+ * uint16_t containing the low word.
+ *
+ * If styleCount is not zero, then immediately following the array of
+ * uint32_t indices into the string table is another array of indices
+ * into a style table starting at stylesStart. Each entry in the
+ * style table is an array of ResStringPool_span structures.
+ */
+struct ResStringPool_header
+{
+ struct ResChunk_header header;
+
+ // Number of strings in this pool (number of uint32_t indices that follow
+ // in the data).
+ uint32_t stringCount;
+
+ // Number of style span arrays in the pool (number of uint32_t indices
+ // follow the string indices).
+ uint32_t styleCount;
+
+ // Flags.
+ enum StringFlags : uint32_t
+ {
+ // If set, the string index is sorted by the string values (based
+ // on strcmp16()).
+ SORTED_FLAG = 1 << 0,
+
+ // String pool is encoded in UTF-8
+ UTF8_FLAG = 1 << 8
+ };
+ StringFlags flags;
+
+ // Index from header of the string data.
+ uint32_t stringsStart;
+
+ // Index from header of the style data.
+ uint32_t stylesStart;
+};
+
+/**
+ * Basic XML tree node. A single item in the XML document. Extended info
+ * about the node can be found after header.headerSize.
+ */
+struct ResXMLTree_node
+{
+ struct ResChunk_header header;
+
+ // Line number in original source file at which this element appeared.
+ uint32_t lineNumber;
+
+ // Optional XML comment that was associated with this element; -1 if none.
+ ResStringPool_ref comment;
+};
+
+/**
+ * Extended XML tree node for namespace start/end nodes.
+ * Appears header.headerSize bytes after a ResXMLTree_node.
+ */
+struct ResXMLTree_namespaceExt
+{
+ // The prefix of the namespace.
+ struct ResStringPool_ref prefix;
+
+ // The URI of the namespace.
+ struct ResStringPool_ref uri;
+};
+
+/**
+ * Extended XML tree node for element start/end nodes.
+ * Appears header.headerSize bytes after a ResXMLTree_node.
+ */
+struct ResXMLTree_endElementExt
+{
+ // String of the full namespace of this element.
+ struct ResStringPool_ref ns;
+
+ // String name of this node if it is an ELEMENT; the raw
+ // character data if this is a CDATA node.
+ struct ResStringPool_ref name;
+};
+
+/**
+ * Extended XML tree node for start tags -- includes attribute
+ * information.
+ * Appears header.headerSize bytes after a ResXMLTree_node.
+ */
+struct ResXMLTree_attrExt
+{
+ // String of the full namespace of this element.
+ struct ResStringPool_ref ns;
+
+ // String name of this node if it is an ELEMENT; the raw
+ // character data if this is a CDATA node.
+ struct ResStringPool_ref name;
+
+ // Byte offset from the start of this structure where the attributes start.
+ uint16_t attributeStart;
+
+ // Size of the ResXMLTree_attribute structures that follow.
+ uint16_t attributeSize;
+
+ // Number of attributes associated with an ELEMENT. These are
+ // available as an array of ResXMLTree_attribute structures
+ // immediately following this node.
+ uint16_t attributeCount;
+
+ // Index (1-based) of the "id" attribute. 0 if none.
+ uint16_t idIndex;
+
+ // Index (1-based) of the "class" attribute. 0 if none.
+ uint16_t classIndex;
+
+ // Index (1-based) of the "style" attribute. 0 if none.
+ uint16_t styleIndex;
+};
+
+struct ResXMLTree_attribute
+{
+ // Namespace of this attribute.
+ struct ResStringPool_ref ns;
+
+ // Name of this attribute.
+ struct ResStringPool_ref name;
+
+ // The original raw string value of this attribute.
+ struct ResStringPool_ref rawValue;
+
+ // Processesd typed value of this attribute.
+ struct Res_value typedValue;
+};
+
+/**
+ * Extended XML tree node for CDATA tags -- includes the CDATA string.
+ * Appears header.headerSize bytes after a ResXMLTree_node.
+ */
+struct ResXMLTree_cdataExt
+{
+ // The raw CDATA character data.
+ struct ResStringPool_ref data;
+
+ // The typed value of the character data if this is a CDATA node.
+ struct Res_value typedData;
+};
\ No newline at end of file
diff --git a/renderdoc/CMakeLists.txt b/renderdoc/CMakeLists.txt
index 915d77289..f142bc355 100644
--- a/renderdoc/CMakeLists.txt
+++ b/renderdoc/CMakeLists.txt
@@ -93,6 +93,7 @@ set(sources
android/android_patch.cpp
android/android_tools.cpp
android/android_utils.cpp
+ android/android_manifest.cpp
android/android.h
android/android_utils.h
android/jdwp.h
@@ -156,6 +157,7 @@ set(sources
3rdparty/jpeg-compressor/jpgd.h
3rdparty/jpeg-compressor/jpge.cpp
3rdparty/jpeg-compressor/jpge.h
+ 3rdparty/android/android_manifest.h
3rdparty/catch/catch.cpp
3rdparty/catch/catch.hpp
3rdparty/pugixml/pugixml.cpp
diff --git a/renderdoc/android/android_manifest.cpp b/renderdoc/android/android_manifest.cpp
new file mode 100644
index 000000000..4a5b5b368
--- /dev/null
+++ b/renderdoc/android/android_manifest.cpp
@@ -0,0 +1,639 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2018 Baldur Karlsson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ ******************************************************************************/
+
+#include "3rdparty/android/android_manifest.h"
+#include "core/core.h"
+#include "strings/string_utils.h"
+#include "android_utils.h"
+
+const uint32_t debuggableResourceId = 0x0101000f;
+const uint32_t addingStringIndex = 0x8b8b8b8b;
+
+namespace Android
+{
+std::string GetStringPoolValue(ResStringPool_header *stringpool, ResStringPool_ref ref)
+{
+ byte *base = (byte *)stringpool;
+
+ uint32_t stringCount = stringpool->stringCount;
+ uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize);
+ byte *stringData = base + stringpool->stringsStart;
+
+ if(ref.index == ~0U)
+ return "";
+
+ if(ref.index >= stringCount)
+ return "__invalid_string__";
+
+ byte *strdata = stringData + stringOffsets[ref.index];
+
+ // strdata now points at len characters of string. Check if it's UTF-8 or UTF-16
+ if((stringpool->flags & ResStringPool_header::UTF8_FLAG) == 0)
+ {
+ uint16_t *str = (uint16_t *)strdata;
+
+ uint32_t len = *(str++);
+
+ // see comment above ResStringPool_header - if high bit is set, then this string is >32767
+ // characters, so it's followed by another uint16_t with the low word
+ if(len & 0x8000)
+ {
+ len &= 0x7fff;
+ len <<= 16;
+ len |= *(str++);
+ }
+
+ std::wstring wstr;
+
+ // wchar_t isn't always 2 bytes, so we iterate over the uint16_t and cast.
+ for(uint32_t i = 0; i < len; i++)
+ wstr.push_back(wchar_t(str[i]));
+
+ return StringFormat::Wide2UTF8(wstr);
+ }
+ else
+ {
+ byte *str = (byte *)strdata;
+
+ uint32_t len = *(str++);
+
+ // the length works similarly for UTF-8 data but with single bytes instead of uint16s.
+ if(len & 0x80)
+ {
+ len &= 0x7f;
+ len <<= 8;
+ len |= *(str++);
+ }
+
+ // the length is encoded twice. I can only assume to preserve uint16 size although I don't see
+ // why that would be necessary - it can't be fully backwards compatible even with the alignment
+ // except with readers that ignore the length entirely and look for trailing NULLs.
+ if(len < 0x80)
+ str++;
+ else
+ str += 2;
+
+ return std::string((char *)str, (char *)(str + len));
+ }
+}
+
+void ShiftStringPoolValue(ResStringPool_ref &ref, uint32_t insertedLocation)
+{
+ // if we found our added attribute, then set the index here (otherwise we'd remap it with the
+ // others!)
+ if(ref.index == addingStringIndex)
+ ref.index = insertedLocation;
+ else if(ref.index != ~0U && ref.index >= insertedLocation)
+ ref.index++;
+}
+
+void ShiftStringPoolValue(Res_value &val, uint32_t insertedLocation)
+{
+ if(val.dataType == Res_value::DataType::String && val.data >= insertedLocation)
+ val.data++;
+}
+
+template
+void InsertBytes(std::vector &bytes, byte *pos, const T &data)
+{
+ byte *start = &bytes[0];
+ byte *byteData = (byte *)&data;
+
+ size_t offs = pos - start;
+
+ bytes.insert(bytes.begin() + offs, byteData, byteData + sizeof(T));
+}
+
+template <>
+void InsertBytes(std::vector &bytes, byte *pos, const std::vector &data)
+{
+ byte *start = &bytes[0];
+
+ size_t offs = pos - start;
+
+ bytes.insert(bytes.begin() + offs, data.begin(), data.end());
+}
+
+bool PatchManifest(std::vector &manifestBytes)
+{
+ // Whether to insert a new string & resource ID at the start or end of the resource map table. I
+ // can't find anything that indicates there is any required ordering to these, so either should be
+ // valid.
+ const bool insertStringAtStart = false;
+
+ // reserve room for our modifications up front, to be sure that if we do make them we'll never
+ // invalidate any pointers. We could add:
+ manifestBytes.reserve(
+ manifestBytes.size() +
+ // - a string (uint32 offset, uint16 length and string characters (possibly
+ // in UTF-16) including NULL)
+ sizeof(uint32_t) + sizeof(uint16_t) + sizeof("debuggable") * 2 +
+ // - a resource ID mapping (one uint32)
+ sizeof(uint32_t) +
+ // - an attribute (ResXMLTree_attribute)
+ sizeof(ResXMLTree_attribute) +
+ // and we add 16 bytes more just for a safety margin with any necessary padding
+ 16);
+
+ // save the capacity so we can check we never resize
+ size_t capacity = manifestBytes.capacity();
+
+ byte *start = &manifestBytes[0];
+ byte *end = start + manifestBytes.size();
+
+ byte *cur = start;
+
+ ResChunk_header *xmlroot = (ResChunk_header *)cur;
+
+ if((byte *)(xmlroot + 1) > end)
+ {
+ RDCERR("Manifest is truncated, %zu bytes doesn't contain full XML header", manifestBytes.size());
+ return false;
+ }
+
+ if(xmlroot->type != ResType::XML)
+ {
+ RDCERR("XML Header is malformed, type is %u expected %u", xmlroot->type, ResType::XML);
+ return false;
+ }
+
+ if(xmlroot->headerSize != sizeof(*xmlroot))
+ {
+ RDCERR("XML Header is malformed, header size is reported as %u but expected %u",
+ xmlroot->headerSize, sizeof(*xmlroot));
+ return false;
+ }
+
+ // this isn't necessarily fatal, but it is unexpected.
+ if(xmlroot->size != manifestBytes.size())
+ RDCWARN("XML header is malformed, size is reported as %u but %zu bytes found", xmlroot->size,
+ manifestBytes.size());
+
+ cur += xmlroot->headerSize;
+
+ ResStringPool_header *stringpool = (ResStringPool_header *)cur;
+
+ if(stringpool->header.type != ResType::StringPool)
+ {
+ RDCERR("Manifest format is unsupported, expected string pool but got %u",
+ stringpool->header.type);
+ return false;
+ }
+
+ if(stringpool->header.headerSize != sizeof(*stringpool))
+ {
+ RDCERR("String pool is malformed, header size is reported as %u but expected %u",
+ stringpool->header.headerSize, sizeof(*stringpool));
+ return false;
+ }
+
+ if(cur + stringpool->header.size > end)
+ {
+ RDCERR("String pool is truncated, expected %u more bytes but only have %u",
+ stringpool->header.size, uint32_t(end - cur));
+ return false;
+ }
+
+ cur += stringpool->header.size;
+
+ ResChunk_header *resMap = (ResChunk_header *)cur;
+
+ if(resMap->type != ResType::ResourceMap)
+ {
+ RDCERR("Manifest format is unsupported, expected resource table but got %u", resMap->type);
+ return false;
+ }
+
+ if(resMap->headerSize != sizeof(*resMap))
+ {
+ RDCERR("Resource map is malformed, header size is reported as %u but expected %u",
+ resMap->headerSize, sizeof(*resMap));
+ return false;
+ }
+
+ if(cur + resMap->size > end)
+ {
+ RDCERR("Resource map is truncated, expected %u more bytes but only have %u", resMap->size,
+ uint32_t(end - cur));
+ return false;
+ }
+
+ uint32_t *resourceMapping = (uint32_t *)(cur + resMap->headerSize);
+ uint32_t resourceMappingCount = (resMap->size - resMap->headerSize) / sizeof(uint32_t);
+
+ cur += resMap->size;
+
+ bool stringAdded = false;
+
+ // now chunks will come along. There will likely first be a namespace begin, then XML tag open and
+ // close. Since the tag is only valid in one place in the XML we can just continue
+ // iterating until we find it - we don't actually need to care about the structure of the XML
+ // since we are identifying a unique tag and adding one attribute.
+ while(cur < end)
+ {
+ ResChunk_header *node = (ResChunk_header *)cur;
+
+ if(node->type != ResType::StartElement)
+ {
+ cur += node->size;
+ continue;
+ }
+
+ ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->headerSize);
+
+ std::string name = GetStringPoolValue(stringpool, startElement->name);
+
+ if(name != "application")
+ {
+ cur += node->size;
+ continue;
+ }
+
+ // found the application tag! Now search its attribtues to see if it already has a debuggable
+ // attribute (that might be set explicitly to false instead of defaulting)
+ if(startElement->attributeSize != sizeof(ResXMLTree_attribute))
+ {
+ RDCWARN("Declared attribute size %u doesn't match what we expect %zu",
+ startElement->attributeSize, sizeof(ResXMLTree_attribute));
+ }
+
+ if(startElement->attributeStart != sizeof(*startElement))
+ {
+ RDCWARN("Declared attribute start offset %u doesn't match what we expect %zu",
+ startElement->attributeStart, sizeof(*startElement));
+ }
+
+ byte *attributesStart = cur + node->headerSize + startElement->attributeStart;
+
+ bool found = false;
+
+ for(uint32_t i = 0; i < startElement->attributeCount; i++)
+ {
+ ResXMLTree_attribute *attribute =
+ (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i);
+
+ std::string attr = GetStringPoolValue(stringpool, attribute->name);
+
+ if(attr != "debuggable")
+ continue;
+
+ uint32_t resourceId = 0;
+
+ if(attribute->name.index < resourceMappingCount)
+ {
+ resourceId = resourceMapping[attribute->name.index];
+ }
+ else
+ {
+ RDCWARN("Found debuggable attribute, but it's not linked to any resource ID");
+
+ if(attribute->typedValue.dataType != Res_value::DataType::Boolean)
+ {
+ RDCERR("Found debuggable attribute that isn't boolean typed! Not modifying");
+ return false;
+ }
+ else
+ {
+ RDCDEBUG("Setting non-resource ID debuggable attribute to true");
+ attribute->typedValue.data = ~0U;
+
+ if(attribute->rawValue.index != ~0U)
+ {
+ RDCWARN("attribute has raw value '%s' which we aren't patching",
+ GetStringPoolValue(stringpool, attribute->rawValue).c_str());
+ }
+
+ // we'll still add a debuggable attribute that is resource ID linked, so we don't mark the
+ // attribute as found and break out of the loop yet
+ continue;
+ }
+ }
+
+ if(resourceId != debuggableResourceId)
+ {
+ RDCERR(
+ "Found debuggable attribute mapped to resource %x, not %x as we expect! Not modifying",
+ resourceId, debuggableResourceId);
+ return false;
+ }
+
+ RDCDEBUG("Found debuggable attribute.");
+
+ if(attribute->typedValue.dataType != Res_value::DataType::Boolean)
+ {
+ RDCERR("Found debuggable attribute that isn't boolean typed! Not modifying");
+ return false;
+ }
+ else
+ {
+ RDCDEBUG("Setting resource ID debuggable attribute to true");
+ attribute->typedValue.data = ~0U;
+
+ if(attribute->rawValue.index != ~0U)
+ {
+ RDCWARN("attribute has raw value '%s' which we aren't patching",
+ GetStringPoolValue(stringpool, attribute->rawValue).c_str());
+ }
+ }
+
+ found = true;
+ break;
+ }
+
+ if(found)
+ break;
+
+ if(startElement->attributeSize != sizeof(ResXMLTree_attribute))
+ {
+ RDCERR("Unexpected attribute size %u, can't add missing attribute",
+ startElement->attributeSize);
+ return false;
+ }
+
+ // default to an invalid value (the manifest would have to be GBs to have this as a valid string
+ // index.
+ // If we don't find the existing string to use, then this will be remapped below when we're
+ // remapping all the other indices.
+ ResStringPool_ref stringIndex = {addingStringIndex};
+
+ // we didn't find the attribute, so we need to search for the appropriate string, add it if not
+ // there, and add the attribute.
+ for(uint32_t i = 0; i < resourceMappingCount; i++)
+ {
+ if(resourceMapping[i] == debuggableResourceId)
+ {
+ std::string str = GetStringPoolValue(stringpool, {i});
+
+ if(str != "debuggable")
+ {
+ RDCWARN("Found debuggable resource ID, but it was linked to string '%s' not 'debuggable'",
+ str.c_str());
+ continue;
+ }
+
+ stringIndex = {i};
+ }
+ }
+
+ // declare the debuggable attribute
+ ResXMLTree_attribute debuggable;
+ debuggable.ns.index = ~0U;
+ debuggable.name = stringIndex;
+ debuggable.rawValue.index = ~0U;
+ debuggable.typedValue.size = sizeof(Res_value);
+ debuggable.typedValue.res0 = 0;
+ debuggable.typedValue.dataType = Res_value::DataType::Boolean;
+ debuggable.typedValue.data = ~0U;
+
+ // search the stringpool for the schema, it should be there already.
+ for(uint32_t i = 0; i < stringpool->stringCount; i++)
+ {
+ std::string val = GetStringPoolValue(stringpool, {i});
+ if(val == "http://schemas.android.com/apk/res/android")
+ {
+ debuggable.ns.index = i;
+ break;
+ }
+ }
+
+ if(debuggable.ns.index == ~0U)
+ RDCWARN("Couldn't find android schema, declaring attribute without schema");
+
+ // it seems the attribute must be added so that the attributes are sorted in resource ID order.
+ // We assume the attributes are already sorted according to this order, so we insert at the
+ // index of the first attribute we encounter with either no resource ID (i.e. if we only
+ // encountered lower resource IDs then we hit a non-resource ID attribute), or a higher resource
+ // ID than ours (in which case we're inserting it in the right place).
+ uint32_t attributeInsertIndex = 0;
+
+ for(uint32_t i = 0; i < startElement->attributeCount; i++)
+ {
+ ResXMLTree_attribute *attr =
+ (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i);
+
+ if(attr->name.index >= resourceMappingCount)
+ {
+ attributeInsertIndex = i;
+ RDCDEBUG("Inserting attribute before %s, with no resource ID",
+ GetStringPoolValue(stringpool, attr->name).c_str());
+ break;
+ }
+
+ uint32_t resourceId = resourceMapping[attr->name.index];
+
+ if(resourceId >= debuggableResourceId)
+ {
+ attributeInsertIndex = i;
+ RDCDEBUG("Inserting attribute before %s, with resource ID %x",
+ GetStringPoolValue(stringpool, attr->name).c_str(), resourceId);
+ break;
+ }
+
+ RDCDEBUG("Skipping past attribute %s, with resource ID %x",
+ GetStringPoolValue(stringpool, attr->name).c_str(), resourceId);
+ }
+
+ InsertBytes(manifestBytes, attributesStart + startElement->attributeSize * attributeInsertIndex,
+ debuggable);
+
+ // update header
+ startElement->attributeCount++;
+ node->size += sizeof(ResXMLTree_attribute);
+
+ stringAdded = (stringIndex.index == addingStringIndex);
+
+ break;
+ }
+
+ // if we added the string, we need to update the string pool and resource map, then finally update
+ // all stringrefs in the nodes. We do this in reverse order so that we don't invalidate pointers
+ // with insertions
+ if(stringAdded)
+ {
+ uint32_t insertIdx = insertStringAtStart ? 0 : resourceMappingCount;
+
+ // add to the resource map
+ {
+ if(insertIdx == 0)
+ InsertBytes(manifestBytes, (byte *)resMap + resMap->headerSize, debuggableResourceId);
+ else
+ InsertBytes(manifestBytes, (byte *)resMap + resMap->size, debuggableResourceId);
+ resMap->size += sizeof(uint32_t);
+ }
+
+ // add to the string pool
+ {
+ // add the offset
+ stringpool->header.size += sizeof(uint32_t);
+ stringpool->stringCount++;
+ stringpool->stringsStart += sizeof(uint32_t);
+ // if we're adding a string we don't bother to do it sorted, so remove the sorted flag
+ stringpool->flags =
+ ResStringPool_header::StringFlags(stringpool->flags & ~ResStringPool_header::SORTED_FLAG);
+
+ byte *base = (byte *)stringpool;
+
+ uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize);
+
+ // we duplicate the offset at the position we're inserting. Then when we fix up all the other
+ // offsets the duplicated one shifts by the right amount.
+ InsertBytes(manifestBytes,
+ (byte *)stringpool + stringpool->header.headerSize + sizeof(uint32_t) * insertIdx,
+ stringOffsets[insertIdx]);
+
+ uint32_t shift = 0;
+
+ byte *stringData = (byte *)stringpool + stringpool->stringsStart;
+
+ // insert the string, with length prefix and trailing NULL
+ if(stringpool->flags & ResStringPool_header::UTF8_FLAG)
+ {
+ std::vector bytes = {0xA, 0xA, 'd', 'e', 'b', 'u', 'g', 'g', 'a', 'b', 'l', 'e', 0};
+ shift = (uint32_t)bytes.size();
+
+ InsertBytes(manifestBytes, stringData + stringOffsets[insertIdx], bytes);
+ }
+ else
+ {
+ std::vector bytes = {0xA, 0x0, 'd', 0, 'e', 0, 'b', 0, 'u', 0, 'g', 0,
+ 'g', 0, 'a', 0, 'b', 0, 'l', 0, 'e', 0, 0, 0};
+ shift = (uint32_t)bytes.size();
+
+ InsertBytes(manifestBytes, stringData + stringOffsets[insertIdx], bytes);
+ }
+
+ // account for added string
+ stringpool->header.size += shift;
+
+ // shift all the offsets *after* the string we inserted (we inserted precisely at that
+ // offset).
+ for(uint32_t i = insertIdx + 1; i < stringpool->stringCount; i++)
+ stringOffsets[i] += shift;
+
+ // if the stringpool isn't integer aligned, add padding bytes
+ uint32_t alignedSize = AlignUp4(stringpool->header.size);
+
+ if(alignedSize > stringpool->header.size)
+ {
+ uint32_t paddingLen = alignedSize - stringpool->header.size;
+
+ RDCDEBUG("Inserting %u padding bytes to align %u up to %u", paddingLen,
+ stringpool->header.size, alignedSize);
+
+ InsertBytes(manifestBytes, base + stringpool->header.size,
+ std::vector((size_t)paddingLen, 0));
+
+ stringpool->header.size += paddingLen;
+ }
+ }
+
+ // now iterate over all nodes and fixup any stringrefs pointing after our insert point
+ cur = start + xmlroot->headerSize;
+ // skip string pool
+ cur += ((ResChunk_header *)cur)->size;
+ // skip resource map
+ cur += ((ResChunk_header *)cur)->size;
+
+ while(cur < end)
+ {
+ ResXMLTree_node *node = (ResXMLTree_node *)cur;
+
+ if(node->header.headerSize != sizeof(*node))
+ RDCWARN("Headersize was reported as %u, but we expected ResXMLTree_node size %zu",
+ node->header.headerSize, sizeof(*node));
+
+ ShiftStringPoolValue(node->comment, insertIdx);
+
+ switch(node->header.type)
+ {
+ // namespace start and end are identical
+ case ResType::NamespaceStart:
+ case ResType::NamespaceEnd:
+ {
+ ResXMLTree_namespaceExt *ns = (ResXMLTree_namespaceExt *)(cur + node->header.headerSize);
+
+ ShiftStringPoolValue(ns->prefix, insertIdx);
+ ShiftStringPoolValue(ns->uri, insertIdx);
+ break;
+ }
+ case ResType::EndElement:
+ {
+ ResXMLTree_endElementExt *endElement =
+ (ResXMLTree_endElementExt *)(cur + node->header.headerSize);
+
+ ShiftStringPoolValue(endElement->ns, insertIdx);
+ ShiftStringPoolValue(endElement->name, insertIdx);
+ break;
+ }
+ case ResType::CDATA:
+ {
+ ResXMLTree_cdataExt *cdata = (ResXMLTree_cdataExt *)(cur + node->header.headerSize);
+
+ ShiftStringPoolValue(cdata->data, insertIdx);
+ ShiftStringPoolValue(cdata->typedData, insertIdx);
+ break;
+ }
+ case ResType::StartElement:
+ {
+ ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->header.headerSize);
+
+ ShiftStringPoolValue(startElement->ns, insertIdx);
+ ShiftStringPoolValue(startElement->name, insertIdx);
+
+ // update attributes
+ byte *attributesStart = cur + node->header.headerSize + startElement->attributeStart;
+
+ for(uint32_t i = 0; i < startElement->attributeCount; i++)
+ {
+ ResXMLTree_attribute *attr =
+ (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i);
+
+ ShiftStringPoolValue(attr->ns, insertIdx);
+ ShiftStringPoolValue(attr->name, insertIdx);
+ ShiftStringPoolValue(attr->rawValue, insertIdx);
+ ShiftStringPoolValue(attr->typedValue, insertIdx);
+ }
+ break;
+ }
+ default:
+ RDCERR("Unhandled chunk %x, can't patch stringpool references", node->header.type);
+ return false;
+ }
+
+ cur += node->header.size;
+ }
+ }
+
+ xmlroot->size = (uint32_t)manifestBytes.size();
+
+ if(manifestBytes.capacity() > capacity)
+ {
+ RDCERR(
+ "manifest vector resized during patching! Update reserve() at the start of "
+ "Android::PatchManifest");
+ }
+
+ return true;
+}
+};
\ No newline at end of file
diff --git a/renderdoc/android/android_patch.cpp b/renderdoc/android/android_patch.cpp
index 913d93733..b14d4699a 100644
--- a/renderdoc/android/android_patch.cpp
+++ b/renderdoc/android/android_patch.cpp
@@ -23,6 +23,7 @@
******************************************************************************/
#include
+#include "3rdparty/miniz/miniz.h"
#include "api/replay/version.h"
#include "core/core.h"
#include "strings/string_utils.h"
@@ -87,6 +88,86 @@ bool RemoveAPKSignature(const string &apk)
return true;
}
+bool ExtractAndRemoveManifest(const std::string &apk, std::vector &manifest)
+{
+ // pull out the manifest with miniz
+ mz_zip_archive zip;
+ RDCEraseEl(zip);
+
+ mz_bool b = mz_zip_reader_init_file(&zip, apk.c_str(), 0);
+
+ if(b)
+ {
+ mz_uint numfiles = mz_zip_reader_get_num_files(&zip);
+
+ for(mz_uint i = 0; i < numfiles; i++)
+ {
+ mz_zip_archive_file_stat zstat;
+ mz_zip_reader_file_stat(&zip, i, &zstat);
+
+ if(!strcmp(zstat.m_filename, "AndroidManifest.xml"))
+ {
+ size_t sz = 0;
+ byte *buf = (byte *)mz_zip_reader_extract_to_heap(&zip, i, &sz, 0);
+
+ RDCLOG("Got manifest of %zu bytes", sz);
+
+ manifest.insert(manifest.begin(), buf, buf + sz);
+ }
+ }
+ }
+ else
+ {
+ RDCERR("Couldn't open %s", apk.c_str());
+ }
+
+ mz_zip_reader_end(&zip);
+
+ if(manifest.empty())
+ return false;
+
+ std::string aapt = getToolPath(ToolDir::BuildTools, "aapt", false);
+
+ RDCDEBUG("Removing AndroidManifest.xml");
+ execCommand(aapt, "remove \"" + apk + "\" AndroidManifest.xml");
+
+ std::string fileList = execCommand(aapt, "list \"" + apk + "\"").strStdout;
+ std::vector files;
+ split(fileList, files, ' ');
+
+ for(const std::string &f : files)
+ {
+ if(trim(f) == "AndroidManifest.xml")
+ {
+ RDCERR("AndroidManifest.xml found, that means removal failed!");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool AddManifestToAPK(const std::string &apk, const std::string &tmpDir,
+ const std::vector &manifest)
+{
+ std::string aapt = getToolPath(ToolDir::BuildTools, "aapt", false);
+
+ // write the manifest to disk
+ FileIO::dump((tmpDir + "AndroidManifest.xml").c_str(), manifest.data(), manifest.size());
+
+ // run aapt to add the manifest
+ Process::ProcessResult result =
+ execCommand(aapt, "add \"" + apk + "\" AndroidManifest.xml", tmpDir);
+
+ if(result.strStdout.empty())
+ {
+ RDCERR("Failed to add manifest to APK. STDERR: %s", result.strStderror.c_str());
+ return false;
+ }
+
+ return true;
+}
+
bool RealignAPK(const string &apk, string &alignedAPK, const string &tmpDir)
{
std::string zipalign = getToolPath(ToolDir::BuildTools, "zipalign", false);
@@ -243,7 +324,10 @@ bool ReinstallPatchedAPK(const string &deviceID, const string &apk, const string
{
RDCLOG("Reinstalling APK");
- adbExecCommand(deviceID, "install --abi " + abi + " \"" + apk + "\"", workDir);
+ if(abi == "null" || abi.empty())
+ adbExecCommand(deviceID, "install \"" + apk + "\"", workDir);
+ else
+ adbExecCommand(deviceID, "install --abi " + abi + " \"" + apk + "\"", workDir);
// Wait until re-install completes
string reinstallResult;
@@ -315,6 +399,37 @@ bool CheckPatchingRequirements()
return true;
}
+std::string DetermineInstalledABI(const std::string &deviceID, const std::string &packageName)
+{
+ RDCLOG("Checking installed ABI for %s", packageName.c_str());
+ string abi;
+
+ string dump = adbExecCommand(deviceID, "shell pm dump " + packageName).strStdout;
+ if(dump.empty())
+ RDCERR("Unable to pm dump %s", packageName.c_str());
+
+ // Walk through the output and look for primaryCpuAbi
+ std::istringstream contents(dump);
+ string line;
+ string prefix("primaryCpuAbi=");
+ while(std::getline(contents, line))
+ {
+ line = trim(line);
+ if(line.compare(0, prefix.size(), prefix) == 0)
+ {
+ // Extract the abi
+ abi = line.substr(line.find_last_of("=") + 1);
+ RDCLOG("primaryCpuAbi found: %s", abi.c_str());
+ break;
+ }
+ }
+
+ if(abi.empty())
+ RDCERR("Unable to determine installed abi for: %s", packageName.c_str());
+
+ return abi;
+}
+
bool PullAPK(const string &deviceID, const string &pkgPath, const string &apk)
{
RDCLOG("Pulling APK to patch");
@@ -369,10 +484,7 @@ std::string GetFirstMatchingLine(const std::string &haystack, const std::string
size_t needleOffset = haystack.find(needle);
if(needleOffset == std::string::npos)
- {
- RDCERR("Couldn't get pkgFlags from adb");
return "";
- }
size_t nextLine = haystack.find('\n', needleOffset + 1);
@@ -430,6 +542,85 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CheckAndroidPackage(const c
extern "C" RENDERDOC_API AndroidFlags RENDERDOC_CC RENDERDOC_MakeDebuggablePackage(
const char *hostname, const char *packageName, RENDERDOC_ProgressCallback progress)
{
- // stub for now
- return AndroidFlags::ManifestPatchFailure;
+ Process::ProcessResult result = {};
+ std::string package(basename(std::string(packageName)));
+
+ int index = 0;
+ std::string deviceID;
+ Android::ExtractDeviceIDAndIndex(hostname, index, deviceID);
+
+ // make sure progress is valid so we don't have to check it everywhere
+ if(!progress)
+ progress = [](float) {};
+
+ progress(0.0f);
+
+ if(!Android::CheckPatchingRequirements())
+ return AndroidFlags::MissingTools;
+
+ progress(0.02f);
+
+ std::string abi = Android::DetermineInstalledABI(deviceID, package);
+
+ // Find the APK on the device
+ std::string pkgPath = Android::GetPathForPackage(deviceID, package) + "base.apk";
+
+ std::string tmpDir = FileIO::GetTempFolderFilename();
+ std::string origAPK(tmpDir + package + ".orig.apk");
+ std::string alignedAPK(origAPK + ".aligned.apk");
+ std::vector manifest;
+
+ // Try the following steps, bailing if anything fails
+ if(!Android::PullAPK(deviceID, pkgPath, origAPK))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(0.4f);
+
+ if(!Android::RemoveAPKSignature(origAPK))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(0.425f);
+
+ if(!Android::ExtractAndRemoveManifest(origAPK, manifest))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(0.45f);
+
+ if(!Android::PatchManifest(manifest))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(0.46f);
+
+ if(!Android::AddManifestToAPK(origAPK, tmpDir, manifest))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(0.475f);
+
+ if(!Android::RealignAPK(origAPK, alignedAPK, tmpDir))
+ return AndroidFlags::RepackagingAPKFailure;
+
+ progress(0.5f);
+
+ if(!Android::DebugSignAPK(alignedAPK, tmpDir))
+ return AndroidFlags::RepackagingAPKFailure;
+
+ progress(0.525f);
+
+ if(!Android::UninstallOriginalAPK(deviceID, packageName, tmpDir))
+ return AndroidFlags::RepackagingAPKFailure;
+
+ progress(0.6f);
+
+ if(!Android::ReinstallPatchedAPK(deviceID, alignedAPK, abi, packageName, tmpDir))
+ return AndroidFlags::RepackagingAPKFailure;
+
+ progress(0.95f);
+
+ if(!Android::IsDebuggable(deviceID, packageName))
+ return AndroidFlags::ManifestPatchFailure;
+
+ progress(1.0f);
+
+ // All clean!
+ return AndroidFlags::Debuggable;
}
diff --git a/renderdoc/android/android_utils.h b/renderdoc/android/android_utils.h
index 5472fbc02..cfc172343 100644
--- a/renderdoc/android/android_utils.h
+++ b/renderdoc/android/android_utils.h
@@ -65,4 +65,6 @@ ABI GetABI(const std::string &abiName);
std::vector GetSupportedABIs(const std::string &deviceID);
std::string GetRenderDocPackageForABI(ABI abi);
std::string GetPathForPackage(const std::string &deviceID, const std::string &packageName);
+
+bool PatchManifest(std::vector &manifest);
};
diff --git a/renderdoc/renderdoc.vcxproj b/renderdoc/renderdoc.vcxproj
index fb9dbe04e..dee7119fc 100644
--- a/renderdoc/renderdoc.vcxproj
+++ b/renderdoc/renderdoc.vcxproj
@@ -105,6 +105,7 @@
+
@@ -308,6 +309,7 @@
+
diff --git a/renderdoc/renderdoc.vcxproj.filters b/renderdoc/renderdoc.vcxproj.filters
index b904265a7..482b1d843 100644
--- a/renderdoc/renderdoc.vcxproj.filters
+++ b/renderdoc/renderdoc.vcxproj.filters
@@ -112,6 +112,9 @@
{f9ed2873-6120-4b34-a3c2-e57b2de31eb9}
+
+ {6e798cbc-0eae-4f48-9d7c-f3fe58bd32dc}
+
@@ -384,6 +387,9 @@
Android
+
+ 3rdparty\android
+
@@ -677,6 +683,9 @@
Android
+
+ Android
+