diff options
Diffstat (limited to 'tools')
270 files changed, 27745 insertions, 1017 deletions
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp index 2d35129..d346731 100644 --- a/tools/aapt/AaptAssets.cpp +++ b/tools/aapt/AaptAssets.cpp @@ -345,7 +345,8 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta return ++currentIndex; } else { - if ((part.length() == 2 || part.length() == 3) && isAlpha(part)) { + if ((part.length() == 2 || part.length() == 3) + && isAlpha(part) && strcmp("car", part.string())) { setLanguage(part); if (++currentIndex == size) { return size; @@ -366,33 +367,6 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta return currentIndex; } - -String8 AaptLocaleValue::toDirName() const { - String8 dirName(""); - if (language[0]) { - dirName += language; - } else { - return dirName; - } - - if (script[0]) { - dirName += "-s"; - dirName += script; - } - - if (region[0]) { - dirName += "-r"; - dirName += region; - } - - if (variant[0]) { - dirName += "-v"; - dirName += variant; - } - - return dirName; -} - void AaptLocaleValue::initFromResTable(const ResTable_config& config) { config.unpackLanguage(language); config.unpackRegion(region); diff --git a/tools/aapt/AaptAssets.h b/tools/aapt/AaptAssets.h index 7ae5368..4fdc964 100644 --- a/tools/aapt/AaptAssets.h +++ b/tools/aapt/AaptAssets.h @@ -78,8 +78,6 @@ struct AaptLocaleValue { void writeTo(ResTable_config* out) const; - String8 toDirName() const; - int compare(const AaptLocaleValue& other) const { return memcmp(this, &other, sizeof(AaptLocaleValue)); } diff --git a/tools/aapt/AaptConfig.cpp b/tools/aapt/AaptConfig.cpp index ede9e99..b12867a 100644 --- a/tools/aapt/AaptConfig.cpp +++ b/tools/aapt/AaptConfig.cpp @@ -123,6 +123,14 @@ bool parse(const String8& str, ConfigDescription* out) { part = parts[index].string(); } + if (parseScreenRound(part, &config)) { + index++; + if (index == N) { + goto success; + } + part = parts[index].string(); + } + if (parseOrientation(part, &config)) { index++; if (index == N) { @@ -241,7 +249,9 @@ void applyVersionForCompatibility(ConfigDescription* config) { } uint16_t minSdk = 0; - if (config->density == ResTable_config::DENSITY_ANY) { + if (config->screenLayout2 & ResTable_config::MASK_SCREENROUND) { + minSdk = SDK_MNC; + } else if (config->density == ResTable_config::DENSITY_ANY) { minSdk = SDK_LOLLIPOP; } else if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY || config->screenWidthDp != ResTable_config::SCREENWIDTH_ANY @@ -395,7 +405,26 @@ bool parseScreenLayoutLong(const char* name, ResTable_config* out) { | ResTable_config::SCREENLONG_NO; return true; } + return false; +} +bool parseScreenRound(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_ANY; + return true; + } else if (strcmp(name, "round") == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_YES; + return true; + } else if (strcmp(name, "notround") == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_NO; + return true; + } return false; } diff --git a/tools/aapt/AaptConfig.h b/tools/aapt/AaptConfig.h index f73a508..04c763f 100644 --- a/tools/aapt/AaptConfig.h +++ b/tools/aapt/AaptConfig.h @@ -60,6 +60,7 @@ bool parseScreenWidthDp(const char* str, android::ResTable_config* out = NULL); bool parseScreenHeightDp(const char* str, android::ResTable_config* out = NULL); bool parseScreenLayoutSize(const char* str, android::ResTable_config* out = NULL); bool parseScreenLayoutLong(const char* str, android::ResTable_config* out = NULL); +bool parseScreenRound(const char* name, android::ResTable_config* out = NULL); bool parseOrientation(const char* str, android::ResTable_config* out = NULL); bool parseUiModeType(const char* str, android::ResTable_config* out = NULL); bool parseUiModeNight(const char* str, android::ResTable_config* out = NULL); diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk index 9956bd7..bbe6860 100644 --- a/tools/aapt/Android.mk +++ b/tools/aapt/Android.mk @@ -64,7 +64,8 @@ aaptHostStaticLibs := \ libutils \ libcutils \ libexpat \ - libziparchive-host + libziparchive-host \ + libbase aaptCFlags := -DAAPT_VERSION=\"$(BUILD_NUMBER)\" aaptCFlags += -Wall -Werror @@ -131,28 +132,4 @@ LOCAL_STATIC_LIBRARIES += libaapt $(aaptHostStaticLibs) include $(BUILD_HOST_NATIVE_TEST) -# ========================================================== -# Build the device executable: aapt -# ========================================================== -ifneq ($(SDK_ONLY),true) -include $(CLEAR_VARS) - -LOCAL_MODULE := aapt -LOCAL_CFLAGS += $(aaptCFlags) -LOCAL_SRC_FILES := $(aaptSources) $(aaptMain) -LOCAL_C_INCLUDES += $(aaptCIncludes) -LOCAL_SHARED_LIBRARIES := \ - libandroidfw \ - libutils \ - libcutils \ - libpng \ - liblog \ - libz -LOCAL_STATIC_LIBRARIES := \ - libexpat_static - -include $(BUILD_EXECUTABLE) - -endif # Not SDK_ONLY - endif # No TARGET_BUILD_APPS or TARGET_BUILD_PDK diff --git a/tools/aapt/Bundle.h b/tools/aapt/Bundle.h index e7cde74..cbe7c5d 100644 --- a/tools/aapt/Bundle.h +++ b/tools/aapt/Bundle.h @@ -54,13 +54,14 @@ public: mWantUTF16(false), mValues(false), mIncludeMetaData(false), mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL), mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL), - mAutoAddOverlay(false), mGenDependencies(false), + mAutoAddOverlay(false), mGenDependencies(false), mNoVersionVectors(false), mCrunchedOutputDir(NULL), mProguardFile(NULL), mAndroidManifestFile(NULL), mPublicOutputFile(NULL), mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL), mMinSdkVersion(NULL), mTargetSdkVersion(NULL), mMaxSdkVersion(NULL), mVersionCode(NULL), mVersionName(NULL), mReplaceVersion(false), mCustomPackage(NULL), mExtraPackages(NULL), mMaxResVersion(NULL), mDebugMode(false), mNonConstantId(false), + mSkipSymbolsWithoutDefaultLocalization(false), mProduct(NULL), mUseCrunchCache(false), mErrorOnFailedInsert(false), mErrorOnMissingConfigEntry(false), mOutputTextSymbols(NULL), mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(NULL), @@ -191,6 +192,8 @@ public: void setDebugMode(bool val) { mDebugMode = val; } bool getNonConstantId() const { return mNonConstantId; } void setNonConstantId(bool val) { mNonConstantId = val; } + bool getSkipSymbolsWithoutDefaultLocalization() const { return mSkipSymbolsWithoutDefaultLocalization; } + void setSkipSymbolsWithoutDefaultLocalization(bool val) { mSkipSymbolsWithoutDefaultLocalization = val; } const char* getProduct() const { return mProduct; } void setProduct(const char * val) { mProduct = val; } void setUseCrunchCache(bool val) { mUseCrunchCache = val; } @@ -203,6 +206,8 @@ public: void setSingleCrunchOutputFile(const char* val) { mSingleCrunchOutputFile = val; } bool getBuildSharedLibrary() const { return mBuildSharedLibrary; } void setBuildSharedLibrary(bool val) { mBuildSharedLibrary = val; } + void setNoVersionVectors(bool val) { mNoVersionVectors = val; } + bool getNoVersionVectors() const { return mNoVersionVectors; } /* * Set and get the file specification. @@ -282,6 +287,7 @@ private: const char* mInstrumentationPackageNameOverride; bool mAutoAddOverlay; bool mGenDependencies; + bool mNoVersionVectors; const char* mCrunchedOutputDir; const char* mProguardFile; const char* mAndroidManifestFile; @@ -312,6 +318,7 @@ private: const char* mMaxResVersion; bool mDebugMode; bool mNonConstantId; + bool mSkipSymbolsWithoutDefaultLocalization; const char* mProduct; bool mUseCrunchCache; bool mErrorOnFailedInsert; diff --git a/tools/aapt/Main.cpp b/tools/aapt/Main.cpp index 8b416aa..f832c60 100644 --- a/tools/aapt/Main.cpp +++ b/tools/aapt/Main.cpp @@ -211,7 +211,12 @@ void usage(void) " specified folder.\n" " --ignore-assets\n" " Assets to be ignored. Default pattern is:\n" - " %s\n", + " %s\n" + " --skip-symbols-without-default-localization\n" + " Prevents symbols from being generated for strings that do not have a default\n" + " localization\n" + " --no-version-vectors\n" + " Do not automatically generate versioned copies of vector XML resources.\n", gDefaultIgnoreAssets); } @@ -657,6 +662,8 @@ int main(int argc, char* const argv[]) bundle.setProduct(argv[0]); } else if (strcmp(cp, "-non-constant-id") == 0) { bundle.setNonConstantId(true); + } else if (strcmp(cp, "-skip-symbols-without-default-localization") == 0) { + bundle.setSkipSymbolsWithoutDefaultLocalization(true); } else if (strcmp(cp, "-shared-lib") == 0) { bundle.setNonConstantId(true); bundle.setBuildSharedLibrary(true); @@ -673,6 +680,8 @@ int main(int argc, char* const argv[]) gUserIgnoreAssets = argv[0]; } else if (strcmp(cp, "-pseudo-localize") == 0) { bundle.setPseudolocalize(PSEUDO_ACCENTED | PSEUDO_BIDI); + } else if (strcmp(cp, "-no-version-vectors") == 0) { + bundle.setNoVersionVectors(true); } else { fprintf(stderr, "ERROR: Unknown option '-%s'\n", cp); wantUsage = true; diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp index beb94fd..5d20815 100644 --- a/tools/aapt/Resource.cpp +++ b/tools/aapt/Resource.cpp @@ -1604,7 +1604,7 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil if (table.hasResources()) { sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); - err = table.addSymbols(symbols); + err = table.addSymbols(symbols, bundle->getSkipSymbolsWithoutDefaultLocalization()); if (err < NO_ERROR) { return err; } diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index c5fccbf..e64fdf7 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -913,6 +913,7 @@ status_t compileResourceFile(Bundle* bundle, if (code == ResXMLTree::START_TAG) { const String16* curTag = NULL; String16 curType; + String16 curName; int32_t curFormat = ResTable_map::TYPE_ANY; bool curIsBag = false; bool curIsBagReplaceOnOverwrite = false; @@ -1321,6 +1322,10 @@ status_t compileResourceFile(Bundle* bundle, ssize_t attri = block.indexOfAttribute(NULL, "type"); if (attri >= 0) { curType = String16(block.getAttributeStringValue(attri, &len)); + ssize_t nameIdx = block.indexOfAttribute(NULL, "name"); + if (nameIdx >= 0) { + curName = String16(block.getAttributeStringValue(nameIdx, &len)); + } ssize_t formatIdx = block.indexOfAttribute(NULL, "format"); if (formatIdx >= 0) { String16 formatStr = String16(block.getAttributeStringValue( @@ -1363,6 +1368,9 @@ status_t compileResourceFile(Bundle* bundle, } if (name.size() > 0) { + if (locale.size() == 0) { + outTable->addDefaultLocalization(name); + } if (translatable == false16) { curIsFormatted = false; // Untranslatable strings must only exist in the default [empty] locale @@ -1658,6 +1666,9 @@ status_t compileResourceFile(Bundle* bundle, hasErrors = localHasErrors = true; } else if (err == NO_ERROR) { + if (curType == string16 && !curParams.language[0] && !curParams.country[0]) { + outTable->addDefaultLocalization(curName); + } if (curIsPseudolocalizable && localeIsDefined(curParams) && bundle->getPseudolocalize() > 0) { // pseudolocalize here @@ -2622,8 +2633,11 @@ status_t ResourceTable::assignResourceIds() return firstError; } -status_t ResourceTable::addSymbols(const sp<AaptSymbols>& outSymbols) { +status_t ResourceTable::addSymbols(const sp<AaptSymbols>& outSymbols, + bool skipSymbolsWithoutDefaultLocalization) { const size_t N = mOrderedPackages.size(); + const String8 defaultLocale; + const String16 stringType("string"); size_t pi; for (pi=0; pi<N; pi++) { @@ -2664,6 +2678,19 @@ status_t ResourceTable::addSymbols(const sp<AaptSymbols>& outSymbols) { return UNKNOWN_ERROR; } if (Res_GETPACKAGE(rid) + 1 == p->getAssignedId()) { + + if (skipSymbolsWithoutDefaultLocalization && + t->getName() == stringType) { + + // Don't generate symbols for strings without a default localization. + if (mHasDefaultLocalization.find(c->getName()) + == mHasDefaultLocalization.end()) { + // printf("Skip symbol [%08x] %s\n", rid, + // String8(c->getName()).string()); + continue; + } + } + typeSymbols->addSymbol(String8(c->getName()), rid, c->getPos()); String16 comment(c->getComment()); @@ -2686,6 +2713,12 @@ ResourceTable::addLocalization(const String16& name, const String8& locale, cons mLocalizations[name][locale] = src; } +void +ResourceTable::addDefaultLocalization(const String16& name) +{ + mHasDefaultLocalization.insert(name); +} + /*! * Flag various sorts of localization problems. '+' indicates checks already implemented; @@ -4611,6 +4644,9 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, const String16& resourceName, const sp<AaptFile>& target, const sp<XMLNode>& root) { + const String16 vector16("vector"); + const String16 animatedVector16("animated-vector"); + const int minSdk = getMinSdkVersion(bundle); if (minSdk >= SDK_LOLLIPOP_MR1) { // Lollipop MR1 and up handles public attributes differently, no @@ -4620,8 +4656,8 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, const ConfigDescription config(target->getGroupEntry().toParams()); if (target->getResourceType() == "" || config.sdkVersion >= SDK_LOLLIPOP_MR1) { - // Skip resources that have no type (AndroidManifest.xml) or are already version qualified with v21 - // or higher. + // Skip resources that have no type (AndroidManifest.xml) or are already version qualified + // with v21 or higher. return NO_ERROR; } @@ -4635,6 +4671,12 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, sp<XMLNode> node = nodesToVisit.top(); nodesToVisit.pop(); + if (bundle->getNoVersionVectors() && (node->getElementName() == vector16 || + node->getElementName() == animatedVector16)) { + // We were told not to version vector tags, so skip the children here. + continue; + } + const Vector<XMLNode::attribute_entry>& attrs = node->getAttributes(); for (size_t i = 0; i < attrs.size(); i++) { const XMLNode::attribute_entry& attr = attrs[i]; diff --git a/tools/aapt/ResourceTable.h b/tools/aapt/ResourceTable.h index 9644224..2c1bec1 100644 --- a/tools/aapt/ResourceTable.h +++ b/tools/aapt/ResourceTable.h @@ -235,8 +235,10 @@ public: const ConfigDescription* config = NULL); status_t assignResourceIds(); - status_t addSymbols(const sp<AaptSymbols>& outSymbols = NULL); + status_t addSymbols(const sp<AaptSymbols>& outSymbols = NULL, + bool skipSymbolsWithoutDefaultLocalization = false); void addLocalization(const String16& name, const String8& locale, const SourcePos& src); + void addDefaultLocalization(const String16& name); status_t validateLocalizations(void); status_t flatten(Bundle* bundle, const sp<const ResourceFilter>& filter, @@ -588,6 +590,8 @@ private: // key = string resource name, value = set of locales in which that name is defined std::map<String16, std::map<String8, SourcePos>> mLocalizations; + // set of string resources names that have a default localization + std::set<String16> mHasDefaultLocalization; std::queue<CompileResourceWorkItem> mWorkQueue; }; diff --git a/tools/aapt/SdkConstants.h b/tools/aapt/SdkConstants.h index 4e0fe10..16e622a 100644 --- a/tools/aapt/SdkConstants.h +++ b/tools/aapt/SdkConstants.h @@ -38,6 +38,7 @@ enum { SDK_KITKAT_WATCH = 20, SDK_LOLLIPOP = 21, SDK_LOLLIPOP_MR1 = 22, + SDK_MNC = 23, }; #endif // H_AAPT_SDK_CONSTANTS diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp index 9033cf7..6902a30 100644 --- a/tools/aapt/XMLNode.cpp +++ b/tools/aapt/XMLNode.cpp @@ -466,7 +466,7 @@ void printXMLBlock(ResXMLTree* block) block->restart(); Vector<namespace_entry> namespaces; - + ResXMLTree::event_code_t code; int depth = 0; while ((code=block->next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { @@ -520,7 +520,12 @@ void printXMLBlock(ResXMLTree* block) printf("\n"); } } else if (code == ResXMLTree::END_TAG) { - depth--; + // Invalid tag nesting can be misused to break the parsing + // code below. Break if detected. + if (--depth < 0) { + printf("***BAD DEPTH in XMLBlock: %d\n", depth); + break; + } } else if (code == ResXMLTree::START_NAMESPACE) { namespace_entry ns; size_t len; @@ -536,7 +541,10 @@ void printXMLBlock(ResXMLTree* block) ns.uri.string()); depth++; } else if (code == ResXMLTree::END_NAMESPACE) { - depth--; + if (--depth < 0) { + printf("***BAD DEPTH in XMLBlock: %d\n", depth); + break; + } const namespace_entry& ns = namespaces.top(); size_t len; const char16_t* prefix16 = block->getNamespacePrefix(&len); @@ -714,7 +722,7 @@ const String8& XMLNode::getFilename() const { return mFilename; } - + const Vector<XMLNode::attribute_entry>& XMLNode::getAttributes() const { @@ -730,7 +738,7 @@ const XMLNode::attribute_entry* XMLNode::getAttribute(const String16& ns, return &ae; } } - + return NULL; } @@ -774,14 +782,14 @@ sp<XMLNode> XMLNode::searchElement(const String16& tagNamespace, const String16& && mElementName == tagName) { return this; } - + for (size_t i=0; i<mChildren.size(); i++) { sp<XMLNode> found = mChildren.itemAt(i)->searchElement(tagNamespace, tagName); if (found != NULL) { return found; } } - + return NULL; } @@ -795,7 +803,7 @@ sp<XMLNode> XMLNode::getChildElement(const String16& tagNamespace, const String1 return child; } } - + return NULL; } @@ -977,7 +985,7 @@ status_t XMLNode::parseValues(const sp<AaptAssets>& assets, ResourceTable* table) { bool hasErrors = false; - + if (getType() == TYPE_ELEMENT) { const size_t N = mAttributes.size(); String16 defPackage(assets->getPackage()); @@ -1013,7 +1021,7 @@ status_t XMLNode::assignResourceIds(const sp<AaptAssets>& assets, const ResourceTable* table) { bool hasErrors = false; - + if (getType() == TYPE_ELEMENT) { String16 attr("attr"); const char* errorMsg; @@ -1093,7 +1101,7 @@ status_t XMLNode::flatten(const sp<AaptFile>& dest, { StringPool strings(mUTF8); Vector<uint32_t> resids; - + // First collect just the strings for attribute names that have a // resource ID assigned to them. This ensures that the resource ID // array is compact, and makes it easier to deal with attribute names @@ -1141,7 +1149,7 @@ status_t XMLNode::flatten(const sp<AaptFile>& dest, dest->getSize(), (stringPool->getSize()*100)/dest->getSize(), dest->getPath().string()); } - + return NO_ERROR; } @@ -1217,7 +1225,7 @@ XMLNode::startNamespace(void *userData, const char *prefix, const char *uri) printf("Start Namespace: %s %s\n", prefix, uri); } ParseState* st = (ParseState*)userData; - sp<XMLNode> node = XMLNode::newNamespace(st->filename, + sp<XMLNode> node = XMLNode::newNamespace(st->filename, String16(prefix != NULL ? prefix : ""), String16(uri)); node->setStartLineNumber(XML_GetCurrentLineNumber(st->parser)); if (st->stack.size() > 0) { @@ -1338,7 +1346,7 @@ status_t XMLNode::collect_strings(StringPool* dest, Vector<uint32_t>* outResIds, bool stripComments, bool stripRawValues) const { collect_attr_strings(dest, outResIds, true); - + int i; if (RESOURCES_TOOLS_NAMESPACE != mNamespaceUri) { if (mNamespacePrefix.size() > 0) { diff --git a/tools/aapt/tests/AaptConfig_test.cpp b/tools/aapt/tests/AaptConfig_test.cpp index e795d81..8bb38ba 100644 --- a/tools/aapt/tests/AaptConfig_test.cpp +++ b/tools/aapt/tests/AaptConfig_test.cpp @@ -19,6 +19,7 @@ #include "AaptConfig.h" #include "ConfigDescription.h" +#include "SdkConstants.h" #include "TestHelper.h" using android::String8; @@ -65,7 +66,7 @@ TEST(AaptConfigTest, ParseBasicQualifiers) { TEST(AaptConfigTest, ParseLocales) { ConfigDescription config; EXPECT_TRUE(TestParse("en-rUS", &config)); - EXPECT_EQ(String8("en-US"), config.toString()); + EXPECT_EQ(String8("en-rUS"), config.toString()); } TEST(AaptConfigTest, ParseQualifierAddedInApi13) { @@ -76,3 +77,24 @@ TEST(AaptConfigTest, ParseQualifierAddedInApi13) { EXPECT_TRUE(TestParse("sw600dp-v8", &config)); EXPECT_EQ(String8("sw600dp-v13"), config.toString()); } + +TEST(AaptConfigTest, TestParsingOfCarAttribute) { + ConfigDescription config; + EXPECT_TRUE(TestParse("car", &config)); + EXPECT_EQ(android::ResTable_config::UI_MODE_TYPE_CAR, config.uiMode); +} + +TEST(AaptConfigTest, TestParsingRoundQualifier) { + ConfigDescription config; + EXPECT_TRUE(TestParse("round", &config)); + EXPECT_EQ(android::ResTable_config::SCREENROUND_YES, + config.screenLayout2 & android::ResTable_config::MASK_SCREENROUND); + EXPECT_EQ(SDK_MNC, config.sdkVersion); + EXPECT_EQ(String8("round-v23"), config.toString()); + + EXPECT_TRUE(TestParse("notround", &config)); + EXPECT_EQ(android::ResTable_config::SCREENROUND_NO, + config.screenLayout2 & android::ResTable_config::MASK_SCREENROUND); + EXPECT_EQ(SDK_MNC, config.sdkVersion); + EXPECT_EQ(String8("notround-v23"), config.toString()); +} diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk new file mode 100644 index 0000000..10f8150 --- /dev/null +++ b/tools/aapt2/Android.mk @@ -0,0 +1,157 @@ +# +# Copyright (C) 2015 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. +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)),) + +# ========================================================== +# Setup some common variables for the different build +# targets here. +# ========================================================== +LOCAL_PATH:= $(call my-dir) + +main := Main.cpp +sources := \ + BigBuffer.cpp \ + BinaryResourceParser.cpp \ + BindingXmlPullParser.cpp \ + ConfigDescription.cpp \ + Debug.cpp \ + Files.cpp \ + Flag.cpp \ + JavaClassGenerator.cpp \ + Linker.cpp \ + Locale.cpp \ + Logger.cpp \ + ManifestMerger.cpp \ + ManifestParser.cpp \ + ManifestValidator.cpp \ + Png.cpp \ + ProguardRules.cpp \ + ResChunkPullParser.cpp \ + Resource.cpp \ + ResourceParser.cpp \ + ResourceTable.cpp \ + ResourceTableResolver.cpp \ + ResourceValues.cpp \ + SdkConstants.cpp \ + StringPool.cpp \ + TableFlattener.cpp \ + Util.cpp \ + ScopedXmlPullParser.cpp \ + SourceXmlPullParser.cpp \ + XliffXmlPullParser.cpp \ + XmlDom.cpp \ + XmlFlattener.cpp \ + ZipEntry.cpp \ + ZipFile.cpp + +testSources := \ + BigBuffer_test.cpp \ + BindingXmlPullParser_test.cpp \ + Compat_test.cpp \ + ConfigDescription_test.cpp \ + JavaClassGenerator_test.cpp \ + Linker_test.cpp \ + Locale_test.cpp \ + ManifestMerger_test.cpp \ + ManifestParser_test.cpp \ + Maybe_test.cpp \ + NameMangler_test.cpp \ + ResourceParser_test.cpp \ + Resource_test.cpp \ + ResourceTable_test.cpp \ + ScopedXmlPullParser_test.cpp \ + StringPiece_test.cpp \ + StringPool_test.cpp \ + Util_test.cpp \ + XliffXmlPullParser_test.cpp \ + XmlDom_test.cpp \ + XmlFlattener_test.cpp + +cIncludes := \ + external/libpng \ + external/libz + +hostLdLibs := + +hostStaticLibs := \ + libandroidfw \ + libutils \ + liblog \ + libcutils \ + libexpat \ + libziparchive-host \ + libpng \ + libbase + +ifneq ($(strip $(USE_MINGW)),) + hostStaticLibs += libz +else + hostLdLibs += -lz +endif + +cFlags := -Wall -Werror -Wno-unused-parameter -UNDEBUG +cppFlags := -std=c++11 -Wno-missing-field-initializers -Wno-unused-private-field + +# ========================================================== +# Build the host static library: libaapt2 +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libaapt2 + +LOCAL_SRC_FILES := $(sources) +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_STATIC_LIBRARY) + + +# ========================================================== +# Build the host tests: libaapt2_tests +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libaapt2_tests +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(testSources) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libaapt2 $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_NATIVE_TEST) + +# ========================================================== +# Build the host executable: aapt2 +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := aapt2 + +LOCAL_SRC_FILES := $(main) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libaapt2 $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_EXECUTABLE) + +endif # No TARGET_BUILD_APPS or TARGET_BUILD_PDK diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h new file mode 100644 index 0000000..30047f7 --- /dev/null +++ b/tools/aapt2/AppInfo.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_APP_INFO_H +#define AAPT_APP_INFO_H + +#include <string> + +namespace aapt { + +/** + * Holds basic information about the app being built. Most of this information + * will come from the app's AndroidManifest. + */ +struct AppInfo { + /** + * App's package name. + */ + std::u16string package; +}; + +} // namespace aapt + +#endif // AAPT_APP_INFO_H diff --git a/tools/aapt2/BigBuffer.cpp b/tools/aapt2/BigBuffer.cpp new file mode 100644 index 0000000..8f57172 --- /dev/null +++ b/tools/aapt2/BigBuffer.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" + +#include <algorithm> +#include <memory> +#include <vector> + +namespace aapt { + +void* BigBuffer::nextBlockImpl(size_t size) { + if (!mBlocks.empty()) { + Block& block = mBlocks.back(); + if (block.mBlockSize - block.size >= size) { + void* outBuffer = block.buffer.get() + block.size; + block.size += size; + mSize += size; + return outBuffer; + } + } + + const size_t actualSize = std::max(mBlockSize, size); + + Block block = {}; + + // Zero-allocate the block's buffer. + block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[actualSize]()); + assert(block.buffer); + + block.size = size; + block.mBlockSize = actualSize; + + mBlocks.push_back(std::move(block)); + mSize += size; + return mBlocks.back().buffer.get(); +} + +} // namespace aapt diff --git a/tools/aapt2/BigBuffer.h b/tools/aapt2/BigBuffer.h new file mode 100644 index 0000000..8b6569c --- /dev/null +++ b/tools/aapt2/BigBuffer.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_BIG_BUFFER_H +#define AAPT_BIG_BUFFER_H + +#include <cassert> +#include <cstring> +#include <memory> +#include <vector> + +namespace aapt { + +/** + * Inspired by protobuf's ZeroCopyOutputStream, offers blocks of memory + * in which to write without knowing the full size of the entire payload. + * This is essentially a list of memory blocks. As one fills up, another + * block is allocated and appended to the end of the list. + */ +class BigBuffer { +public: + /** + * A contiguous block of allocated memory. + */ + struct Block { + /** + * Pointer to the memory. + */ + std::unique_ptr<uint8_t[]> buffer; + + /** + * Size of memory that is currently occupied. The actual + * allocation may be larger. + */ + size_t size; + + private: + friend class BigBuffer; + + /** + * The size of the memory block allocation. + */ + size_t mBlockSize; + }; + + typedef std::vector<Block>::const_iterator const_iterator; + + /** + * Create a BigBuffer with block allocation sizes + * of blockSize. + */ + BigBuffer(size_t blockSize); + + BigBuffer(const BigBuffer&) = delete; // No copying. + + BigBuffer(BigBuffer&& rhs); + + /** + * Number of occupied bytes in all the allocated blocks. + */ + size_t size() const; + + /** + * Returns a pointer to an array of T, where T is + * a POD type. The elements are zero-initialized. + */ + template <typename T> + T* nextBlock(size_t count = 1); + + /** + * Moves the specified BigBuffer into this one. When this method + * returns, buffer is empty. + */ + void appendBuffer(BigBuffer&& buffer); + + /** + * Pads the block with 'bytes' bytes of zero values. + */ + void pad(size_t bytes); + + /** + * Pads the block so that it aligns on a 4 byte boundary. + */ + void align4(); + + const_iterator begin() const; + const_iterator end() const; + +private: + /** + * Returns a pointer to a buffer of the requested size. + * The buffer is zero-initialized. + */ + void* nextBlockImpl(size_t size); + + size_t mBlockSize; + size_t mSize; + std::vector<Block> mBlocks; +}; + +inline BigBuffer::BigBuffer(size_t blockSize) : mBlockSize(blockSize), mSize(0) { +} + +inline BigBuffer::BigBuffer(BigBuffer&& rhs) : + mBlockSize(rhs.mBlockSize), mSize(rhs.mSize), mBlocks(std::move(rhs.mBlocks)) { +} + +inline size_t BigBuffer::size() const { + return mSize; +} + +template <typename T> +inline T* BigBuffer::nextBlock(size_t count) { + assert(count != 0); + return reinterpret_cast<T*>(nextBlockImpl(sizeof(T) * count)); +} + +inline void BigBuffer::appendBuffer(BigBuffer&& buffer) { + std::move(buffer.mBlocks.begin(), buffer.mBlocks.end(), std::back_inserter(mBlocks)); + mSize += buffer.mSize; + buffer.mBlocks.clear(); + buffer.mSize = 0; +} + +inline void BigBuffer::pad(size_t bytes) { + nextBlock<char>(bytes); +} + +inline void BigBuffer::align4() { + const size_t unaligned = mSize % 4; + if (unaligned != 0) { + pad(4 - unaligned); + } +} + +inline BigBuffer::const_iterator BigBuffer::begin() const { + return mBlocks.begin(); +} + +inline BigBuffer::const_iterator BigBuffer::end() const { + return mBlocks.end(); +} + +} // namespace aapt + +#endif // AAPT_BIG_BUFFER_H diff --git a/tools/aapt2/BigBuffer_test.cpp b/tools/aapt2/BigBuffer_test.cpp new file mode 100644 index 0000000..01ee8d7 --- /dev/null +++ b/tools/aapt2/BigBuffer_test.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(BigBufferTest, AllocateSingleBlock) { + BigBuffer buffer(4); + + EXPECT_NE(nullptr, buffer.nextBlock<char>(2)); + EXPECT_EQ(2u, buffer.size()); +} + +TEST(BigBufferTest, ReturnSameBlockIfNextAllocationFits) { + BigBuffer buffer(16); + + char* b1 = buffer.nextBlock<char>(8); + EXPECT_NE(nullptr, b1); + + char* b2 = buffer.nextBlock<char>(4); + EXPECT_NE(nullptr, b2); + + EXPECT_EQ(b1 + 8, b2); +} + +TEST(BigBufferTest, AllocateExactSizeBlockIfLargerThanBlockSize) { + BigBuffer buffer(16); + + EXPECT_NE(nullptr, buffer.nextBlock<char>(32)); + EXPECT_EQ(32u, buffer.size()); +} + +TEST(BigBufferTest, AppendAndMoveBlock) { + BigBuffer buffer(16); + + uint32_t* b1 = buffer.nextBlock<uint32_t>(); + ASSERT_NE(nullptr, b1); + *b1 = 33; + + { + BigBuffer buffer2(16); + b1 = buffer2.nextBlock<uint32_t>(); + ASSERT_NE(nullptr, b1); + *b1 = 44; + + buffer.appendBuffer(std::move(buffer2)); + EXPECT_EQ(0u, buffer2.size()); + EXPECT_EQ(buffer2.begin(), buffer2.end()); + } + + EXPECT_EQ(2 * sizeof(uint32_t), buffer.size()); + + auto b = buffer.begin(); + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(33u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(44u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_EQ(b, buffer.end()); +} + +TEST(BigBufferTest, PadAndAlignProperly) { + BigBuffer buffer(16); + + ASSERT_NE(buffer.nextBlock<char>(2), nullptr); + ASSERT_EQ(2u, buffer.size()); + buffer.pad(2); + ASSERT_EQ(4u, buffer.size()); + buffer.align4(); + ASSERT_EQ(4u, buffer.size()); + buffer.pad(2); + ASSERT_EQ(6u, buffer.size()); + buffer.align4(); + ASSERT_EQ(8u, buffer.size()); +} + +} // namespace aapt diff --git a/tools/aapt2/BinaryResourceParser.cpp b/tools/aapt2/BinaryResourceParser.cpp new file mode 100644 index 0000000..3559f43 --- /dev/null +++ b/tools/aapt2/BinaryResourceParser.cpp @@ -0,0 +1,877 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BinaryResourceParser.h" +#include "Logger.h" +#include "ResChunkPullParser.h" +#include "Resolver.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <androidfw/TypeWrappers.h> +#include <map> +#include <string> + +namespace aapt { + +using namespace android; + +/* + * Visitor that converts a reference's resource ID to a resource name, + * given a mapping from resource ID to resource name. + */ +struct ReferenceIdToNameVisitor : ValueVisitor { + ReferenceIdToNameVisitor(const std::shared_ptr<IResolver>& resolver, + std::map<ResourceId, ResourceName>* cache) : + mResolver(resolver), mCache(cache) { + } + + void visit(Reference& reference, ValueVisitorArgs&) override { + idToName(reference); + } + + void visit(Attribute& attr, ValueVisitorArgs&) override { + for (auto& entry : attr.symbols) { + idToName(entry.symbol); + } + } + + void visit(Style& style, ValueVisitorArgs&) override { + if (style.parent.id.isValid()) { + idToName(style.parent); + } + + for (auto& entry : style.entries) { + idToName(entry.key); + entry.value->accept(*this, {}); + } + } + + void visit(Styleable& styleable, ValueVisitorArgs&) override { + for (auto& attr : styleable.entries) { + idToName(attr); + } + } + + void visit(Array& array, ValueVisitorArgs&) override { + for (auto& item : array.items) { + item->accept(*this, {}); + } + } + + void visit(Plural& plural, ValueVisitorArgs&) override { + for (auto& item : plural.values) { + if (item) { + item->accept(*this, {}); + } + } + } + +private: + void idToName(Reference& reference) { + if (!reference.id.isValid()) { + return; + } + + auto cacheIter = mCache->find(reference.id); + if (cacheIter != mCache->end()) { + reference.name = cacheIter->second; + reference.id = 0; + } else { + Maybe<ResourceName> result = mResolver->findName(reference.id); + if (result) { + reference.name = result.value(); + + // Add to cache. + mCache->insert({reference.id, reference.name}); + + reference.id = 0; + } + } + } + + std::shared_ptr<IResolver> mResolver; + std::map<ResourceId, ResourceName>* mCache; +}; + + +BinaryResourceParser::BinaryResourceParser(const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver, + const Source& source, + const void* data, + size_t len) : + mTable(table), mResolver(resolver), mSource(source), mData(data), mDataLen(len) { +} + +bool BinaryResourceParser::parse() { + ResChunkPullParser parser(mData, mDataLen); + + bool error = false; + while(ResChunkPullParser::isGoodEvent(parser.next())) { + if (parser.getChunk()->type != android::RES_TABLE_TYPE) { + Logger::warn(mSource) + << "unknown chunk of type '" + << parser.getChunk()->type + << "'." + << std::endl; + continue; + } + + error |= !parseTable(parser.getChunk()); + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad document: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + return !error; +} + +bool BinaryResourceParser::getSymbol(const void* data, ResourceNameRef* outSymbol) { + if (!mSymbolEntries || mSymbolEntryCount == 0) { + return false; + } + + if (reinterpret_cast<uintptr_t>(data) < reinterpret_cast<uintptr_t>(mData)) { + return false; + } + + // We only support 32 bit offsets right now. + const uintptr_t offset = reinterpret_cast<uintptr_t>(data) - + reinterpret_cast<uintptr_t>(mData); + if (offset > std::numeric_limits<uint32_t>::max()) { + return false; + } + + for (size_t i = 0; i < mSymbolEntryCount; i++) { + if (mSymbolEntries[i].offset == offset) { + // This offset is a symbol! + const StringPiece16 str = util::getString(mSymbolPool, + mSymbolEntries[i].stringIndex); + StringPiece16 typeStr; + ResourceParser::extractResourceName(str, &outSymbol->package, &typeStr, + &outSymbol->entry); + const ResourceType* type = parseResourceType(typeStr); + if (!type) { + return false; + } + outSymbol->type = *type; + + // Since we scan the symbol table in order, we can start looking for the + // next symbol from this point. + mSymbolEntryCount -= i + 1; + mSymbolEntries += i + 1; + return true; + } + } + return false; +} + +bool BinaryResourceParser::parseSymbolTable(const ResChunk_header* chunk) { + const SymbolTable_header* symbolTableHeader = convertTo<SymbolTable_header>(chunk); + if (!symbolTableHeader) { + Logger::error(mSource) + << "could not parse chunk as SymbolTable_header." + << std::endl; + return false; + } + + const size_t entrySizeBytes = symbolTableHeader->count * sizeof(SymbolTable_entry); + if (entrySizeBytes > getChunkDataLen(symbolTableHeader->header)) { + Logger::error(mSource) + << "entries extend beyond chunk." + << std::endl; + return false; + } + + mSymbolEntries = reinterpret_cast<const SymbolTable_entry*>( + getChunkData(symbolTableHeader->header)); + mSymbolEntryCount = symbolTableHeader->count; + + ResChunkPullParser parser(getChunkData(symbolTableHeader->header) + entrySizeBytes, + getChunkDataLen(symbolTableHeader->header) - entrySizeBytes); + if (!ResChunkPullParser::isGoodEvent(parser.next())) { + Logger::error(mSource) + << "failed to parse chunk: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + + if (parser.getChunk()->type != android::RES_STRING_POOL_TYPE) { + Logger::error(mSource) + << "expected Symbol string pool." + << std::endl; + return false; + } + + if (mSymbolPool.setTo(parser.getChunk(), parser.getChunk()->size) != NO_ERROR) { + Logger::error(mSource) + << "failed to parse symbol string pool with code: " + << mSymbolPool.getError() + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parseTable(const ResChunk_header* chunk) { + const ResTable_header* tableHeader = convertTo<ResTable_header>(chunk); + if (!tableHeader) { + Logger::error(mSource) + << "could not parse chunk as ResTable_header." + << std::endl; + return false; + } + + ResChunkPullParser parser(getChunkData(tableHeader->header), + getChunkDataLen(tableHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (parser.getChunk()->type) { + case android::RES_STRING_POOL_TYPE: + if (mValuePool.getError() == NO_INIT) { + if (mValuePool.setTo(parser.getChunk(), parser.getChunk()->size) != + NO_ERROR) { + Logger::error(mSource) + << "failed to parse value string pool with code: " + << mValuePool.getError() + << "." + << std::endl; + return false; + } + + // Reserve some space for the strings we are going to add. + mTable->getValueStringPool().hintWillAdd( + mValuePool.size(), mValuePool.styleCount()); + } else { + Logger::warn(mSource) + << "unexpected string pool." + << std::endl; + } + break; + + case RES_TABLE_SYMBOL_TABLE_TYPE: + if (!parseSymbolTable(parser.getChunk())) { + return false; + } + break; + + case RES_TABLE_SOURCE_POOL_TYPE: { + if (mSourcePool.setTo(getChunkData(*parser.getChunk()), + getChunkDataLen(*parser.getChunk())) != NO_ERROR) { + Logger::error(mSource) + << "failed to parse source pool with code: " + << mSourcePool.getError() + << "." + << std::endl; + return false; + } + break; + } + + case android::RES_TABLE_PACKAGE_TYPE: + if (!parsePackage(parser.getChunk())) { + return false; + } + break; + + default: + Logger::warn(mSource) + << "unexpected chunk of type " + << parser.getChunk()->type + << "." + << std::endl; + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad resource table: " << parser.getLastError() + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parsePackage(const ResChunk_header* chunk) { + if (mValuePool.getError() != NO_ERROR) { + Logger::error(mSource) + << "no value string pool for ResTable." + << std::endl; + return false; + } + + const ResTable_package* packageHeader = convertTo<ResTable_package>(chunk); + if (!packageHeader) { + Logger::error(mSource) + << "could not parse chunk as ResTable_header." + << std::endl; + return false; + } + + if (mTable->getPackageId() == ResourceTable::kUnsetPackageId) { + // This is the first time the table has it's package ID set. + mTable->setPackageId(packageHeader->id); + } else if (mTable->getPackageId() != packageHeader->id) { + Logger::error(mSource) + << "ResTable_package has package ID " + << std::hex << packageHeader->id << std::dec + << " but ResourceTable has package ID " + << std::hex << mTable->getPackageId() << std::dec + << std::endl; + return false; + } + + size_t len = strnlen16(reinterpret_cast<const char16_t*>(packageHeader->name), + sizeof(packageHeader->name) / sizeof(packageHeader->name[0])); + mTable->setPackage(StringPiece16(reinterpret_cast<const char16_t*>(packageHeader->name), len)); + + ResChunkPullParser parser(getChunkData(packageHeader->header), + getChunkDataLen(packageHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (parser.getChunk()->type) { + case android::RES_STRING_POOL_TYPE: + if (mTypePool.getError() == NO_INIT) { + if (mTypePool.setTo(parser.getChunk(), parser.getChunk()->size) != + NO_ERROR) { + Logger::error(mSource) + << "failed to parse type string pool with code " + << mTypePool.getError() + << "." + << std::endl; + return false; + } + } else if (mKeyPool.getError() == NO_INIT) { + if (mKeyPool.setTo(parser.getChunk(), parser.getChunk()->size) != + NO_ERROR) { + Logger::error(mSource) + << "failed to parse key string pool with code " + << mKeyPool.getError() + << "." + << std::endl; + return false; + } + } else { + Logger::warn(mSource) + << "unexpected string pool." + << std::endl; + } + break; + + case android::RES_TABLE_TYPE_SPEC_TYPE: + if (!parseTypeSpec(parser.getChunk())) { + return false; + } + break; + + case android::RES_TABLE_TYPE_TYPE: + if (!parseType(parser.getChunk())) { + return false; + } + break; + + case RES_TABLE_PUBLIC_TYPE: + if (!parsePublic(parser.getChunk())) { + return false; + } + break; + + default: + Logger::warn(mSource) + << "unexpected chunk of type " + << parser.getChunk()->type + << "." + << std::endl; + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad package: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + + // Now go through the table and change resource ID references to + // symbolic references. + + ReferenceIdToNameVisitor visitor(mResolver, &mIdIndex); + for (auto& type : *mTable) { + for (auto& entry : type->entries) { + for (auto& configValue : entry->values) { + configValue.value->accept(visitor, {}); + } + } + } + return true; +} + +bool BinaryResourceParser::parsePublic(const ResChunk_header* chunk) { + const Public_header* header = convertTo<Public_header>(chunk); + + if (header->typeId == 0) { + Logger::error(mSource) + << "invalid type ID " << header->typeId << std::endl; + return false; + } + + const ResourceType* parsedType = parseResourceType(util::getString(mTypePool, + header->typeId - 1)); + if (!parsedType) { + Logger::error(mSource) + << "invalid type " << util::getString(mTypePool, header->typeId - 1) << std::endl; + return false; + } + + const uintptr_t chunkEnd = reinterpret_cast<uintptr_t>(chunk) + chunk->size; + const Public_entry* entry = reinterpret_cast<const Public_entry*>( + getChunkData(header->header)); + for (uint32_t i = 0; i < header->count; i++) { + if (reinterpret_cast<uintptr_t>(entry) + sizeof(*entry) > chunkEnd) { + Logger::error(mSource) + << "Public_entry extends beyond chunk." + << std::endl; + return false; + } + + const ResourceId resId = { mTable->getPackageId(), header->typeId, entry->entryId }; + const ResourceName name = { + mTable->getPackage(), + *parsedType, + util::getString(mKeyPool, entry->key.index).toString() }; + + SourceLine source; + if (mSourcePool.getError() == NO_ERROR) { + source.path = util::utf16ToUtf8(util::getString(mSourcePool, entry->source.index)); + source.line = entry->sourceLine; + } + + if (!mTable->markPublicAllowMangled(name, resId, source)) { + return false; + } + + // Add this resource name->id mapping to the index so + // that we can resolve all ID references to name references. + auto cacheIter = mIdIndex.find(resId); + if (cacheIter == mIdIndex.end()) { + mIdIndex.insert({ resId, name }); + } + + entry++; + } + return true; +} + +bool BinaryResourceParser::parseTypeSpec(const ResChunk_header* chunk) { + if (mTypePool.getError() != NO_ERROR) { + Logger::error(mSource) + << "no type string pool available for ResTable_typeSpec." + << std::endl; + return false; + } + + const ResTable_typeSpec* typeSpec = convertTo<ResTable_typeSpec>(chunk); + if (!typeSpec) { + Logger::error(mSource) + << "could not parse chunk as ResTable_typeSpec." + << std::endl; + return false; + } + + if (typeSpec->id == 0) { + Logger::error(mSource) + << "ResTable_typeSpec has invalid id: " + << typeSpec->id + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parseType(const ResChunk_header* chunk) { + if (mTypePool.getError() != NO_ERROR) { + Logger::error(mSource) + << "no type string pool available for ResTable_typeSpec." + << std::endl; + return false; + } + + if (mKeyPool.getError() != NO_ERROR) { + Logger::error(mSource) + << "no key string pool available for ResTable_type." + << std::endl; + return false; + } + + const ResTable_type* type = convertTo<ResTable_type>(chunk); + if (!type) { + Logger::error(mSource) + << "could not parse chunk as ResTable_type." + << std::endl; + return false; + } + + if (type->id == 0) { + Logger::error(mSource) + << "ResTable_type has invalid id: " + << type->id + << "." + << std::endl; + return false; + } + + const ConfigDescription config(type->config); + const StringPiece16 typeName = util::getString(mTypePool, type->id - 1); + + const ResourceType* parsedType = parseResourceType(typeName); + if (!parsedType) { + Logger::error(mSource) + << "invalid type name '" + << typeName + << "' for type with ID " + << uint32_t(type->id) + << "." << std::endl; + return false; + } + + android::TypeVariant tv(type); + for (auto it = tv.beginEntries(); it != tv.endEntries(); ++it) { + if (!*it) { + continue; + } + + const ResTable_entry* entry = *it; + const ResourceName name = { + mTable->getPackage(), + *parsedType, + util::getString(mKeyPool, entry->key.index).toString() + }; + + const ResourceId resId = { mTable->getPackageId(), type->id, it.index() }; + + std::unique_ptr<Value> resourceValue; + const ResTable_entry_source* sourceBlock = nullptr; + if (entry->flags & ResTable_entry::FLAG_COMPLEX) { + const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry); + if (mapEntry->size - sizeof(*mapEntry) == sizeof(*sourceBlock)) { + const uint8_t* data = reinterpret_cast<const uint8_t*>(mapEntry); + data += mapEntry->size - sizeof(*sourceBlock); + sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); + } + + // TODO(adamlesinski): Check that the entry count is valid. + resourceValue = parseMapEntry(name, config, mapEntry); + } else { + if (entry->size - sizeof(*entry) == sizeof(*sourceBlock)) { + const uint8_t* data = reinterpret_cast<const uint8_t*>(entry); + data += entry->size - sizeof(*sourceBlock); + sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); + } + + const Res_value* value = reinterpret_cast<const Res_value*>( + reinterpret_cast<const uint8_t*>(entry) + entry->size); + resourceValue = parseValue(name, config, value, entry->flags); + } + + if (!resourceValue) { + // TODO(adamlesinski): For now this is ok, but it really shouldn't be. + continue; + } + + SourceLine source = mSource.line(0); + if (sourceBlock) { + size_t len; + const char* str = mSourcePool.string8At(sourceBlock->pathIndex, &len); + if (str) { + source.path.assign(str, len); + } + source.line = sourceBlock->line; + } + + if (!mTable->addResourceAllowMangled(name, config, source, std::move(resourceValue))) { + return false; + } + + if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0) { + if (!mTable->markPublicAllowMangled(name, resId, mSource.line(0))) { + return false; + } + } + + // Add this resource name->id mapping to the index so + // that we can resolve all ID references to name references. + auto cacheIter = mIdIndex.find(resId); + if (cacheIter == mIdIndex.end()) { + mIdIndex.insert({ resId, name }); + } + } + return true; +} + +std::unique_ptr<Item> BinaryResourceParser::parseValue(const ResourceNameRef& name, + const ConfigDescription& config, + const Res_value* value, + uint16_t flags) { + if (name.type == ResourceType::kId) { + return util::make_unique<Id>(); + } + + if (value->dataType == Res_value::TYPE_STRING) { + StringPiece16 str = util::getString(mValuePool, value->data); + + const ResStringPool_span* spans = mValuePool.styleAt(value->data); + if (spans != nullptr) { + StyleString styleStr = { str.toString() }; + while (spans->name.index != ResStringPool_span::END) { + styleStr.spans.push_back(Span{ + util::getString(mValuePool, spans->name.index).toString(), + spans->firstChar, + spans->lastChar + }); + spans++; + } + return util::make_unique<StyledString>( + mTable->getValueStringPool().makeRef( + styleStr, StringPool::Context{1, config})); + } else { + if (name.type != ResourceType::kString && + util::stringStartsWith<char16_t>(str, u"res/")) { + // This must be a FileReference. + return util::make_unique<FileReference>(mTable->getValueStringPool().makeRef( + str, StringPool::Context{ 0, config })); + } + + // There are no styles associated with this string, so treat it as + // a simple string. + return util::make_unique<String>( + mTable->getValueStringPool().makeRef( + str, StringPool::Context{1, config})); + } + } + + if (value->dataType == Res_value::TYPE_REFERENCE || + value->dataType == Res_value::TYPE_ATTRIBUTE) { + const Reference::Type type = (value->dataType == Res_value::TYPE_REFERENCE) ? + Reference::Type::kResource : Reference::Type::kAttribute; + + if (value->data != 0) { + // This is a normal reference. + return util::make_unique<Reference>(value->data, type); + } + + // This reference has an invalid ID. Check if it is an unresolved symbol. + ResourceNameRef symbol; + if (getSymbol(&value->data, &symbol)) { + return util::make_unique<Reference>(symbol, type); + } + + // This is not an unresolved symbol, so it must be the magic @null reference. + Res_value nullType = {}; + nullType.dataType = Res_value::TYPE_REFERENCE; + return util::make_unique<BinaryPrimitive>(nullType); + } + + if (value->dataType == ExtendedTypes::TYPE_RAW_STRING) { + return util::make_unique<RawString>( + mTable->getValueStringPool().makeRef(util::getString(mValuePool, value->data), + StringPool::Context{ 1, config })); + } + + // Treat this as a raw binary primitive. + return util::make_unique<BinaryPrimitive>(*value); +} + +std::unique_ptr<Value> BinaryResourceParser::parseMapEntry(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + switch (name.type) { + case ResourceType::kStyle: + return parseStyle(name, config, map); + case ResourceType::kAttr: + return parseAttr(name, config, map); + case ResourceType::kArray: + return parseArray(name, config, map); + case ResourceType::kStyleable: + return parseStyleable(name, config, map); + case ResourceType::kPlurals: + return parsePlural(name, config, map); + default: + break; + } + return {}; +} + +std::unique_ptr<Style> BinaryResourceParser::parseStyle(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + if (map->parent.ident == 0) { + // The parent is either not set or it is an unresolved symbol. + // Check to see if it is a symbol. + ResourceNameRef symbol; + if (getSymbol(&map->parent.ident, &symbol)) { + style->parent.name = symbol.toResourceName(); + } + } else { + // The parent is a regular reference to a resource. + style->parent.id = map->parent.ident; + } + + for (const ResTable_map& mapEntry : map) { + style->entries.emplace_back(); + Style::Entry& styleEntry = style->entries.back(); + + if (mapEntry.name.ident == 0) { + // The map entry's key (attribute) is not set. This must be + // a symbol reference, so resolve it. + ResourceNameRef symbol; + bool result = getSymbol(&mapEntry.name.ident, &symbol); + assert(result); + styleEntry.key.name = symbol.toResourceName(); + } else { + // The map entry's key (attribute) is a regular reference. + styleEntry.key.id = mapEntry.name.ident; + } + + // Parse the attribute's value. + styleEntry.value = parseValue(name, config, &mapEntry.value, 0); + assert(styleEntry.value); + } + return style; +} + +std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + const bool isWeak = (map->flags & ResTable_entry::FLAG_WEAK) != 0; + std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak); + + // First we must discover what type of attribute this is. Find the type mask. + auto typeMaskIter = std::find_if(begin(map), end(map), [](const ResTable_map& entry) -> bool { + return entry.name.ident == ResTable_map::ATTR_TYPE; + }); + + if (typeMaskIter != end(map)) { + attr->typeMask = typeMaskIter->value.data; + } + + if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) { + for (const ResTable_map& mapEntry : map) { + if (Res_INTERNALID(mapEntry.name.ident)) { + continue; + } + + Attribute::Symbol symbol; + symbol.value = mapEntry.value.data; + if (mapEntry.name.ident == 0) { + // The map entry's key (id) is not set. This must be + // a symbol reference, so resolve it. + ResourceNameRef symbolName; + bool result = getSymbol(&mapEntry.name.ident, &symbolName); + assert(result); + symbol.symbol.name = symbolName.toResourceName(); + } else { + // The map entry's key (id) is a regular reference. + symbol.symbol.id = mapEntry.name.ident; + } + + attr->symbols.push_back(std::move(symbol)); + } + } + + // TODO(adamlesinski): Find min, max, i80n, etc attributes. + return attr; +} + +std::unique_ptr<Array> BinaryResourceParser::parseArray(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Array> array = util::make_unique<Array>(); + for (const ResTable_map& mapEntry : map) { + array->items.push_back(parseValue(name, config, &mapEntry.value, 0)); + } + return array; +} + +std::unique_ptr<Styleable> BinaryResourceParser::parseStyleable(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + for (const ResTable_map& mapEntry : map) { + if (mapEntry.name.ident == 0) { + // The map entry's key (attribute) is not set. This must be + // a symbol reference, so resolve it. + ResourceNameRef symbol; + bool result = getSymbol(&mapEntry.name.ident, &symbol); + assert(result); + styleable->entries.emplace_back(symbol); + } else { + // The map entry's key (attribute) is a regular reference. + styleable->entries.emplace_back(mapEntry.name.ident); + } + } + return styleable; +} + +std::unique_ptr<Plural> BinaryResourceParser::parsePlural(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + for (const ResTable_map& mapEntry : map) { + std::unique_ptr<Item> item = parseValue(name, config, &mapEntry.value, 0); + + switch (mapEntry.name.ident) { + case android::ResTable_map::ATTR_ZERO: + plural->values[Plural::Zero] = std::move(item); + break; + case android::ResTable_map::ATTR_ONE: + plural->values[Plural::One] = std::move(item); + break; + case android::ResTable_map::ATTR_TWO: + plural->values[Plural::Two] = std::move(item); + break; + case android::ResTable_map::ATTR_FEW: + plural->values[Plural::Few] = std::move(item); + break; + case android::ResTable_map::ATTR_MANY: + plural->values[Plural::Many] = std::move(item); + break; + case android::ResTable_map::ATTR_OTHER: + plural->values[Plural::Other] = std::move(item); + break; + } + } + return plural; +} + +} // namespace aapt diff --git a/tools/aapt2/BinaryResourceParser.h b/tools/aapt2/BinaryResourceParser.h new file mode 100644 index 0000000..32876cd --- /dev/null +++ b/tools/aapt2/BinaryResourceParser.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_BINARY_RESOURCE_PARSER_H +#define AAPT_BINARY_RESOURCE_PARSER_H + +#include "Resolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Source.h" + +#include <androidfw/ResourceTypes.h> +#include <string> + +namespace aapt { + +struct SymbolTable_entry; + +/* + * Parses a binary resource table (resources.arsc) and adds the entries + * to a ResourceTable. This is different than the libandroidfw ResTable + * in that it scans the table from top to bottom and doesn't require + * support for random access. It is also able to parse non-runtime + * chunks and types. + */ +class BinaryResourceParser { +public: + /* + * Creates a parser, which will read `len` bytes from `data`, and + * add any resources parsed to `table`. `source` is for logging purposes. + */ + BinaryResourceParser(const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver, + const Source& source, + const void* data, size_t len); + + BinaryResourceParser(const BinaryResourceParser&) = delete; // No copy. + + /* + * Parses the binary resource table and returns true if successful. + */ + bool parse(); + +private: + // Helper method to retrieve the symbol name for a given table offset specified + // as a pointer. + bool getSymbol(const void* data, ResourceNameRef* outSymbol); + + bool parseTable(const android::ResChunk_header* chunk); + bool parseSymbolTable(const android::ResChunk_header* chunk); + + // Looks up the resource ID in the reference and converts it to a name if available. + bool idToName(Reference* reference); + + bool parsePackage(const android::ResChunk_header* chunk); + bool parsePublic(const android::ResChunk_header* chunk); + bool parseTypeSpec(const android::ResChunk_header* chunk); + bool parseType(const android::ResChunk_header* chunk); + + std::unique_ptr<Item> parseValue(const ResourceNameRef& name, + const ConfigDescription& config, const android::Res_value* value, uint16_t flags); + + std::unique_ptr<Value> parseMapEntry(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Style> parseStyle(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Attribute> parseAttr(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Array> parseArray(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Plural> parsePlural(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Styleable> parseStyleable(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::shared_ptr<ResourceTable> mTable; + + std::shared_ptr<IResolver> mResolver; + + const Source mSource; + + const void* mData; + const size_t mDataLen; + + // The package name of the resource table. + std::u16string mPackage; + + // The array of symbol entries. Each element points to an offset + // in the table and an index into the symbol table string pool. + const SymbolTable_entry* mSymbolEntries = nullptr; + + // Number of symbol entries. + size_t mSymbolEntryCount = 0; + + // The symbol table string pool. Holds the names of symbols + // referenced in this table but not defined nor resolved to an + // ID. + android::ResStringPool mSymbolPool; + + // The source string pool. Resource entries may have an extra + // field that points into this string pool, which denotes where + // the resource was parsed from originally. + android::ResStringPool mSourcePool; + + // The standard value string pool for resource values. + android::ResStringPool mValuePool; + + // The string pool that holds the names of the types defined + // in this table. + android::ResStringPool mTypePool; + + // The string pool that holds the names of the entries defined + // in this table. + android::ResStringPool mKeyPool; + + // A mapping of resource ID to resource name. When we finish parsing + // we use this to convert all resource IDs to symbolic references. + std::map<ResourceId, ResourceName> mIdIndex; +}; + +} // namespace aapt + +namespace android { + +/** + * Iterator functionality for ResTable_map_entry. + */ + +inline const ResTable_map* begin(const ResTable_map_entry* map) { + return reinterpret_cast<const ResTable_map*>( + reinterpret_cast<const uint8_t*>(map) + map->size); +} + +inline const ResTable_map* end(const ResTable_map_entry* map) { + return reinterpret_cast<const ResTable_map*>( + reinterpret_cast<const uint8_t*>(map) + map->size) + map->count; +} + +} // namespace android + +#endif // AAPT_BINARY_RESOURCE_PARSER_H diff --git a/tools/aapt2/BindingXmlPullParser.cpp b/tools/aapt2/BindingXmlPullParser.cpp new file mode 100644 index 0000000..4b7a656 --- /dev/null +++ b/tools/aapt2/BindingXmlPullParser.cpp @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BindingXmlPullParser.h" +#include "Util.h" + +#include <iostream> +#include <sstream> +#include <string> +#include <vector> + +namespace aapt { + +constexpr const char16_t* kBindingNamespaceUri = u"http://schemas.android.com/apk/binding"; +constexpr const char16_t* kAndroidNamespaceUri = u"http://schemas.android.com/apk/res/android"; +constexpr const char16_t* kVariableTagName = u"variable"; +constexpr const char* kBindingTagPrefix = "android:binding_"; + +BindingXmlPullParser::BindingXmlPullParser(const std::shared_ptr<XmlPullParser>& parser) : + mParser(parser), mOverride(false), mNextTagId(0) { +} + +bool BindingXmlPullParser::readVariableDeclaration() { + VarDecl var; + + const auto endAttrIter = mParser->endAttributes(); + for (auto attrIter = mParser->beginAttributes(); attrIter != endAttrIter; ++attrIter) { + if (!attrIter->namespaceUri.empty()) { + continue; + } + + if (attrIter->name == u"name") { + var.name = util::utf16ToUtf8(attrIter->value); + } else if (attrIter->name == u"type") { + var.type = util::utf16ToUtf8(attrIter->value); + } + } + + XmlPullParser::skipCurrentElement(mParser.get()); + + if (var.name.empty()) { + mLastError = "variable declaration missing name"; + return false; + } + + if (var.type.empty()) { + mLastError = "variable declaration missing type"; + return false; + } + + mVarDecls.push_back(std::move(var)); + return true; +} + +bool BindingXmlPullParser::readExpressions() { + mOverride = true; + std::vector<XmlPullParser::Attribute> expressions; + std::string idValue; + + const auto endAttrIter = mParser->endAttributes(); + for (auto attr = mParser->beginAttributes(); attr != endAttrIter; ++attr) { + if (attr->namespaceUri == kAndroidNamespaceUri && attr->name == u"id") { + idValue = util::utf16ToUtf8(attr->value); + } else { + StringPiece16 value = util::trimWhitespace(attr->value); + if (util::stringStartsWith<char16_t>(value, u"@{") && + util::stringEndsWith<char16_t>(value, u"}")) { + // This is attribute's value is an expression of the form + // @{expression}. We need to capture the expression inside. + expressions.push_back(XmlPullParser::Attribute{ + attr->namespaceUri, + attr->name, + value.substr(2, value.size() - 3).toString() + }); + } else { + // This is a normal attribute, use as is. + mAttributes.emplace_back(*attr); + } + } + } + + // Check if we have any expressions. + if (!expressions.empty()) { + // We have expressions, so let's assign the target a tag number + // and add it to our targets list. + int32_t targetId = mNextTagId++; + mTargets.push_back(Target{ + util::utf16ToUtf8(mParser->getElementName()), + idValue, + targetId, + std::move(expressions) + }); + + std::stringstream numGen; + numGen << kBindingTagPrefix << targetId; + mAttributes.push_back(XmlPullParser::Attribute{ + std::u16string(kAndroidNamespaceUri), + std::u16string(u"tag"), + util::utf8ToUtf16(numGen.str()) + }); + } + return true; +} + +XmlPullParser::Event BindingXmlPullParser::next() { + // Clear old state in preparation for the next event. + mOverride = false; + mAttributes.clear(); + + while (true) { + Event event = mParser->next(); + if (event == Event::kStartElement) { + if (mParser->getElementNamespace().empty() && + mParser->getElementName() == kVariableTagName) { + // This is a variable tag. Record data from it, and + // then discard the entire element. + if (!readVariableDeclaration()) { + // mLastError is set, so getEvent will return kBadDocument. + return getEvent(); + } + continue; + } else { + // Check for expressions of the form @{} in attribute text. + const auto endAttrIter = mParser->endAttributes(); + for (auto attr = mParser->beginAttributes(); attr != endAttrIter; ++attr) { + StringPiece16 value = util::trimWhitespace(attr->value); + if (util::stringStartsWith<char16_t>(value, u"@{") && + util::stringEndsWith<char16_t>(value, u"}")) { + if (!readExpressions()) { + return getEvent(); + } + break; + } + } + } + } else if (event == Event::kStartNamespace || event == Event::kEndNamespace) { + if (mParser->getNamespaceUri() == kBindingNamespaceUri) { + // Skip binding namespace tags. + continue; + } + } + return event; + } + return Event::kBadDocument; +} + +bool BindingXmlPullParser::writeToFile(std::ostream& out) const { + out << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + out << "<Layout directory=\"\" layout=\"\" layoutId=\"\">\n"; + + // Write the variables. + out << " <Variables>\n"; + for (const VarDecl& v : mVarDecls) { + out << " <entries name=\"" << v.name << "\" type=\"" << v.type << "\"/>\n"; + } + out << " </Variables>\n"; + + // Write the imports. + + std::stringstream tagGen; + + // Write the targets. + out << " <Targets>\n"; + for (const Target& t : mTargets) { + tagGen.str({}); + tagGen << kBindingTagPrefix << t.tagId; + out << " <Target boundClass=\"" << t.className << "\" id=\"" << t.id + << "\" tag=\"" << tagGen.str() << "\">\n"; + out << " <Expressions>\n"; + for (const XmlPullParser::Attribute& a : t.expressions) { + out << " <Expression attribute=\"" << a.namespaceUri << ":" << a.name + << "\" text=\"" << a.value << "\"/>\n"; + } + out << " </Expressions>\n"; + out << " </Target>\n"; + } + out << " </Targets>\n"; + + out << "</Layout>\n"; + return bool(out); +} + +XmlPullParser::const_iterator BindingXmlPullParser::beginAttributes() const { + if (mOverride) { + return mAttributes.begin(); + } + return mParser->beginAttributes(); +} + +XmlPullParser::const_iterator BindingXmlPullParser::endAttributes() const { + if (mOverride) { + return mAttributes.end(); + } + return mParser->endAttributes(); +} + +size_t BindingXmlPullParser::getAttributeCount() const { + if (mOverride) { + return mAttributes.size(); + } + return mParser->getAttributeCount(); +} + +XmlPullParser::Event BindingXmlPullParser::getEvent() const { + if (!mLastError.empty()) { + return Event::kBadDocument; + } + return mParser->getEvent(); +} + +const std::string& BindingXmlPullParser::getLastError() const { + if (!mLastError.empty()) { + return mLastError; + } + return mParser->getLastError(); +} + +const std::u16string& BindingXmlPullParser::getComment() const { + return mParser->getComment(); +} + +size_t BindingXmlPullParser::getLineNumber() const { + return mParser->getLineNumber(); +} + +size_t BindingXmlPullParser::getDepth() const { + return mParser->getDepth(); +} + +const std::u16string& BindingXmlPullParser::getText() const { + return mParser->getText(); +} + +const std::u16string& BindingXmlPullParser::getNamespacePrefix() const { + return mParser->getNamespacePrefix(); +} + +const std::u16string& BindingXmlPullParser::getNamespaceUri() const { + return mParser->getNamespaceUri(); +} + +bool BindingXmlPullParser::applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const { + return mParser->applyPackageAlias(package, defaultPackage); +} + +const std::u16string& BindingXmlPullParser::getElementNamespace() const { + return mParser->getElementNamespace(); +} + +const std::u16string& BindingXmlPullParser::getElementName() const { + return mParser->getElementName(); +} + +} // namespace aapt diff --git a/tools/aapt2/BindingXmlPullParser.h b/tools/aapt2/BindingXmlPullParser.h new file mode 100644 index 0000000..cfb16ef --- /dev/null +++ b/tools/aapt2/BindingXmlPullParser.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_BINDING_XML_PULL_PARSER_H +#define AAPT_BINDING_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <iostream> +#include <memory> +#include <string> + +namespace aapt { + +class BindingXmlPullParser : public XmlPullParser { +public: + BindingXmlPullParser(const std::shared_ptr<XmlPullParser>& parser); + BindingXmlPullParser(const BindingXmlPullParser& rhs) = delete; + + Event getEvent() const override; + const std::string& getLastError() const override; + Event next() override; + + const std::u16string& getComment() const override; + size_t getLineNumber() const override; + size_t getDepth() const override; + + const std::u16string& getText() const override; + + const std::u16string& getNamespacePrefix() const override; + const std::u16string& getNamespaceUri() const override; + bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) + const override; + + const std::u16string& getElementNamespace() const override; + const std::u16string& getElementName() const override; + + const_iterator beginAttributes() const override; + const_iterator endAttributes() const override; + size_t getAttributeCount() const override; + + bool writeToFile(std::ostream& out) const; + +private: + struct VarDecl { + std::string name; + std::string type; + }; + + struct Import { + std::string name; + std::string type; + }; + + struct Target { + std::string className; + std::string id; + int32_t tagId; + + std::vector<XmlPullParser::Attribute> expressions; + }; + + bool readVariableDeclaration(); + bool readExpressions(); + + std::shared_ptr<XmlPullParser> mParser; + std::string mLastError; + bool mOverride; + std::vector<XmlPullParser::Attribute> mAttributes; + std::vector<VarDecl> mVarDecls; + std::vector<Target> mTargets; + int32_t mNextTagId; +}; + +} // namespace aapt + +#endif // AAPT_BINDING_XML_PULL_PARSER_H diff --git a/tools/aapt2/BindingXmlPullParser_test.cpp b/tools/aapt2/BindingXmlPullParser_test.cpp new file mode 100644 index 0000000..28edcb6 --- /dev/null +++ b/tools/aapt2/BindingXmlPullParser_test.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "SourceXmlPullParser.h" +#include "BindingXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char16_t* kAndroidNamespaceUri = u"http://schemas.android.com/apk/res/android"; + +TEST(BindingXmlPullParserTest, SubstituteBindingExpressionsWithTag) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" + << " android:id=\"@+id/content\">\n" + << " <variable name=\"user\" type=\"com.android.test.User\"/>\n" + << " <TextView android:text=\"@{user.name}\" android:layout_width=\"wrap_content\"\n" + << " android:layout_height=\"wrap_content\"/>\n" + << "</LinearLayout>\n"; + std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); + BindingXmlPullParser parser(sourceParser); + + ASSERT_EQ(XmlPullParser::Event::kStartNamespace, parser.next()); + EXPECT_EQ(std::u16string(u"http://schemas.android.com/apk/res/android"), + parser.getNamespaceUri()); + + ASSERT_EQ(XmlPullParser::Event::kStartElement, parser.next()); + EXPECT_EQ(std::u16string(u"LinearLayout"), parser.getElementName()); + + while (parser.next() == XmlPullParser::Event::kText) {} + + ASSERT_EQ(XmlPullParser::Event::kStartElement, parser.getEvent()); + EXPECT_EQ(std::u16string(u"TextView"), parser.getElementName()); + + ASSERT_EQ(3u, parser.getAttributeCount()); + const auto endAttr = parser.endAttributes(); + EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"layout_width")); + EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"layout_height")); + EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"tag")); + + while (parser.next() == XmlPullParser::Event::kText) {} + + ASSERT_EQ(XmlPullParser::Event::kEndElement, parser.getEvent()); + + while (parser.next() == XmlPullParser::Event::kText) {} + + ASSERT_EQ(XmlPullParser::Event::kEndElement, parser.getEvent()); + ASSERT_EQ(XmlPullParser::Event::kEndNamespace, parser.next()); +} + +TEST(BindingXmlPullParserTest, GenerateVariableDeclarations) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" + << " android:id=\"@+id/content\">\n" + << " <variable name=\"user\" type=\"com.android.test.User\"/>\n" + << "</LinearLayout>\n"; + std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); + BindingXmlPullParser parser(sourceParser); + + while (XmlPullParser::isGoodEvent(parser.next())) { + ASSERT_NE(XmlPullParser::Event::kBadDocument, parser.getEvent()); + } + + std::stringstream output; + ASSERT_TRUE(parser.writeToFile(output)); + + std::string result = output.str(); + EXPECT_NE(std::string::npos, + result.find("<entries name=\"user\" type=\"com.android.test.User\"/>")); +} + +TEST(BindingXmlPullParserTest, FailOnMissingNameOrTypeInVariableDeclaration) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" + << " android:id=\"@+id/content\">\n" + << " <variable name=\"user\"/>\n" + << "</LinearLayout>\n"; + std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); + BindingXmlPullParser parser(sourceParser); + + while (XmlPullParser::isGoodEvent(parser.next())) {} + + EXPECT_EQ(XmlPullParser::Event::kBadDocument, parser.getEvent()); + EXPECT_FALSE(parser.getLastError().empty()); +} + + +} // namespace aapt diff --git a/tools/aapt2/Compat_test.cpp b/tools/aapt2/Compat_test.cpp new file mode 100644 index 0000000..96aee44 --- /dev/null +++ b/tools/aapt2/Compat_test.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(CompatTest, VersionAttributesInStyle) { +} + +TEST(CompatTest, VersionAttributesInXML) { +} + +TEST(CompatTest, DoNotOverrideExistingVersionedFiles) { +} + +TEST(CompatTest, VersionAttributesInStyleWithCorrectPrecedence) { +} + +} // namespace aapt diff --git a/tools/aapt2/ConfigDescription.cpp b/tools/aapt2/ConfigDescription.cpp new file mode 100644 index 0000000..6ddf94a --- /dev/null +++ b/tools/aapt2/ConfigDescription.cpp @@ -0,0 +1,752 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ConfigDescription.h" +#include "Locale.h" +#include "SdkConstants.h" +#include "StringPiece.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <string> +#include <vector> + +namespace aapt { + +using android::ResTable_config; + +static const char* kWildcardName = "any"; + +static bool parseMcc(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val != 3) return false; + + int d = atoi(val); + if (d != 0) { + if (out) out->mcc = d; + return true; + } + + return false; +} + +static bool parseMnc(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'n') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val == 0 || c-val > 3) return false; + + if (out) { + out->mnc = atoi(val); + if (out->mnc == 0) { + out->mnc = ACONFIGURATION_MNC_ZERO; + } + } + + return true; +} + +static bool parseLayoutDirection(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_ANY; + return true; + } else if (strcmp(name, "ldltr") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_LTR; + return true; + } else if (strcmp(name, "ldrtl") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_RTL; + return true; + } + + return false; +} + +static bool parseScreenLayoutSize(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_ANY; + return true; + } else if (strcmp(name, "small") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_SMALL; + return true; + } else if (strcmp(name, "normal") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_NORMAL; + return true; + } else if (strcmp(name, "large") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_LARGE; + return true; + } else if (strcmp(name, "xlarge") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_XLARGE; + return true; + } + + return false; +} + +static bool parseScreenLayoutLong(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_ANY; + return true; + } else if (strcmp(name, "long") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_YES; + return true; + } else if (strcmp(name, "notlong") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_NO; + return true; + } + + return false; +} + +static bool parseOrientation(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->orientation = out->ORIENTATION_ANY; + return true; + } else if (strcmp(name, "port") == 0) { + if (out) out->orientation = out->ORIENTATION_PORT; + return true; + } else if (strcmp(name, "land") == 0) { + if (out) out->orientation = out->ORIENTATION_LAND; + return true; + } else if (strcmp(name, "square") == 0) { + if (out) out->orientation = out->ORIENTATION_SQUARE; + return true; + } + + return false; +} + +static bool parseUiModeType(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_ANY; + return true; + } else if (strcmp(name, "desk") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_DESK; + return true; + } else if (strcmp(name, "car") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_CAR; + return true; + } else if (strcmp(name, "television") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_TELEVISION; + return true; + } else if (strcmp(name, "appliance") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_APPLIANCE; + return true; + } else if (strcmp(name, "watch") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_WATCH; + return true; + } + + return false; +} + +static bool parseUiModeNight(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_ANY; + return true; + } else if (strcmp(name, "night") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_YES; + return true; + } else if (strcmp(name, "notnight") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_NO; + return true; + } + + return false; +} + +static bool parseDensity(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->density = ResTable_config::DENSITY_DEFAULT; + return true; + } + + if (strcmp(name, "anydpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_ANY; + return true; + } + + if (strcmp(name, "nodpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_NONE; + return true; + } + + if (strcmp(name, "ldpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_LOW; + return true; + } + + if (strcmp(name, "mdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_MEDIUM; + return true; + } + + if (strcmp(name, "tvdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_TV; + return true; + } + + if (strcmp(name, "hdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_HIGH; + return true; + } + + if (strcmp(name, "xhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XHIGH; + return true; + } + + if (strcmp(name, "xxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXHIGH; + return true; + } + + if (strcmp(name, "xxxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXXHIGH; + return true; + } + + char* c = (char*)name; + while (*c >= '0' && *c <= '9') { + c++; + } + + // check that we have 'dpi' after the last digit. + if (toupper(c[0]) != 'D' || + toupper(c[1]) != 'P' || + toupper(c[2]) != 'I' || + c[3] != 0) { + return false; + } + + // temporarily replace the first letter with \0 to + // use atoi. + char tmp = c[0]; + c[0] = '\0'; + + int d = atoi(name); + c[0] = tmp; + + if (d != 0) { + if (out) out->density = d; + return true; + } + + return false; +} + +static bool parseTouchscreen(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_ANY; + return true; + } else if (strcmp(name, "notouch") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_NOTOUCH; + return true; + } else if (strcmp(name, "stylus") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_STYLUS; + return true; + } else if (strcmp(name, "finger") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_FINGER; + return true; + } + + return false; +} + +static bool parseKeysHidden(const char* name, ResTable_config* out) { + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_ANY; + } else if (strcmp(name, "keysexposed") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_NO; + } else if (strcmp(name, "keyshidden") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_YES; + } else if (strcmp(name, "keyssoft") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_SOFT; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + return true; + } + + return false; +} + +static bool parseKeyboard(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->keyboard = out->KEYBOARD_ANY; + return true; + } else if (strcmp(name, "nokeys") == 0) { + if (out) out->keyboard = out->KEYBOARD_NOKEYS; + return true; + } else if (strcmp(name, "qwerty") == 0) { + if (out) out->keyboard = out->KEYBOARD_QWERTY; + return true; + } else if (strcmp(name, "12key") == 0) { + if (out) out->keyboard = out->KEYBOARD_12KEY; + return true; + } + + return false; +} + +static bool parseNavHidden(const char* name, ResTable_config* out) { + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_ANY; + } else if (strcmp(name, "navexposed") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_NO; + } else if (strcmp(name, "navhidden") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_YES; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + return true; + } + + return false; +} + +static bool parseNavigation(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->navigation = out->NAVIGATION_ANY; + return true; + } else if (strcmp(name, "nonav") == 0) { + if (out) out->navigation = out->NAVIGATION_NONAV; + return true; + } else if (strcmp(name, "dpad") == 0) { + if (out) out->navigation = out->NAVIGATION_DPAD; + return true; + } else if (strcmp(name, "trackball") == 0) { + if (out) out->navigation = out->NAVIGATION_TRACKBALL; + return true; + } else if (strcmp(name, "wheel") == 0) { + if (out) out->navigation = out->NAVIGATION_WHEEL; + return true; + } + + return false; +} + +static bool parseScreenSize(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidth = out->SCREENWIDTH_ANY; + out->screenHeight = out->SCREENHEIGHT_ANY; + } + return true; + } + + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || *x != 'x') return false; + std::string xName(name, x-name); + x++; + + const char* y = x; + while (*y >= '0' && *y <= '9') y++; + if (y == name || *y != 0) return false; + std::string yName(x, y-x); + + uint16_t w = (uint16_t)atoi(xName.c_str()); + uint16_t h = (uint16_t)atoi(yName.c_str()); + if (w < h) { + return false; + } + + if (out) { + out->screenWidth = w; + out->screenHeight = h; + } + + return true; +} + +static bool parseSmallestScreenWidthDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->smallestScreenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 's') return false; + name++; + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->smallestScreenWidthDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseScreenWidthDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->screenWidthDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseScreenHeightDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenHeightDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'h') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->screenHeightDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseVersion(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->sdkVersion = out->SDKVERSION_ANY; + out->minorVersion = out->MINORVERSION_ANY; + } + return true; + } + + if (*name != 'v') { + return false; + } + + name++; + const char* s = name; + while (*s >= '0' && *s <= '9') s++; + if (s == name || *s != 0) return false; + std::string sdkName(name, s-name); + + if (out) { + out->sdkVersion = (uint16_t)atoi(sdkName.c_str()); + out->minorVersion = 0; + } + + return true; +} + +bool ConfigDescription::parse(const StringPiece& str, ConfigDescription* out) { + std::vector<std::string> parts = util::splitAndLowercase(str, '-'); + + ConfigDescription config; + ssize_t partsConsumed = 0; + LocaleValue locale; + + const auto partsEnd = parts.end(); + auto partIter = parts.begin(); + + if (str.size() == 0) { + goto success; + } + + if (parseMcc(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseMnc(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + // Locale spans a few '-' separators, so we let it + // control the index. + partsConsumed = locale.initFromParts(partIter, partsEnd); + if (partsConsumed < 0) { + return false; + } else { + locale.writeTo(&config); + partIter += partsConsumed; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseLayoutDirection(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseSmallestScreenWidthDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenWidthDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenHeightDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenLayoutSize(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenLayoutLong(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseOrientation(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseUiModeType(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseUiModeNight(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseDensity(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseTouchscreen(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseKeysHidden(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseKeyboard(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseNavHidden(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseNavigation(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenSize(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseVersion(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + // Unrecognized. + return false; + +success: + if (out != NULL) { + applyVersionForCompatibility(&config); + *out = config; + } + return true; +} + +void ConfigDescription::applyVersionForCompatibility(ConfigDescription* config) { + uint16_t minSdk = 0; + if (config->density == ResTable_config::DENSITY_ANY) { + minSdk = SDK_LOLLIPOP; + } else if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY + || config->screenWidthDp != ResTable_config::SCREENWIDTH_ANY + || config->screenHeightDp != ResTable_config::SCREENHEIGHT_ANY) { + minSdk = SDK_HONEYCOMB_MR2; + } else if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE) + != ResTable_config::UI_MODE_TYPE_ANY + || (config->uiMode & ResTable_config::MASK_UI_MODE_NIGHT) + != ResTable_config::UI_MODE_NIGHT_ANY) { + minSdk = SDK_FROYO; + } else if ((config->screenLayout & ResTable_config::MASK_SCREENSIZE) + != ResTable_config::SCREENSIZE_ANY + || (config->screenLayout & ResTable_config::MASK_SCREENLONG) + != ResTable_config::SCREENLONG_ANY + || config->density != ResTable_config::DENSITY_DEFAULT) { + minSdk = SDK_DONUT; + } + + if (minSdk > config->sdkVersion) { + config->sdkVersion = minSdk; + } +} + +} // namespace aapt diff --git a/tools/aapt2/ConfigDescription.h b/tools/aapt2/ConfigDescription.h new file mode 100644 index 0000000..67b4b75 --- /dev/null +++ b/tools/aapt2/ConfigDescription.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_CONFIG_DESCRIPTION_H +#define AAPT_CONFIG_DESCRIPTION_H + +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> +#include <ostream> + +namespace aapt { + +/* + * Subclass of ResTable_config that adds convenient + * initialization and comparison methods. + */ +struct ConfigDescription : public android::ResTable_config { + /* + * Parse a string of the form 'fr-sw600dp-land' and fill in the + * given ResTable_config with resulting configuration parameters. + * + * The resulting configuration has the appropriate sdkVersion defined + * for backwards compatibility. + */ + static bool parse(const StringPiece& str, ConfigDescription* out = nullptr); + + /** + * If the configuration uses an axis that was added after + * the original Android release, make sure the SDK version + * is set accordingly. + */ + static void applyVersionForCompatibility(ConfigDescription* config); + + ConfigDescription(); + ConfigDescription(const android::ResTable_config& o); + ConfigDescription(const ConfigDescription& o); + ConfigDescription(ConfigDescription&& o); + + ConfigDescription& operator=(const android::ResTable_config& o); + ConfigDescription& operator=(const ConfigDescription& o); + ConfigDescription& operator=(ConfigDescription&& o); + + bool operator<(const ConfigDescription& o) const; + bool operator<=(const ConfigDescription& o) const; + bool operator==(const ConfigDescription& o) const; + bool operator!=(const ConfigDescription& o) const; + bool operator>=(const ConfigDescription& o) const; + bool operator>(const ConfigDescription& o) const; +}; + +inline ConfigDescription::ConfigDescription() { + memset(this, 0, sizeof(*this)); + size = sizeof(android::ResTable_config); +} + +inline ConfigDescription::ConfigDescription(const android::ResTable_config& o) { + *static_cast<android::ResTable_config*>(this) = o; + size = sizeof(android::ResTable_config); +} + +inline ConfigDescription::ConfigDescription(const ConfigDescription& o) { + *static_cast<android::ResTable_config*>(this) = o; +} + +inline ConfigDescription::ConfigDescription(ConfigDescription&& o) { + *this = o; +} + +inline ConfigDescription& ConfigDescription::operator=(const android::ResTable_config& o) { + *static_cast<android::ResTable_config*>(this) = o; + size = sizeof(android::ResTable_config); + return *this; +} + +inline ConfigDescription& ConfigDescription::operator=(const ConfigDescription& o) { + *static_cast<android::ResTable_config*>(this) = o; + return *this; +} + +inline ConfigDescription& ConfigDescription::operator=(ConfigDescription&& o) { + *this = o; + return *this; +} + +inline bool ConfigDescription::operator<(const ConfigDescription& o) const { + return compare(o) < 0; +} + +inline bool ConfigDescription::operator<=(const ConfigDescription& o) const { + return compare(o) <= 0; +} + +inline bool ConfigDescription::operator==(const ConfigDescription& o) const { + return compare(o) == 0; +} + +inline bool ConfigDescription::operator!=(const ConfigDescription& o) const { + return compare(o) != 0; +} + +inline bool ConfigDescription::operator>=(const ConfigDescription& o) const { + return compare(o) >= 0; +} + +inline bool ConfigDescription::operator>(const ConfigDescription& o) const { + return compare(o) > 0; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const ConfigDescription& o) { + return out << o.toString().string(); +} + +} // namespace aapt + +#endif // AAPT_CONFIG_DESCRIPTION_H diff --git a/tools/aapt2/ConfigDescription_test.cpp b/tools/aapt2/ConfigDescription_test.cpp new file mode 100644 index 0000000..c57e351 --- /dev/null +++ b/tools/aapt2/ConfigDescription_test.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ConfigDescription.h" +#include "StringPiece.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +static ::testing::AssertionResult TestParse(const StringPiece& input, + ConfigDescription* config = nullptr) { + if (ConfigDescription::parse(input, config)) { + return ::testing::AssertionSuccess() << input << " was successfully parsed"; + } + return ::testing::AssertionFailure() << input << " could not be parsed"; +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersAreOutOfOrder) { + EXPECT_FALSE(TestParse("en-sw600dp-ldrtl")); + EXPECT_FALSE(TestParse("land-en")); + EXPECT_FALSE(TestParse("hdpi-320dpi")); +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersAreNotMatched) { + EXPECT_FALSE(TestParse("en-sw600dp-ILLEGAL")); +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersHaveTrailingDash) { + EXPECT_FALSE(TestParse("en-sw600dp-land-")); +} + +TEST(ConfigDescriptionTest, ParseBasicQualifiers) { + ConfigDescription config; + EXPECT_TRUE(TestParse("", &config)); + EXPECT_EQ(std::string(""), config.toString().string()); + + EXPECT_TRUE(TestParse("fr-land", &config)); + EXPECT_EQ(std::string("fr-land"), config.toString().string()); + + EXPECT_TRUE(TestParse("mcc310-pl-sw720dp-normal-long-port-night-" + "xhdpi-keyssoft-qwerty-navexposed-nonav", &config)); + EXPECT_EQ(std::string("mcc310-pl-sw720dp-normal-long-port-night-" + "xhdpi-keyssoft-qwerty-navexposed-nonav-v13"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseLocales) { + ConfigDescription config; + EXPECT_TRUE(TestParse("en-rUS", &config)); + EXPECT_EQ(std::string("en-rUS"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseQualifierAddedInApi13) { + ConfigDescription config; + EXPECT_TRUE(TestParse("sw600dp", &config)); + EXPECT_EQ(std::string("sw600dp-v13"), config.toString().string()); + + EXPECT_TRUE(TestParse("sw600dp-v8", &config)); + EXPECT_EQ(std::string("sw600dp-v13"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseCarAttribute) { + ConfigDescription config; + EXPECT_TRUE(TestParse("car", &config)); + EXPECT_EQ(android::ResTable_config::UI_MODE_TYPE_CAR, config.uiMode); +} + +} // namespace aapt diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp new file mode 100644 index 0000000..cf222c6 --- /dev/null +++ b/tools/aapt2/Debug.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Debug.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <algorithm> +#include <iostream> +#include <map> +#include <memory> +#include <queue> +#include <set> +#include <vector> + +namespace aapt { + +struct PrintVisitor : ConstValueVisitor { + void visit(const Attribute& attr, ValueVisitorArgs&) override { + std::cout << "(attr) type="; + attr.printMask(std::cout); + static constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_FLAGS; + if (attr.typeMask & kMask) { + for (const auto& symbol : attr.symbols) { + std::cout << "\n " + << symbol.symbol.name.entry << " (" << symbol.symbol.id << ") = " + << symbol.value; + } + } + } + + void visit(const Style& style, ValueVisitorArgs&) override { + std::cout << "(style)"; + if (style.parent.name.isValid() || style.parent.id.isValid()) { + std::cout << " parent="; + if (style.parent.name.isValid()) { + std::cout << style.parent.name << " "; + } + + if (style.parent.id.isValid()) { + std::cout << style.parent.id; + } + } + + for (const auto& entry : style.entries) { + std::cout << "\n "; + if (entry.key.name.isValid()) { + std::cout << entry.key.name.package << ":" << entry.key.name.entry; + } + + if (entry.key.id.isValid()) { + std::cout << "(" << entry.key.id << ")"; + } + + std::cout << "=" << *entry.value; + } + } + + void visit(const Array& array, ValueVisitorArgs&) override { + array.print(std::cout); + } + + void visit(const Plural& plural, ValueVisitorArgs&) override { + plural.print(std::cout); + } + + void visit(const Styleable& styleable, ValueVisitorArgs&) override { + styleable.print(std::cout); + } + + void visitItem(const Item& item, ValueVisitorArgs& args) override { + item.print(std::cout); + } +}; + +void Debug::printTable(const std::shared_ptr<ResourceTable>& table) { + std::cout << "Package name=" << table->getPackage(); + if (table->getPackageId() != ResourceTable::kUnsetPackageId) { + std::cout << " id=" << std::hex << table->getPackageId() << std::dec; + } + std::cout << std::endl; + + for (const auto& type : *table) { + std::cout << " type " << type->type; + if (type->typeId != ResourceTableType::kUnsetTypeId) { + std::cout << " id=" << std::hex << type->typeId << std::dec; + } + std::cout << " entryCount=" << type->entries.size() << std::endl; + + std::vector<const ResourceEntry*> sortedEntries; + for (const auto& entry : type->entries) { + auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), entry.get(), + [](const ResourceEntry* a, const ResourceEntry* b) -> bool { + return a->entryId < b->entryId; + }); + sortedEntries.insert(iter, entry.get()); + } + + for (const ResourceEntry* entry : sortedEntries) { + ResourceId id = { table->getPackageId(), type->typeId, entry->entryId }; + ResourceName name = { table->getPackage(), type->type, entry->name }; + std::cout << " spec resource " << id << " " << name; + if (entry->publicStatus.isPublic) { + std::cout << " PUBLIC"; + } + std::cout << std::endl; + + PrintVisitor visitor; + for (const auto& value : entry->values) { + std::cout << " (" << value.config << ") "; + value.value->accept(visitor, {}); + std::cout << std::endl; + } + } + } +} + +static size_t getNodeIndex(const std::vector<ResourceName>& names, const ResourceName& name) { + auto iter = std::lower_bound(names.begin(), names.end(), name); + assert(iter != names.end() && *iter == name); + return std::distance(names.begin(), iter); +} + +void Debug::printStyleGraph(const std::shared_ptr<ResourceTable>& table, + const ResourceName& targetStyle) { + std::map<ResourceName, std::set<ResourceName>> graph; + + std::queue<ResourceName> stylesToVisit; + stylesToVisit.push(targetStyle); + for (; !stylesToVisit.empty(); stylesToVisit.pop()) { + const ResourceName& styleName = stylesToVisit.front(); + std::set<ResourceName>& parents = graph[styleName]; + if (!parents.empty()) { + // We've already visited this style. + continue; + } + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table->findResource(styleName); + if (entry) { + for (const auto& value : entry->values) { + visitFunc<Style>(*value.value, [&](const Style& style) { + if (style.parent.name.isValid()) { + parents.insert(style.parent.name); + stylesToVisit.push(style.parent.name); + } + }); + } + } + } + + std::vector<ResourceName> names; + for (const auto& entry : graph) { + names.push_back(entry.first); + } + + std::cout << "digraph styles {\n"; + for (const auto& name : names) { + std::cout << " node_" << getNodeIndex(names, name) + << " [label=\"" << name << "\"];\n"; + } + + for (const auto& entry : graph) { + const ResourceName& styleName = entry.first; + size_t styleNodeIndex = getNodeIndex(names, styleName); + + for (const auto& parentName : entry.second) { + std::cout << " node_" << styleNodeIndex << " -> " + << "node_" << getNodeIndex(names, parentName) << ";\n"; + } + } + + std::cout << "}" << std::endl; +} + +} // namespace aapt diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h new file mode 100644 index 0000000..cdb3dcb --- /dev/null +++ b/tools/aapt2/Debug.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_DEBUG_H +#define AAPT_DEBUG_H + +#include "Resource.h" +#include "ResourceTable.h" + +#include <memory> + +namespace aapt { + +struct Debug { + static void printTable(const std::shared_ptr<ResourceTable>& table); + static void printStyleGraph(const std::shared_ptr<ResourceTable>& table, + const ResourceName& targetStyle); +}; + +} // namespace aapt + +#endif // AAPT_DEBUG_H diff --git a/tools/aapt2/Files.cpp b/tools/aapt2/Files.cpp new file mode 100644 index 0000000..8484148 --- /dev/null +++ b/tools/aapt2/Files.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Files.h" +#include "Util.h" + +#include <cerrno> +#include <dirent.h> +#include <string> +#include <sys/stat.h> + +#ifdef HAVE_MS_C_RUNTIME +// Windows includes. +#include <direct.h> +#endif + +namespace aapt { + +FileType getFileType(const StringPiece& path) { + struct stat sb; + if (stat(path.data(), &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) { + return FileType::kNonexistant; + } + return FileType::kUnknown; + } + + if (S_ISREG(sb.st_mode)) { + return FileType::kRegular; + } else if (S_ISDIR(sb.st_mode)) { + return FileType::kDirectory; + } else if (S_ISCHR(sb.st_mode)) { + return FileType::kCharDev; + } else if (S_ISBLK(sb.st_mode)) { + return FileType::kBlockDev; + } else if (S_ISFIFO(sb.st_mode)) { + return FileType::kFifo; +#if defined(S_ISLNK) + } else if (S_ISLNK(sb.st_mode)) { + return FileType::kSymlink; +#endif +#if defined(S_ISSOCK) + } else if (S_ISSOCK(sb.st_mode)) { + return FileType::kSocket; +#endif + } else { + return FileType::kUnknown; + } +} + +std::vector<std::string> listFiles(const StringPiece& root) { + DIR* dir = opendir(root.data()); + if (dir == nullptr) { + Logger::error(Source{ root.toString() }) + << "unable to open file: " + << strerror(errno) + << "." + << std::endl; + return {}; + } + + std::vector<std::string> files; + dirent* entry; + while ((entry = readdir(dir))) { + files.emplace_back(entry->d_name); + } + + closedir(dir); + return files; +} + +inline static int mkdirImpl(const StringPiece& path) { +#ifdef HAVE_MS_C_RUNTIME + return _mkdir(path.toString().c_str()); +#else + return mkdir(path.toString().c_str(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); +#endif +} + +bool mkdirs(const StringPiece& path) { + const char* start = path.begin(); + const char* end = path.end(); + for (const char* current = start; current != end; ++current) { + if (*current == sDirSep) { + StringPiece parentPath(start, current - start); + int result = mkdirImpl(parentPath); + if (result < 0 && errno != EEXIST) { + return false; + } + } + } + return mkdirImpl(path) == 0 || errno == EEXIST; +} + +std::string getStem(const StringPiece& path) { + const char* start = path.begin(); + const char* end = path.end(); + for (const char* current = end - 1; current != start - 1; --current) { + if (*current == sDirSep) { + return std::string(start, current - start); + } + } + return {}; +} + +bool FileFilter::setPattern(const StringPiece& pattern) { + mPatternTokens = util::splitAndLowercase(pattern, ':'); + return true; +} + +bool FileFilter::operator()(const std::string& filename, FileType type) const { + if (filename == "." || filename == "..") { + return false; + } + + const char kDir[] = "dir"; + const char kFile[] = "file"; + const size_t filenameLen = filename.length(); + bool chatty = true; + for (const std::string& token : mPatternTokens) { + const char* tokenStr = token.c_str(); + if (*tokenStr == '!') { + chatty = false; + tokenStr++; + } + + if (strncasecmp(tokenStr, kDir, sizeof(kDir)) == 0) { + if (type != FileType::kDirectory) { + continue; + } + tokenStr += sizeof(kDir); + } + + if (strncasecmp(tokenStr, kFile, sizeof(kFile)) == 0) { + if (type != FileType::kRegular) { + continue; + } + tokenStr += sizeof(kFile); + } + + bool ignore = false; + size_t n = strlen(tokenStr); + if (*tokenStr == '*') { + // Math suffix. + tokenStr++; + n--; + if (n <= filenameLen) { + ignore = strncasecmp(tokenStr, filename.c_str() + filenameLen - n, n) == 0; + } + } else if (n > 1 && tokenStr[n - 1] == '*') { + // Match prefix. + ignore = strncasecmp(tokenStr, filename.c_str(), n - 1) == 0; + } else { + ignore = strcasecmp(tokenStr, filename.c_str()) == 0; + } + + if (ignore) { + if (chatty) { + Logger::warn() + << "skipping " << + (type == FileType::kDirectory ? "dir '" : "file '") + << filename + << "' due to ignore pattern '" + << token + << "'." + << std::endl; + } + return false; + } + } + return true; +} + + +} // namespace aapt diff --git a/tools/aapt2/Files.h b/tools/aapt2/Files.h new file mode 100644 index 0000000..844fd2b --- /dev/null +++ b/tools/aapt2/Files.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_FILES_H +#define AAPT_FILES_H + +#include "Logger.h" +#include "Source.h" +#include "StringPiece.h" + +#include <cassert> +#include <string> +#include <vector> + +namespace aapt { + +#ifdef _WIN32 +constexpr const char sDirSep = '\\'; +#else +constexpr const char sDirSep = '/'; +#endif + +enum class FileType { + kUnknown = 0, + kNonexistant, + kRegular, + kDirectory, + kCharDev, + kBlockDev, + kFifo, + kSymlink, + kSocket, +}; + +FileType getFileType(const StringPiece& path); + +/* + * Lists files under the directory `root`. Files are listed + * with just their leaf (filename) names. + */ +std::vector<std::string> listFiles(const StringPiece& root); + +/* + * Appends a path to `base`, separated by the directory separator. + */ +void appendPath(std::string* base, const StringPiece& part); + +/* + * Appends a series of paths to `base`, separated by the + * system directory separator. + */ +template <typename... Ts > +void appendPath(std::string* base, const StringPiece& part, const Ts&... parts); + +/* + * Makes all the directories in `path`. The last element in the path + * is interpreted as a directory. + */ +bool mkdirs(const StringPiece& path); + +/** + * Returns all but the last part of the path. + */ +std::string getStem(const StringPiece& path); + +/* + * Filter that determines which resource files/directories are + * processed by AAPT. Takes a pattern string supplied by the user. + * Pattern format is specified in the + * FileFilter::setPattern(const std::string&) method. + */ +class FileFilter { +public: + /* + * Patterns syntax: + * - Delimiter is : + * - Entry can start with the flag ! to avoid printing a warning + * about the file being ignored. + * - Entry can have the flag "<dir>" to match only directories + * or <file> to match only files. Default is to match both. + * - Entry can be a simplified glob "<prefix>*" or "*<suffix>" + * where prefix/suffix must have at least 1 character (so that + * we don't match a '*' catch-all pattern.) + * - The special filenames "." and ".." are always ignored. + * - Otherwise the full string is matched. + * - match is not case-sensitive. + */ + bool setPattern(const StringPiece& pattern); + + /** + * Applies the filter, returning true for pass, false for fail. + */ + bool operator()(const std::string& filename, FileType type) const; + +private: + std::vector<std::string> mPatternTokens; +}; + +inline void appendPath(std::string* base, const StringPiece& part) { + assert(base); + *base += sDirSep; + base->append(part.data(), part.size()); +} + +template <typename... Ts > +void appendPath(std::string* base, const StringPiece& part, const Ts&... parts) { + assert(base); + *base += sDirSep; + base->append(part.data(), part.size()); + appendPath(base, parts...); +} + +} // namespace aapt + +#endif // AAPT_FILES_H diff --git a/tools/aapt2/Flag.cpp b/tools/aapt2/Flag.cpp new file mode 100644 index 0000000..76985da --- /dev/null +++ b/tools/aapt2/Flag.cpp @@ -0,0 +1,132 @@ +#include "Flag.h" +#include "StringPiece.h" + +#include <functional> +#include <iomanip> +#include <iostream> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +struct Flag { + std::string name; + std::string description; + std::function<bool(const StringPiece&, std::string*)> action; + bool required; + bool* flagResult; + bool flagValueWhenSet; + bool parsed; +}; + +static std::vector<Flag> sFlags; +static std::vector<std::string> sArgs; + +static std::function<bool(const StringPiece&, std::string*)> wrap( + const std::function<void(const StringPiece&)>& action) { + return [action](const StringPiece& arg, std::string*) -> bool { + action(arg); + return true; + }; +} + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back(Flag{ + name.toString(), description.toString(), wrap(action), + false, nullptr, false, false }); +} + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back(Flag{ name.toString(), description.toString(), wrap(action), + true, nullptr, false, false }); +} + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<bool(const StringPiece&, std::string*)> action) { + sFlags.push_back(Flag{ name.toString(), description.toString(), action, + true, nullptr, false, false }); +} + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool resultWhenSet, + bool* result) { + sFlags.push_back(Flag{ + name.toString(), description.toString(), {}, + false, result, resultWhenSet, false }); +} + +void usageAndDie(const StringPiece& command) { + std::cerr << command << " [options]"; + for (const Flag& flag : sFlags) { + if (flag.required) { + std::cerr << " " << flag.name << " arg"; + } + } + std::cerr << " files..." << std::endl << std::endl << "Options:" << std::endl; + + for (const Flag& flag : sFlags) { + std::string command = flag.name; + if (!flag.flagResult) { + command += " arg "; + } + std::cerr << " " << std::setw(30) << std::left << command + << flag.description << std::endl; + } + exit(1); +} + +void parse(int argc, char** argv, const StringPiece& command) { + std::string errorStr; + for (int i = 0; i < argc; i++) { + const StringPiece arg(argv[i]); + if (*arg.data() != '-') { + sArgs.push_back(arg.toString()); + continue; + } + + bool match = false; + for (Flag& flag : sFlags) { + if (arg == flag.name) { + match = true; + flag.parsed = true; + if (flag.flagResult) { + *flag.flagResult = flag.flagValueWhenSet; + } else { + i++; + if (i >= argc) { + std::cerr << flag.name << " missing argument." << std::endl + << std::endl; + usageAndDie(command); + } + + if (!flag.action(argv[i], &errorStr)) { + std::cerr << errorStr << "." << std::endl << std::endl; + usageAndDie(command); + } + } + break; + } + } + + if (!match) { + std::cerr << "unknown option '" << arg << "'." << std::endl << std::endl; + usageAndDie(command); + } + } + + for (const Flag& flag : sFlags) { + if (flag.required && !flag.parsed) { + std::cerr << "missing required flag " << flag.name << std::endl << std::endl; + usageAndDie(command); + } + } +} + +const std::vector<std::string>& getArgs() { + return sArgs; +} + +} // namespace flag +} // namespace aapt diff --git a/tools/aapt2/Flag.h b/tools/aapt2/Flag.h new file mode 100644 index 0000000..e863742 --- /dev/null +++ b/tools/aapt2/Flag.h @@ -0,0 +1,34 @@ +#ifndef AAPT_FLAG_H +#define AAPT_FLAG_H + +#include "StringPiece.h" + +#include <functional> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<bool(const StringPiece&, std::string*)> action); + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool resultWhenSet, + bool* result); + +void usageAndDie(const StringPiece& command); + +void parse(int argc, char** argv, const StringPiece& command); + +const std::vector<std::string>& getArgs(); + +} // namespace flag +} // namespace aapt + +#endif // AAPT_FLAG_H diff --git a/tools/aapt2/JavaClassGenerator.cpp b/tools/aapt2/JavaClassGenerator.cpp new file mode 100644 index 0000000..e2ffe79 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "JavaClassGenerator.h" +#include "NameMangler.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" + +#include <algorithm> +#include <ostream> +#include <set> +#include <sstream> +#include <tuple> + +namespace aapt { + +// The number of attributes to emit per line in a Styleable array. +constexpr size_t kAttribsPerLine = 4; + +JavaClassGenerator::JavaClassGenerator(const std::shared_ptr<const ResourceTable>& table, + Options options) : + mTable(table), mOptions(options) { +} + +static void generateHeader(std::ostream& out, const StringPiece16& package) { + out << "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" + " *\n" + " * This class was automatically generated by the\n" + " * aapt tool from the resource data it found. It\n" + " * should not be modified by hand.\n" + " */\n\n"; + out << "package " << package << ";" + << std::endl + << std::endl; +} + +static const std::set<StringPiece16> sJavaIdentifiers = { + u"abstract", u"assert", u"boolean", u"break", u"byte", + u"case", u"catch", u"char", u"class", u"const", u"continue", + u"default", u"do", u"double", u"else", u"enum", u"extends", + u"final", u"finally", u"float", u"for", u"goto", u"if", + u"implements", u"import", u"instanceof", u"int", u"interface", + u"long", u"native", u"new", u"package", u"private", u"protected", + u"public", u"return", u"short", u"static", u"strictfp", u"super", + u"switch", u"synchronized", u"this", u"throw", u"throws", + u"transient", u"try", u"void", u"volatile", u"while", u"true", + u"false", u"null" +}; + +static bool isValidSymbol(const StringPiece16& symbol) { + return sJavaIdentifiers.find(symbol) == sJavaIdentifiers.end(); +} + +/* + * Java symbols can not contain . or -, but those are valid in a resource name. + * Replace those with '_'. + */ +static std::u16string transform(const StringPiece16& symbol) { + std::u16string output = symbol.toString(); + for (char16_t& c : output) { + if (c == u'.' || c == u'-') { + c = u'_'; + } + } + return output; +} + +struct GenArgs : ValueVisitorArgs { + GenArgs(std::ostream* o, const std::u16string* p, std::u16string* e) : + out(o), package(p), entryName(e) { + } + + std::ostream* out; + const std::u16string* package; + std::u16string* entryName; +}; + +void JavaClassGenerator::visit(const Styleable& styleable, ValueVisitorArgs& a) { + const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; + std::ostream* out = static_cast<GenArgs&>(a).out; + const std::u16string* package = static_cast<GenArgs&>(a).package; + std::u16string* entryName = static_cast<GenArgs&>(a).entryName; + + // This must be sorted by resource ID. + std::vector<std::pair<ResourceId, ResourceNameRef>> sortedAttributes; + sortedAttributes.reserve(styleable.entries.size()); + for (const auto& attr : styleable.entries) { + // If we are not encoding final attributes, the styleable entry may have no ID + // if we are building a static library. + assert((!mOptions.useFinal || attr.id.isValid()) && "no ID set for Styleable entry"); + assert(attr.name.isValid() && "no name set for Styleable entry"); + sortedAttributes.emplace_back(attr.id, attr.name); + } + std::sort(sortedAttributes.begin(), sortedAttributes.end()); + + // First we emit the array containing the IDs of each attribute. + *out << " " + << "public static final int[] " << transform(*entryName) << " = {"; + + const size_t attrCount = sortedAttributes.size(); + for (size_t i = 0; i < attrCount; i++) { + if (i % kAttribsPerLine == 0) { + *out << std::endl << " "; + } + + *out << sortedAttributes[i].first; + if (i != attrCount - 1) { + *out << ", "; + } + } + *out << std::endl << " };" << std::endl; + + // Now we emit the indices into the array. + for (size_t i = 0; i < attrCount; i++) { + *out << " " + << "public static" << finalModifier + << " int " << transform(*entryName); + + // We may reference IDs from other packages, so prefix the entry name with + // the package. + const ResourceNameRef& itemName = sortedAttributes[i].second; + if (itemName.package != *package) { + *out << "_" << transform(itemName.package); + } + *out << "_" << transform(itemName.entry) << " = " << i << ";" << std::endl; + } +} + +bool JavaClassGenerator::generateType(const std::u16string& package, size_t packageId, + const ResourceTableType& type, std::ostream& out) { + const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; + + std::u16string unmangledPackage; + std::u16string unmangledName; + for (const auto& entry : type.entries) { + ResourceId id = { packageId, type.typeId, entry->entryId }; + assert(id.isValid()); + + unmangledName = entry->name; + if (NameMangler::unmangle(&unmangledName, &unmangledPackage)) { + // The entry name was mangled, and we successfully unmangled it. + // Check that we want to emit this symbol. + if (package != unmangledPackage) { + // Skip the entry if it doesn't belong to the package we're writing. + continue; + } + } else { + if (package != mTable->getPackage()) { + // We are processing a mangled package name, + // but this is a non-mangled resource. + continue; + } + } + + if (!isValidSymbol(unmangledName)) { + ResourceNameRef resourceName = { package, type.type, unmangledName }; + std::stringstream err; + err << "invalid symbol name '" << resourceName << "'"; + mError = err.str(); + return false; + } + + if (type.type == ResourceType::kStyleable) { + assert(!entry->values.empty()); + entry->values.front().value->accept(*this, GenArgs{ &out, &package, &unmangledName }); + } else { + out << " " << "public static" << finalModifier + << " int " << transform(unmangledName) << " = " << id << ";" << std::endl; + } + } + return true; +} + +bool JavaClassGenerator::generate(const std::u16string& package, std::ostream& out) { + const size_t packageId = mTable->getPackageId(); + + generateHeader(out, package); + + out << "public final class R {" << std::endl; + + for (const auto& type : *mTable) { + out << " public static final class " << type->type << " {" << std::endl; + if (!generateType(package, packageId, *type, out)) { + return false; + } + out << " }" << std::endl; + } + + out << "}" << std::endl; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/JavaClassGenerator.h b/tools/aapt2/JavaClassGenerator.h new file mode 100644 index 0000000..f8b9ee3 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_JAVA_CLASS_GENERATOR_H +#define AAPT_JAVA_CLASS_GENERATOR_H + +#include "ResourceTable.h" +#include "ResourceValues.h" + +#include <ostream> +#include <string> + +namespace aapt { + +/* + * Generates the R.java file for a resource table. + */ +class JavaClassGenerator : ConstValueVisitor { +public: + /* + * A set of options for this JavaClassGenerator. + */ + struct Options { + /* + * Specifies whether to use the 'final' modifier + * on resource entries. Default is true. + */ + bool useFinal = true; + }; + + JavaClassGenerator(const std::shared_ptr<const ResourceTable>& table, Options options); + + /* + * Writes the R.java file to `out`. Only symbols belonging to `package` are written. + * All symbols technically belong to a single package, but linked libraries will + * have their names mangled, denoting that they came from a different package. + * We need to generate these symbols in a separate file. + * Returns true on success. + */ + bool generate(const std::u16string& package, std::ostream& out); + + /* + * ConstValueVisitor implementation. + */ + void visit(const Styleable& styleable, ValueVisitorArgs& args); + + const std::string& getError() const; + +private: + bool generateType(const std::u16string& package, size_t packageId, + const ResourceTableType& type, std::ostream& out); + + std::shared_ptr<const ResourceTable> mTable; + Options mOptions; + std::string mError; +}; + +inline const std::string& JavaClassGenerator::getError() const { + return mError; +} + +} // namespace aapt + +#endif // AAPT_JAVA_CLASS_GENERATOR_H diff --git a/tools/aapt2/JavaClassGenerator_test.cpp b/tools/aapt2/JavaClassGenerator_test.cpp new file mode 100644 index 0000000..b385ff4 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator_test.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "JavaClassGenerator.h" +#include "Linker.h" +#include "MockResolver.h" +#include "ResourceTable.h" +#include "ResourceTableResolver.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +struct JavaClassGeneratorTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + mTable->setPackageId(0x01); + } + + bool addResource(const ResourceNameRef& name, ResourceId id) { + return mTable->addResource(name, id, {}, SourceLine{ "test.xml", 21 }, + util::make_unique<Id>()); + } + + std::shared_ptr<ResourceTable> mTable; +}; + +TEST_F(JavaClassGeneratorTest, FailWhenEntryIsJavaKeyword) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"class" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_FALSE(generator.generate(mTable->getPackage(), out)); +} + +TEST_F(JavaClassGeneratorTest, TransformInvalidJavaIdentifierCharacter) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"hey-man" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kAttr, u"cool.attr" }, + ResourceId{ 0x01, 0x01, 0x0000 })); + + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + Reference ref(ResourceName{ u"android", ResourceType::kAttr, u"cool.attr"}); + ref.id = ResourceId{ 0x01, 0x01, 0x0000 }; + styleable->entries.emplace_back(ref); + + ASSERT_TRUE(mTable->addResource(ResourceName{ {}, ResourceType::kStyleable, u"hey.dude" }, + ResourceId{ 0x01, 0x03, 0x0000 }, {}, + SourceLine{ "test.xml", 21 }, std::move(styleable))); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); + std::string output = out.str(); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_man = 0x01020000;")); + + EXPECT_NE(std::string::npos, + output.find("public static final int[] hey_dude = {")); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_dude_cool_attr = 0;")); +} + + +TEST_F(JavaClassGeneratorTest, EmitPackageMangledSymbols) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"foo" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + ResourceTable table; + table.setPackage(u"com.lib"); + ASSERT_TRUE(table.addResource(ResourceName{ {}, ResourceType::kId, u"test" }, {}, + SourceLine{ "lib.xml", 33 }, util::make_unique<Id>())); + ASSERT_TRUE(mTable->merge(std::move(table))); + + Linker linker(mTable, + std::make_shared<MockResolver>(mTable, std::map<ResourceName, ResourceId>()), + {}); + ASSERT_TRUE(linker.linkAndValidate()); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("int foo =")); + EXPECT_EQ(std::string::npos, output.find("int test =")); + + out.str(""); + EXPECT_TRUE(generator.generate(u"com.lib", out)); + output = out.str(); + EXPECT_NE(std::string::npos, output.find("int test =")); + EXPECT_EQ(std::string::npos, output.find("int foo =")); +} + +TEST_F(JavaClassGeneratorTest, EmitOtherPackagesAttributesInStyleable) { + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + styleable->entries.emplace_back(ResourceNameRef{ mTable->getPackage(), + ResourceType::kAttr, + u"bar" }); + styleable->entries.emplace_back(ResourceNameRef{ u"com.lib", ResourceType::kAttr, u"bar" }); + ASSERT_TRUE(mTable->addResource(ResourceName{ {}, ResourceType::kStyleable, u"Foo" }, {}, {}, + std::move(styleable))); + + std::shared_ptr<IResolver> resolver = std::make_shared<MockResolver>(mTable, + std::map<ResourceName, ResourceId>({ + { ResourceName{ u"android", ResourceType::kAttr, u"bar" }, + ResourceId{ 0x01, 0x01, 0x0000 } }, + { ResourceName{ u"com.lib", ResourceType::kAttr, u"bar" }, + ResourceId{ 0x02, 0x01, 0x0000 } }})); + + Linker linker(mTable, resolver, {}); + ASSERT_TRUE(linker.linkAndValidate()); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("int Foo_bar =")); + EXPECT_NE(std::string::npos, output.find("int Foo_com_lib_bar =")); +} + +} // namespace aapt diff --git a/tools/aapt2/Linker.cpp b/tools/aapt2/Linker.cpp new file mode 100644 index 0000000..f3f04a5 --- /dev/null +++ b/tools/aapt2/Linker.cpp @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Linker.h" +#include "Logger.h" +#include "NameMangler.h" +#include "Resolver.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <array> +#include <bitset> +#include <iostream> +#include <map> +#include <ostream> +#include <set> +#include <sstream> +#include <tuple> +#include <vector> + +namespace aapt { + +Linker::Args::Args(const ResourceNameRef& r, const SourceLine& s) : referrer(r), source(s) { +} + +Linker::Linker(const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver, const Options& options) : + mResolver(resolver), mTable(table), mOptions(options), mError(false) { +} + +bool Linker::linkAndValidate() { + std::bitset<256> usedTypeIds; + std::array<std::set<uint16_t>, 256> usedIds; + usedTypeIds.set(0); + + // Collect which resource IDs are already taken. + for (auto& type : *mTable) { + if (type->typeId != ResourceTableType::kUnsetTypeId) { + // The ID for this type has already been set. We + // mark this ID as taken so we don't re-assign it + // later. + usedTypeIds.set(type->typeId); + } + + for (auto& entry : type->entries) { + if (type->typeId != ResourceTableType::kUnsetTypeId && + entry->entryId != ResourceEntry::kUnsetEntryId) { + // The ID for this entry has already been set. We + // mark this ID as taken so we don't re-assign it + // later. + usedIds[type->typeId].insert(entry->entryId); + } + } + } + + // Assign resource IDs that are available. + size_t nextTypeIndex = 0; + for (auto& type : *mTable) { + if (type->typeId == ResourceTableType::kUnsetTypeId) { + while (nextTypeIndex < usedTypeIds.size() && usedTypeIds[nextTypeIndex]) { + nextTypeIndex++; + } + type->typeId = nextTypeIndex++; + } + + const auto endEntryIter = std::end(usedIds[type->typeId]); + auto nextEntryIter = std::begin(usedIds[type->typeId]); + size_t nextIndex = 0; + for (auto& entry : type->entries) { + if (entry->entryId == ResourceTableType::kUnsetTypeId) { + while (nextEntryIter != endEntryIter && + nextIndex == *nextEntryIter) { + nextIndex++; + ++nextEntryIter; + } + entry->entryId = nextIndex++; + } + } + } + + // Now do reference linking. + for (auto& type : *mTable) { + for (auto& entry : type->entries) { + if (entry->publicStatus.isPublic && entry->values.empty()) { + // A public resource has no values. It will not be encoded + // properly without a symbol table. This is a unresolved symbol. + addUnresolvedSymbol(ResourceNameRef{ + mTable->getPackage(), type->type, entry->name }, + entry->publicStatus.source); + continue; + } + + for (auto& valueConfig : entry->values) { + // Dispatch to the right method of this linker + // based on the value's type. + valueConfig.value->accept(*this, Args{ + ResourceNameRef{ mTable->getPackage(), type->type, entry->name }, + valueConfig.source + }); + } + } + } + return !mError; +} + +const Linker::ResourceNameToSourceMap& Linker::getUnresolvedReferences() const { + return mUnresolvedSymbols; +} + +void Linker::doResolveReference(Reference& reference, const SourceLine& source) { + Maybe<ResourceId> result = mResolver->findId(reference.name); + if (!result) { + addUnresolvedSymbol(reference.name, source); + return; + } + assert(result.value().isValid()); + + if (mOptions.linkResourceIds) { + reference.id = result.value(); + } else { + reference.id = 0; + } +} + +const Attribute* Linker::doResolveAttribute(Reference& attribute, const SourceLine& source) { + Maybe<IResolver::Entry> result = mResolver->findAttribute(attribute.name); + if (!result || !result.value().attr) { + addUnresolvedSymbol(attribute.name, source); + return nullptr; + } + + const IResolver::Entry& entry = result.value(); + assert(entry.id.isValid()); + + if (mOptions.linkResourceIds) { + attribute.id = entry.id; + } else { + attribute.id = 0; + } + return entry.attr; +} + +void Linker::visit(Reference& reference, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + if (!reference.name.isValid()) { + // We can't have a completely bad reference. + if (!reference.id.isValid()) { + Logger::error() << "srsly? " << args.referrer << std::endl; + assert(reference.id.isValid()); + } + + // This reference has no name but has an ID. + // It is a really bad error to have no name and have the same + // package ID. + assert(reference.id.packageId() != mTable->getPackageId()); + + // The reference goes outside this package, let it stay as a + // resource ID because it will not change. + return; + } + + doResolveReference(reference, args.source); + + // TODO(adamlesinski): Verify the referencedType is another reference + // or a compatible primitive. +} + +void Linker::processAttributeValue(const ResourceNameRef& name, const SourceLine& source, + const Attribute& attr, std::unique_ptr<Item>& value) { + std::unique_ptr<Item> convertedValue; + visitFunc<RawString>(*value, [&](RawString& str) { + // This is a raw string, so check if it can be converted to anything. + // We can NOT swap value with the converted value in here, since + // we called through the original value. + + auto onCreateReference = [&](const ResourceName& name) { + // We should never get here. All references would have been + // parsed in the parser phase. + assert(false); + }; + + convertedValue = ResourceParser::parseItemForAttribute(*str.value, attr, + onCreateReference); + if (!convertedValue && attr.typeMask & android::ResTable_map::TYPE_STRING) { + // Last effort is to parse as a string. + util::StringBuilder builder; + builder.append(*str.value); + if (builder) { + convertedValue = util::make_unique<String>( + mTable->getValueStringPool().makeRef(builder.str())); + } + } + }); + + if (convertedValue) { + value = std::move(convertedValue); + } + + // Process this new or old value (it can be a reference!). + value->accept(*this, Args{ name, source }); + + // Flatten the value to see what resource type it is. + android::Res_value resValue; + value->flatten(resValue); + + // Always allow references. + const uint32_t typeMask = attr.typeMask | android::ResTable_map::TYPE_REFERENCE; + if (!(typeMask & ResourceParser::androidTypeToAttributeTypeMask(resValue.dataType))) { + Logger::error(source) + << *value + << " is not compatible with attribute " + << attr + << "." + << std::endl; + mError = true; + } +} + +void Linker::visit(Style& style, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + if (style.parent.name.isValid() || style.parent.id.isValid()) { + visit(style.parent, a); + } + + for (Style::Entry& styleEntry : style.entries) { + const Attribute* attr = doResolveAttribute(styleEntry.key, args.source); + if (attr) { + processAttributeValue(args.referrer, args.source, *attr, styleEntry.value); + } + } +} + +void Linker::visit(Attribute& attr, ValueVisitorArgs& a) { + static constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_FLAGS; + if (attr.typeMask & kMask) { + for (auto& symbol : attr.symbols) { + visit(symbol.symbol, a); + } + } +} + +void Linker::visit(Styleable& styleable, ValueVisitorArgs& a) { + for (auto& attrRef : styleable.entries) { + visit(attrRef, a); + } +} + +void Linker::visit(Array& array, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + for (auto& item : array.items) { + item->accept(*this, Args{ args.referrer, args.source }); + } +} + +void Linker::visit(Plural& plural, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + for (auto& item : plural.values) { + if (item) { + item->accept(*this, Args{ args.referrer, args.source }); + } + } +} + +void Linker::addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source) { + mUnresolvedSymbols[name.toResourceName()].push_back(source); +} + +} // namespace aapt diff --git a/tools/aapt2/Linker.h b/tools/aapt2/Linker.h new file mode 100644 index 0000000..6f03515 --- /dev/null +++ b/tools/aapt2/Linker.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_LINKER_H +#define AAPT_LINKER_H + +#include "Resolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Source.h" +#include "StringPiece.h" + +#include <androidfw/AssetManager.h> +#include <map> +#include <memory> +#include <ostream> +#include <set> +#include <vector> + +namespace aapt { + +/** + * The Linker has two jobs. It follows resource references + * and verifies that their targert exists and that their + * types are compatible. The Linker will also assign resource + * IDs and fill in all the dependent references with the newly + * assigned resource IDs. + * + * To do this, the Linker builds a graph of references. This + * can be useful to do other analysis, like building a + * dependency graph of source files. The hope is to be able to + * add functionality that operates on the graph without + * overcomplicating the Linker. + * + * TODO(adamlesinski): Build the graph first then run the separate + * steps over the graph. + */ +class Linker : ValueVisitor { +public: + struct Options { + /** + * Assign resource Ids to references when linking. + * When building a static library, set this to false. + */ + bool linkResourceIds = true; + }; + + /** + * Create a Linker for the given resource table with the sources available in + * IResolver. IResolver should contain the ResourceTable as a source too. + */ + Linker(const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver, const Options& options); + + Linker(const Linker&) = delete; + + virtual ~Linker() = default; + + /** + * Entry point to the linker. Assigns resource IDs, follows references, + * and validates types. Returns true if all references to defined values + * are type-compatible. Missing resource references are recorded but do + * not cause this method to fail. + */ + bool linkAndValidate(); + + /** + * Returns any references to resources that were not defined in any of the + * sources. + */ + using ResourceNameToSourceMap = std::map<ResourceName, std::vector<SourceLine>>; + const ResourceNameToSourceMap& getUnresolvedReferences() const; + +protected: + virtual void doResolveReference(Reference& reference, const SourceLine& source); + virtual const Attribute* doResolveAttribute(Reference& attribute, const SourceLine& source); + + std::shared_ptr<IResolver> mResolver; + +private: + struct Args : public ValueVisitorArgs { + Args(const ResourceNameRef& r, const SourceLine& s); + + const ResourceNameRef& referrer; + const SourceLine& source; + }; + + // + // Overrides of ValueVisitor + // + void visit(Reference& reference, ValueVisitorArgs& args) override; + void visit(Attribute& attribute, ValueVisitorArgs& args) override; + void visit(Styleable& styleable, ValueVisitorArgs& args) override; + void visit(Style& style, ValueVisitorArgs& args) override; + void visit(Array& array, ValueVisitorArgs& args) override; + void visit(Plural& plural, ValueVisitorArgs& args) override; + + void processAttributeValue(const ResourceNameRef& name, const SourceLine& source, + const Attribute& attr, std::unique_ptr<Item>& value); + + void addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source); + + std::shared_ptr<ResourceTable> mTable; + std::map<ResourceName, std::vector<SourceLine>> mUnresolvedSymbols; + Options mOptions; + bool mError; +}; + +} // namespace aapt + +#endif // AAPT_LINKER_H diff --git a/tools/aapt2/Linker_test.cpp b/tools/aapt2/Linker_test.cpp new file mode 100644 index 0000000..d897f98 --- /dev/null +++ b/tools/aapt2/Linker_test.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Linker.h" +#include "ResourceTable.h" +#include "ResourceTableResolver.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +struct LinkerTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + mTable->setPackageId(0x01); + mLinker = std::make_shared<Linker>(mTable, std::make_shared<ResourceTableResolver>( + mTable, std::vector<std::shared_ptr<const android::AssetManager>>()), + Linker::Options{}); + + // Create a few attributes for use in the tests. + + addResource(ResourceName{ {}, ResourceType::kAttr, u"integer" }, + util::make_unique<Attribute>(false, android::ResTable_map::TYPE_INTEGER)); + + addResource(ResourceName{ {}, ResourceType::kAttr, u"string" }, + util::make_unique<Attribute>(false, android::ResTable_map::TYPE_STRING)); + + addResource(ResourceName{ {}, ResourceType::kId, u"apple" }, util::make_unique<Id>()); + + addResource(ResourceName{ {}, ResourceType::kId, u"banana" }, util::make_unique<Id>()); + + std::unique_ptr<Attribute> flagAttr = util::make_unique<Attribute>( + false, android::ResTable_map::TYPE_FLAGS); + flagAttr->symbols.push_back(Attribute::Symbol{ + ResourceNameRef{ u"android", ResourceType::kId, u"apple" }, 1 }); + flagAttr->symbols.push_back(Attribute::Symbol{ + ResourceNameRef{ u"android", ResourceType::kId, u"banana" }, 2 }); + addResource(ResourceName{ {}, ResourceType::kAttr, u"flags" }, std::move(flagAttr)); + } + + /* + * Convenience method for adding resources with the default configuration and some + * bogus source line. + */ + bool addResource(const ResourceNameRef& name, std::unique_ptr<Value> value) { + return mTable->addResource(name, {}, SourceLine{ "test.xml", 21 }, std::move(value)); + } + + std::shared_ptr<ResourceTable> mTable; + std::shared_ptr<Linker> mLinker; +}; + +TEST_F(LinkerTest, DoNotInterpretEscapedStringAsReference) { + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kString, u"foo" }, + util::make_unique<String>(mTable->getValueStringPool().makeRef(u"?123")))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); +} + +TEST_F(LinkerTest, EscapeAndConvertRawString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u" 123")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + EXPECT_NE(nullptr, dynamic_cast<BinaryPrimitive*>(result->entries.front().value.get())); +} + +TEST_F(LinkerTest, FailToConvertRawString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"yo what is up?")) + }); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_FALSE(mLinker->linkAndValidate()); +} + +TEST_F(LinkerTest, ConvertRawStringToString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"string" }, + util::make_unique<RawString>( + mTable->getValueStringPool().makeRef(u" \"this is \\u00fa\".")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + const String* str = dynamic_cast<const String*>(result->entries.front().value.get()); + ASSERT_NE(nullptr, str); + EXPECT_EQ(*str->value, u"this is \u00fa."); +} + +TEST_F(LinkerTest, ConvertRawStringToFlags) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"flags" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"banana | apple")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + const BinaryPrimitive* bin = dynamic_cast<const BinaryPrimitive*>( + result->entries.front().value.get()); + ASSERT_NE(nullptr, bin); + EXPECT_EQ(bin->value.data, 1u | 2u); +} + +TEST_F(LinkerTest, AllowReferenceWithOnlyResourceIdPointingToDifferentPackage) { + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kInteger, u"foo" }, + util::make_unique<Reference>(ResourceId{ 0x02, 0x01, 0x01 }))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); +} + +} // namespace aapt diff --git a/tools/aapt2/Locale.cpp b/tools/aapt2/Locale.cpp new file mode 100644 index 0000000..eed0ea7 --- /dev/null +++ b/tools/aapt2/Locale.cpp @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Locale.h" +#include "Util.h" + +#include <algorithm> +#include <ctype.h> +#include <string> +#include <vector> + +namespace aapt { + +using android::ResTable_config; + +void LocaleValue::setLanguage(const char* languageChars) { + size_t i = 0; + while ((*languageChars) != '\0') { + language[i++] = ::tolower(*languageChars); + languageChars++; + } +} + +void LocaleValue::setRegion(const char* regionChars) { + size_t i = 0; + while ((*regionChars) != '\0') { + region[i++] = ::toupper(*regionChars); + regionChars++; + } +} + +void LocaleValue::setScript(const char* scriptChars) { + size_t i = 0; + while ((*scriptChars) != '\0') { + if (i == 0) { + script[i++] = ::toupper(*scriptChars); + } else { + script[i++] = ::tolower(*scriptChars); + } + scriptChars++; + } +} + +void LocaleValue::setVariant(const char* variantChars) { + size_t i = 0; + while ((*variantChars) != '\0') { + variant[i++] = *variantChars; + variantChars++; + } +} + +static inline bool isAlpha(const std::string& str) { + return std::all_of(std::begin(str), std::end(str), ::isalpha); +} + +static inline bool isNumber(const std::string& str) { + return std::all_of(std::begin(str), std::end(str), ::isdigit); +} + +bool LocaleValue::initFromFilterString(const std::string& str) { + // A locale (as specified in the filter) is an underscore separated name such + // as "en_US", "en_Latn_US", or "en_US_POSIX". + std::vector<std::string> parts = util::splitAndLowercase(str, '_'); + + const int numTags = parts.size(); + bool valid = false; + if (numTags >= 1) { + const std::string& lang = parts[0]; + if (isAlpha(lang) && (lang.length() == 2 || lang.length() == 3)) { + setLanguage(lang.c_str()); + valid = true; + } + } + + if (!valid || numTags == 1) { + return valid; + } + + // At this point, valid == true && numTags > 1. + const std::string& part2 = parts[1]; + if ((part2.length() == 2 && isAlpha(part2)) || + (part2.length() == 3 && isNumber(part2))) { + setRegion(part2.c_str()); + } else if (part2.length() == 4 && isAlpha(part2)) { + setScript(part2.c_str()); + } else if (part2.length() >= 5 && part2.length() <= 8) { + setVariant(part2.c_str()); + } else { + valid = false; + } + + if (!valid || numTags == 2) { + return valid; + } + + // At this point, valid == true && numTags > 1. + const std::string& part3 = parts[2]; + if (((part3.length() == 2 && isAlpha(part3)) || + (part3.length() == 3 && isNumber(part3))) && script[0]) { + setRegion(part3.c_str()); + } else if (part3.length() >= 5 && part3.length() <= 8) { + setVariant(part3.c_str()); + } else { + valid = false; + } + + if (!valid || numTags == 3) { + return valid; + } + + const std::string& part4 = parts[3]; + if (part4.length() >= 5 && part4.length() <= 8) { + setVariant(part4.c_str()); + } else { + valid = false; + } + + if (!valid || numTags > 4) { + return false; + } + + return true; +} + +ssize_t LocaleValue::initFromParts(std::vector<std::string>::iterator iter, + std::vector<std::string>::iterator end) { + const std::vector<std::string>::iterator startIter = iter; + + std::string& part = *iter; + if (part[0] == 'b' && part[1] == '+') { + // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags, + // except that the separator is "+" and not "-". + std::vector<std::string> subtags = util::splitAndLowercase(part, '+'); + subtags.erase(subtags.begin()); + if (subtags.size() == 1) { + setLanguage(subtags[0].c_str()); + } else if (subtags.size() == 2) { + setLanguage(subtags[0].c_str()); + + // The second tag can either be a region, a variant or a script. + switch (subtags[1].size()) { + case 2: + case 3: + setRegion(subtags[1].c_str()); + break; + case 4: + setScript(subtags[1].c_str()); + break; + case 5: + case 6: + case 7: + case 8: + setVariant(subtags[1].c_str()); + break; + default: + return -1; + } + } else if (subtags.size() == 3) { + // The language is always the first subtag. + setLanguage(subtags[0].c_str()); + + // The second subtag can either be a script or a region code. + // If its size is 4, it's a script code, else it's a region code. + if (subtags[1].size() == 4) { + setScript(subtags[1].c_str()); + } else if (subtags[1].size() == 2 || subtags[1].size() == 3) { + setRegion(subtags[1].c_str()); + } else { + return -1; + } + + // The third tag can either be a region code (if the second tag was + // a script), else a variant code. + if (subtags[2].size() > 4) { + setVariant(subtags[2].c_str()); + } else { + setRegion(subtags[2].c_str()); + } + } else if (subtags.size() == 4) { + setLanguage(subtags[0].c_str()); + setScript(subtags[1].c_str()); + setRegion(subtags[2].c_str()); + setVariant(subtags[3].c_str()); + } else { + return -1; + } + + ++iter; + + } else { + if ((part.length() == 2 || part.length() == 3) + && isAlpha(part) && part != "car") { + setLanguage(part.c_str()); + ++iter; + + if (iter != end) { + const std::string& regionPart = *iter; + if (regionPart.c_str()[0] == 'r' && regionPart.length() == 3) { + setRegion(regionPart.c_str() + 1); + ++iter; + } + } + } + } + + return static_cast<ssize_t>(iter - startIter); +} + + +std::string LocaleValue::toDirName() const { + std::string dirName; + if (language[0]) { + dirName += language; + } else { + return dirName; + } + + if (script[0]) { + dirName += "-s"; + dirName += script; + } + + if (region[0]) { + dirName += "-r"; + dirName += region; + } + + if (variant[0]) { + dirName += "-v"; + dirName += variant; + } + + return dirName; +} + +void LocaleValue::initFromResTable(const ResTable_config& config) { + config.unpackLanguage(language); + config.unpackRegion(region); + if (config.localeScript[0]) { + memcpy(script, config.localeScript, sizeof(config.localeScript)); + } + + if (config.localeVariant[0]) { + memcpy(variant, config.localeVariant, sizeof(config.localeVariant)); + } +} + +void LocaleValue::writeTo(ResTable_config* out) const { + out->packLanguage(language); + out->packRegion(region); + + if (script[0]) { + memcpy(out->localeScript, script, sizeof(out->localeScript)); + } + + if (variant[0]) { + memcpy(out->localeVariant, variant, sizeof(out->localeVariant)); + } +} + +} // namespace aapt diff --git a/tools/aapt2/Locale.h b/tools/aapt2/Locale.h new file mode 100644 index 0000000..ceec764 --- /dev/null +++ b/tools/aapt2/Locale.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_LOCALE_VALUE_H +#define AAPT_LOCALE_VALUE_H + +#include <androidfw/ResourceTypes.h> +#include <string> +#include <vector> + +namespace aapt { + +/** + * A convenience class to build and parse locales. + */ +struct LocaleValue { + char language[4]; + char region[4]; + char script[4]; + char variant[8]; + + inline LocaleValue(); + + /** + * Initialize this LocaleValue from a config string. + */ + bool initFromFilterString(const std::string& config); + + /** + * Initialize this LocaleValue from parts of a vector. + */ + ssize_t initFromParts(std::vector<std::string>::iterator iter, + std::vector<std::string>::iterator end); + + /** + * Initialize this LocaleValue from a ResTable_config. + */ + void initFromResTable(const android::ResTable_config& config); + + /** + * Set the locale in a ResTable_config from this LocaleValue. + */ + void writeTo(android::ResTable_config* out) const; + + std::string toDirName() const; + + inline int compare(const LocaleValue& other) const; + + inline bool operator<(const LocaleValue& o) const; + inline bool operator<=(const LocaleValue& o) const; + inline bool operator==(const LocaleValue& o) const; + inline bool operator!=(const LocaleValue& o) const; + inline bool operator>=(const LocaleValue& o) const; + inline bool operator>(const LocaleValue& o) const; + +private: + void setLanguage(const char* language); + void setRegion(const char* language); + void setScript(const char* script); + void setVariant(const char* variant); +}; + +// +// Implementation +// + +LocaleValue::LocaleValue() { + memset(this, 0, sizeof(LocaleValue)); +} + +int LocaleValue::compare(const LocaleValue& other) const { + return memcmp(this, &other, sizeof(LocaleValue)); +} + +bool LocaleValue::operator<(const LocaleValue& o) const { + return compare(o) < 0; +} + +bool LocaleValue::operator<=(const LocaleValue& o) const { + return compare(o) <= 0; +} + +bool LocaleValue::operator==(const LocaleValue& o) const { + return compare(o) == 0; +} + +bool LocaleValue::operator!=(const LocaleValue& o) const { + return compare(o) != 0; +} + +bool LocaleValue::operator>=(const LocaleValue& o) const { + return compare(o) >= 0; +} + +bool LocaleValue::operator>(const LocaleValue& o) const { + return compare(o) > 0; +} + +} // namespace aapt + +#endif // AAPT_LOCALE_VALUE_H diff --git a/tools/aapt2/Locale_test.cpp b/tools/aapt2/Locale_test.cpp new file mode 100644 index 0000000..4e154d6 --- /dev/null +++ b/tools/aapt2/Locale_test.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Locale.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +static ::testing::AssertionResult TestLanguage(const char* input, const char* lang) { + std::vector<std::string> parts = util::splitAndLowercase(std::string(input), '-'); + LocaleValue lv; + ssize_t count = lv.initFromParts(std::begin(parts), std::end(parts)); + if (count < 0) { + return ::testing::AssertionFailure() << " failed to parse '" << input << "'."; + } + + if (count != 1) { + return ::testing::AssertionFailure() << count + << " parts were consumed parsing '" << input << "' but expected 1."; + } + + if (memcmp(lv.language, lang, std::min(strlen(lang), sizeof(lv.language))) != 0) { + return ::testing::AssertionFailure() << "expected " << lang << " but got " + << std::string(lv.language, sizeof(lv.language)) << "."; + } + + return ::testing::AssertionSuccess(); +} + +static ::testing::AssertionResult TestLanguageRegion(const char* input, const char* lang, + const char* region) { + std::vector<std::string> parts = util::splitAndLowercase(std::string(input), '-'); + LocaleValue lv; + ssize_t count = lv.initFromParts(std::begin(parts), std::end(parts)); + if (count < 0) { + return ::testing::AssertionFailure() << " failed to parse '" << input << "'."; + } + + if (count != 2) { + return ::testing::AssertionFailure() << count + << " parts were consumed parsing '" << input << "' but expected 2."; + } + + if (memcmp(lv.language, lang, std::min(strlen(lang), sizeof(lv.language))) != 0) { + return ::testing::AssertionFailure() << "expected " << input << " but got " + << std::string(lv.language, sizeof(lv.language)) << "."; + } + + if (memcmp(lv.region, region, std::min(strlen(region), sizeof(lv.region))) != 0) { + return ::testing::AssertionFailure() << "expected " << region << " but got " + << std::string(lv.region, sizeof(lv.region)) << "."; + } + + return ::testing::AssertionSuccess(); +} + +TEST(ConfigDescriptionTest, ParseLanguage) { + EXPECT_TRUE(TestLanguage("en", "en")); + EXPECT_TRUE(TestLanguage("fr", "fr")); + EXPECT_FALSE(TestLanguage("land", "")); + EXPECT_TRUE(TestLanguage("fr-land", "fr")); + + EXPECT_TRUE(TestLanguageRegion("fr-rCA", "fr", "CA")); +} + +} // namespace aapt diff --git a/tools/aapt2/Logger.cpp b/tools/aapt2/Logger.cpp new file mode 100644 index 0000000..3847185 --- /dev/null +++ b/tools/aapt2/Logger.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2015 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. + */ +#include "Logger.h" +#include "Source.h" + +#include <memory> +#include <iostream> + +namespace aapt { + +Log::Log(std::ostream& _out, std::ostream& _err) : out(_out), err(_err) { +} + +std::shared_ptr<Log> Logger::sLog(std::make_shared<Log>(std::cerr, std::cerr)); + +void Logger::setLog(const std::shared_ptr<Log>& log) { + sLog = log; +} + +std::ostream& Logger::error() { + return sLog->err << "error: "; +} + +std::ostream& Logger::error(const Source& source) { + return sLog->err << source << ": error: "; +} + +std::ostream& Logger::error(const SourceLine& source) { + return sLog->err << source << ": error: "; +} + +std::ostream& Logger::warn() { + return sLog->err << "warning: "; +} + +std::ostream& Logger::warn(const Source& source) { + return sLog->err << source << ": warning: "; +} + +std::ostream& Logger::warn(const SourceLine& source) { + return sLog->err << source << ": warning: "; +} + +std::ostream& Logger::note() { + return sLog->out << "note: "; +} + +std::ostream& Logger::note(const Source& source) { + return sLog->err << source << ": note: "; +} + +std::ostream& Logger::note(const SourceLine& source) { + return sLog->err << source << ": note: "; +} + +SourceLogger::SourceLogger(const Source& source) +: mSource(source) { +} + +std::ostream& SourceLogger::error() { + return Logger::error(mSource); +} + +std::ostream& SourceLogger::error(size_t line) { + return Logger::error(SourceLine{ mSource.path, line }); +} + +std::ostream& SourceLogger::warn() { + return Logger::warn(mSource); +} + +std::ostream& SourceLogger::warn(size_t line) { + return Logger::warn(SourceLine{ mSource.path, line }); +} + +std::ostream& SourceLogger::note() { + return Logger::note(mSource); +} + +std::ostream& SourceLogger::note(size_t line) { + return Logger::note(SourceLine{ mSource.path, line }); +} + +} // namespace aapt diff --git a/tools/aapt2/Logger.h b/tools/aapt2/Logger.h new file mode 100644 index 0000000..1d437eb --- /dev/null +++ b/tools/aapt2/Logger.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_LOGGER_H +#define AAPT_LOGGER_H + +#include "Source.h" + +#include <memory> +#include <ostream> +#include <string> +#include <utils/String8.h> + +namespace aapt { + +struct Log { + Log(std::ostream& out, std::ostream& err); + Log(const Log& rhs) = delete; + + std::ostream& out; + std::ostream& err; +}; + +class Logger { +public: + static void setLog(const std::shared_ptr<Log>& log); + + static std::ostream& error(); + static std::ostream& error(const Source& source); + static std::ostream& error(const SourceLine& sourceLine); + + static std::ostream& warn(); + static std::ostream& warn(const Source& source); + static std::ostream& warn(const SourceLine& sourceLine); + + static std::ostream& note(); + static std::ostream& note(const Source& source); + static std::ostream& note(const SourceLine& sourceLine); + +private: + static std::shared_ptr<Log> sLog; +}; + +class SourceLogger { +public: + SourceLogger(const Source& source); + + std::ostream& error(); + std::ostream& error(size_t line); + + std::ostream& warn(); + std::ostream& warn(size_t line); + + std::ostream& note(); + std::ostream& note(size_t line); + +private: + Source mSource; +}; + +inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) { + android::String8 utf8(str.data(), str.size()); + return out.write(utf8.string(), utf8.size()); +} + +} // namespace aapt + +#endif // AAPT_LOGGER_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp new file mode 100644 index 0000000..41c229d --- /dev/null +++ b/tools/aapt2/Main.cpp @@ -0,0 +1,1277 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "AppInfo.h" +#include "BigBuffer.h" +#include "BinaryResourceParser.h" +#include "BindingXmlPullParser.h" +#include "Debug.h" +#include "Files.h" +#include "Flag.h" +#include "JavaClassGenerator.h" +#include "Linker.h" +#include "ManifestMerger.h" +#include "ManifestParser.h" +#include "ManifestValidator.h" +#include "NameMangler.h" +#include "Png.h" +#include "ProguardRules.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceTableResolver.h" +#include "ResourceValues.h" +#include "SdkConstants.h" +#include "SourceXmlPullParser.h" +#include "StringPiece.h" +#include "TableFlattener.h" +#include "Util.h" +#include "XmlFlattener.h" +#include "ZipFile.h" + +#include <algorithm> +#include <androidfw/AssetManager.h> +#include <cstdlib> +#include <dirent.h> +#include <errno.h> +#include <fstream> +#include <iostream> +#include <sstream> +#include <sys/stat.h> +#include <unordered_set> +#include <utils/Errors.h> + +constexpr const char* kAaptVersionStr = "2.0-alpha"; + +using namespace aapt; + +/** + * Used with smart pointers to free malloc'ed memory. + */ +struct DeleteMalloc { + void operator()(void* ptr) { + free(ptr); + } +}; + +struct StaticLibraryData { + Source source; + std::unique_ptr<ZipFile> apk; +}; + +/** + * Collect files from 'root', filtering out any files that do not + * match the FileFilter 'filter'. + */ +bool walkTree(const Source& root, const FileFilter& filter, + std::vector<Source>* outEntries) { + bool error = false; + + for (const std::string& dirName : listFiles(root.path)) { + std::string dir = root.path; + appendPath(&dir, dirName); + + FileType ft = getFileType(dir); + if (!filter(dirName, ft)) { + continue; + } + + if (ft != FileType::kDirectory) { + continue; + } + + for (const std::string& fileName : listFiles(dir)) { + std::string file(dir); + appendPath(&file, fileName); + + FileType ft = getFileType(file); + if (!filter(fileName, ft)) { + continue; + } + + if (ft != FileType::kRegular) { + Logger::error(Source{ file }) << "not a regular file." << std::endl; + error = true; + continue; + } + outEntries->push_back(Source{ file }); + } + } + return !error; +} + +void versionStylesForCompat(const std::shared_ptr<ResourceTable>& table) { + for (auto& type : *table) { + if (type->type != ResourceType::kStyle) { + continue; + } + + for (auto& entry : type->entries) { + // Add the versioned styles we want to create + // here. They are added to the table after + // iterating over the original set of styles. + // + // A stack is used since auto-generated styles + // from later versions should override + // auto-generated styles from earlier versions. + // Iterating over the styles is done in order, + // so we will always visit sdkVersions from smallest + // to largest. + std::stack<ResourceConfigValue> addStack; + + for (ResourceConfigValue& configValue : entry->values) { + visitFunc<Style>(*configValue.value, [&](Style& style) { + // Collect which entries we've stripped and the smallest + // SDK level which was stripped. + size_t minSdkStripped = std::numeric_limits<size_t>::max(); + std::vector<Style::Entry> stripped; + + // Iterate over the style's entries and erase/record the + // attributes whose SDK level exceeds the config's sdkVersion. + auto iter = style.entries.begin(); + while (iter != style.entries.end()) { + if (iter->key.name.package == u"android") { + size_t sdkLevel = findAttributeSdkLevel(iter->key.name); + if (sdkLevel > 1 && sdkLevel > configValue.config.sdkVersion) { + // Record that we are about to strip this. + stripped.emplace_back(std::move(*iter)); + minSdkStripped = std::min(minSdkStripped, sdkLevel); + + // Erase this from this style. + iter = style.entries.erase(iter); + continue; + } + } + ++iter; + } + + if (!stripped.empty()) { + // We have stripped attributes, so let's create a new style to hold them. + ConfigDescription versionConfig(configValue.config); + versionConfig.sdkVersion = minSdkStripped; + + ResourceConfigValue value = { + versionConfig, + configValue.source, + {}, + + // Create a copy of the original style. + std::unique_ptr<Value>(configValue.value->clone( + &table->getValueStringPool())) + }; + + Style& newStyle = static_cast<Style&>(*value.value); + + // Move the recorded stripped attributes into this new style. + std::move(stripped.begin(), stripped.end(), + std::back_inserter(newStyle.entries)); + + // We will add this style to the table later. If we do it now, we will + // mess up iteration. + addStack.push(std::move(value)); + } + }); + } + + auto comparator = + [](const ResourceConfigValue& lhs, const ConfigDescription& rhs) -> bool { + return lhs.config < rhs; + }; + + while (!addStack.empty()) { + ResourceConfigValue& value = addStack.top(); + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), + value.config, comparator); + if (iter == entry->values.end() || iter->config != value.config) { + entry->values.insert(iter, std::move(value)); + } + addStack.pop(); + } + } + } +} + +struct CompileItem { + ResourceName name; + ConfigDescription config; + Source source; + std::string extension; +}; + +struct LinkItem { + ResourceName name; + ConfigDescription config; + Source source; + std::string originalPath; + ZipFile* apk; + std::u16string originalPackage; +}; + +template <typename TChar> +static BasicStringPiece<TChar> getExtension(const BasicStringPiece<TChar>& str) { + auto iter = std::find(str.begin(), str.end(), static_cast<TChar>('.')); + if (iter == str.end()) { + return BasicStringPiece<TChar>(); + } + size_t offset = (iter - str.begin()) + 1; + return str.substr(offset, str.size() - offset); +} + +std::string buildFileReference(const ResourceNameRef& name, const ConfigDescription& config, + const StringPiece& extension) { + std::stringstream path; + path << "res/" << name.type; + if (config != ConfigDescription{}) { + path << "-" << config; + } + path << "/" << util::utf16ToUtf8(name.entry); + if (!extension.empty()) { + path << "." << extension; + } + return path.str(); +} + +std::string buildFileReference(const CompileItem& item) { + return buildFileReference(item.name, item.config, item.extension); +} + +std::string buildFileReference(const LinkItem& item) { + return buildFileReference(item.name, item.config, getExtension<char>(item.originalPath)); +} + +template <typename T> +bool addFileReference(const std::shared_ptr<ResourceTable>& table, const T& item) { + StringPool& pool = table->getValueStringPool(); + StringPool::Ref ref = pool.makeRef(util::utf8ToUtf16(buildFileReference(item)), + StringPool::Context{ 0, item.config }); + return table->addResource(item.name, item.config, item.source.line(0), + util::make_unique<FileReference>(ref)); +} + +struct AaptOptions { + enum class Phase { + Link, + Compile, + Dump, + DumpStyleGraph, + }; + + enum class PackageType { + StandardApp, + StaticLibrary, + }; + + // The phase to process. + Phase phase; + + // The type of package to produce. + PackageType packageType = PackageType::StandardApp; + + // Details about the app. + AppInfo appInfo; + + // The location of the manifest file. + Source manifest; + + // The APK files to link. + std::vector<Source> input; + + // The libraries these files may reference. + std::vector<Source> libraries; + + // Output path. This can be a directory or file + // depending on the phase. + Source output; + + // Directory in which to write binding xml files. + Source bindingOutput; + + // Directory to in which to generate R.java. + Maybe<Source> generateJavaClass; + + // File in which to produce proguard rules. + Maybe<Source> generateProguardRules; + + // Whether to output verbose details about + // compilation. + bool verbose = false; + + // Whether or not to auto-version styles or layouts + // referencing attributes defined in a newer SDK + // level than the style or layout is defined for. + bool versionStylesAndLayouts = true; + + // The target style that will have it's style hierarchy dumped + // when the phase is DumpStyleGraph. + ResourceName dumpStyleTarget; +}; + +struct IdCollector : public xml::Visitor { + IdCollector(const Source& source, const std::shared_ptr<ResourceTable>& table) : + mSource(source), mTable(table) { + } + + virtual void visit(xml::Text* node) override {} + + virtual void visit(xml::Namespace* node) override { + for (const auto& child : node->children) { + child->accept(this); + } + } + + virtual void visit(xml::Element* node) override { + for (const xml::Attribute& attr : node->attributes) { + bool create = false; + bool priv = false; + ResourceNameRef nameRef; + if (ResourceParser::tryParseReference(attr.value, &nameRef, &create, &priv)) { + if (create) { + mTable->addResource(nameRef, {}, mSource.line(node->lineNumber), + util::make_unique<Id>()); + } + } + } + + for (const auto& child : node->children) { + child->accept(this); + } + } + +private: + Source mSource; + std::shared_ptr<ResourceTable> mTable; +}; + +bool compileXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, + const CompileItem& item, ZipFile* outApk) { + std::ifstream in(item.source.path, std::ifstream::binary); + if (!in) { + Logger::error(item.source) << strerror(errno) << std::endl; + return false; + } + + SourceLogger logger(item.source); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + if (!root) { + return false; + } + + // Collect any resource ID's declared here. + IdCollector idCollector(item.source, table); + root->accept(&idCollector); + + BigBuffer outBuffer(1024); + if (!xml::flatten(root.get(), options.appInfo.package, &outBuffer)) { + logger.error() << "failed to encode XML." << std::endl; + return false; + } + + // Write the resulting compiled XML file to the output APK. + if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressStored, + nullptr) != android::NO_ERROR) { + Logger::error(options.output) << "failed to write compiled '" << item.source + << "' to apk." << std::endl; + return false; + } + return true; +} + +/** + * Determines if a layout should be auto generated based on SDK level. We do not + * generate a layout if there is already a layout defined whose SDK version is greater than + * the one we want to generate. + */ +bool shouldGenerateVersionedResource(const std::shared_ptr<const ResourceTable>& table, + const ResourceName& name, const ConfigDescription& config, + int sdkVersionToGenerate) { + assert(sdkVersionToGenerate > config.sdkVersion); + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table->findResource(name); + assert(type && entry); + + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), config, + [](const ResourceConfigValue& lhs, const ConfigDescription& config) -> bool { + return lhs.config < config; + }); + + assert(iter != entry->values.end()); + ++iter; + + if (iter == entry->values.end()) { + return true; + } + + ConfigDescription newConfig = config; + newConfig.sdkVersion = sdkVersionToGenerate; + return newConfig < iter->config; +} + +bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver, const LinkItem& item, + const void* data, size_t dataLen, ZipFile* outApk, std::queue<LinkItem>* outQueue, + proguard::KeepSet* keepSet) { + SourceLogger logger(item.source); + std::unique_ptr<xml::Node> root = xml::inflate(data, dataLen, &logger); + if (!root) { + return false; + } + + xml::FlattenOptions xmlOptions; + if (options.packageType == AaptOptions::PackageType::StaticLibrary) { + xmlOptions.keepRawValues = true; + } + + if (options.versionStylesAndLayouts) { + // We strip attributes that do not belong in this version of the resource. + // Non-version qualified resources have an implicit version 1 requirement. + xmlOptions.maxSdkAttribute = item.config.sdkVersion ? item.config.sdkVersion : 1; + } + + if (options.generateProguardRules) { + proguard::collectProguardRules(item.name.type, item.source, root.get(), keepSet); + } + + BigBuffer outBuffer(1024); + Maybe<size_t> minStrippedSdk = xml::flattenAndLink(item.source, root.get(), + item.originalPackage, resolver, + xmlOptions, &outBuffer); + if (!minStrippedSdk) { + logger.error() << "failed to encode XML." << std::endl; + return false; + } + + if (minStrippedSdk.value() > 0) { + // Something was stripped, so let's generate a new file + // with the version of the smallest SDK version stripped. + // We can only generate a versioned layout if there doesn't exist a layout + // with sdk version greater than the current one but less than the one we + // want to generate. + if (shouldGenerateVersionedResource(table, item.name, item.config, + minStrippedSdk.value())) { + LinkItem newWork = item; + newWork.config.sdkVersion = minStrippedSdk.value(); + outQueue->push(newWork); + + if (!addFileReference(table, newWork)) { + Logger::error(options.output) << "failed to add auto-versioned resource '" + << newWork.name << "'." << std::endl; + return false; + } + } + } + + if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressDeflated, + nullptr) != android::NO_ERROR) { + Logger::error(options.output) << "failed to write linked file '" + << buildFileReference(item) << "' to apk." << std::endl; + return false; + } + return true; +} + +bool compilePng(const AaptOptions& options, const CompileItem& item, ZipFile* outApk) { + std::ifstream in(item.source.path, std::ifstream::binary); + if (!in) { + Logger::error(item.source) << strerror(errno) << std::endl; + return false; + } + + BigBuffer outBuffer(4096); + std::string err; + Png png; + if (!png.process(item.source, in, &outBuffer, {}, &err)) { + Logger::error(item.source) << err << std::endl; + return false; + } + + if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressStored, + nullptr) != android::NO_ERROR) { + Logger::error(options.output) << "failed to write compiled '" << item.source + << "' to apk." << std::endl; + return false; + } + return true; +} + +bool copyFile(const AaptOptions& options, const CompileItem& item, ZipFile* outApk) { + if (outApk->add(item.source.path.data(), buildFileReference(item).data(), + ZipEntry::kCompressStored, nullptr) != android::NO_ERROR) { + Logger::error(options.output) << "failed to copy file '" << item.source << "' to apk." + << std::endl; + return false; + } + return true; +} + +bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver>& resolver, + const std::map<std::shared_ptr<ResourceTable>, StaticLibraryData>& libApks, + const android::ResTable& table, ZipFile* outApk, proguard::KeepSet* keepSet) { + if (options.verbose) { + Logger::note(options.manifest) << "compiling AndroidManifest.xml." << std::endl; + } + + std::ifstream in(options.manifest.path, std::ifstream::binary); + if (!in) { + Logger::error(options.manifest) << strerror(errno) << std::endl; + return false; + } + + SourceLogger logger(options.manifest); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + if (!root) { + return false; + } + + ManifestMerger merger({}); + if (!merger.setAppManifest(options.manifest, options.appInfo.package, std::move(root))) { + return false; + } + + for (const auto& entry : libApks) { + ZipFile* libApk = entry.second.apk.get(); + const std::u16string& libPackage = entry.first->getPackage(); + const Source& libSource = entry.second.source; + + ZipEntry* zipEntry = libApk->getEntryByName("AndroidManifest.xml"); + if (!zipEntry) { + continue; + } + + std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( + libApk->uncompress(zipEntry)); + assert(uncompressedData); + + SourceLogger logger(libSource); + std::unique_ptr<xml::Node> libRoot = xml::inflate(uncompressedData.get(), + zipEntry->getUncompressedLen(), &logger); + if (!libRoot) { + return false; + } + + if (!merger.mergeLibraryManifest(libSource, libPackage, std::move(libRoot))) { + return false; + } + } + + if (options.generateProguardRules) { + proguard::collectProguardRulesForManifest(options.manifest, merger.getMergedXml(), + keepSet); + } + + BigBuffer outBuffer(1024); + if (!xml::flattenAndLink(options.manifest, merger.getMergedXml(), options.appInfo.package, + resolver, {}, &outBuffer)) { + return false; + } + + std::unique_ptr<uint8_t[]> data = util::copy(outBuffer); + + android::ResXMLTree tree; + if (tree.setTo(data.get(), outBuffer.size(), false) != android::NO_ERROR) { + return false; + } + + ManifestValidator validator(table); + if (!validator.validate(options.manifest, &tree)) { + return false; + } + + if (outApk->add(data.get(), outBuffer.size(), "AndroidManifest.xml", + ZipEntry::kCompressStored, nullptr) != android::NO_ERROR) { + Logger::error(options.output) << "failed to write 'AndroidManifest.xml' to apk." + << std::endl; + return false; + } + return true; +} + +static bool compileValues(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); + ResourceParser parser(table, source, config, xmlParser); + return parser.parse(); +} + +struct ResourcePathData { + std::u16string resourceDir; + std::u16string name; + std::string extension; + ConfigDescription config; +}; + +/** + * Resource file paths are expected to look like: + * [--/res/]type[-config]/name + */ +static Maybe<ResourcePathData> extractResourcePathData(const Source& source) { + // TODO(adamlesinski): Use Windows path separator on windows. + std::vector<std::string> parts = util::splitAndLowercase(source.path, '/'); + if (parts.size() < 2) { + Logger::error(source) << "bad resource path." << std::endl; + return {}; + } + + std::string& dir = parts[parts.size() - 2]; + StringPiece dirStr = dir; + + ConfigDescription config; + size_t dashPos = dir.find('-'); + if (dashPos != std::string::npos) { + StringPiece configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1)); + if (!ConfigDescription::parse(configStr, &config)) { + Logger::error(source) + << "invalid configuration '" + << configStr + << "'." + << std::endl; + return {}; + } + dirStr = dirStr.substr(0, dashPos); + } + + std::string& filename = parts[parts.size() - 1]; + StringPiece name = filename; + StringPiece extension; + size_t dotPos = filename.find('.'); + if (dotPos != std::string::npos) { + extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1)); + name = name.substr(0, dotPos); + } + + return ResourcePathData{ + util::utf8ToUtf16(dirStr), + util::utf8ToUtf16(name), + extension.toString(), + config + }; +} + +bool writeResourceTable(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, + const TableFlattener::Options& flattenerOptions, ZipFile* outApk) { + if (table->begin() != table->end()) { + BigBuffer buffer(1024); + TableFlattener flattener(flattenerOptions); + if (!flattener.flatten(&buffer, *table)) { + Logger::error() << "failed to flatten resource table." << std::endl; + return false; + } + + if (options.verbose) { + Logger::note() << "Final resource table size=" << util::formatSize(buffer.size()) + << std::endl; + } + + if (outApk->add(buffer, "resources.arsc", ZipEntry::kCompressStored, nullptr) != + android::NO_ERROR) { + Logger::note(options.output) << "failed to store resource table." << std::endl; + return false; + } + } + return true; +} + +/** + * For each FileReference in the table, adds a LinkItem to the link queue for processing. + */ +static void addApkFilesToLinkQueue(const std::u16string& package, const Source& source, + const std::shared_ptr<ResourceTable>& table, + const std::unique_ptr<ZipFile>& apk, + std::queue<LinkItem>* outLinkQueue) { + bool mangle = package != table->getPackage(); + for (auto& type : *table) { + for (auto& entry : type->entries) { + ResourceName name = { package, type->type, entry->name }; + if (mangle) { + NameMangler::mangle(table->getPackage(), &name.entry); + } + + for (auto& value : entry->values) { + visitFunc<FileReference>(*value.value, [&](FileReference& ref) { + std::string pathUtf8 = util::utf16ToUtf8(*ref.path); + Source newSource = source; + newSource.path += "/"; + newSource.path += pathUtf8; + outLinkQueue->push(LinkItem{ + name, value.config, newSource, pathUtf8, apk.get(), + table->getPackage() }); + // Now rewrite the file path. + if (mangle) { + ref.path = table->getValueStringPool().makeRef(util::utf8ToUtf16( + buildFileReference(name, value.config, + getExtension<char>(pathUtf8)))); + } + }); + } + } + } +} + +static constexpr int kOpenFlags = ZipFile::kOpenCreate | ZipFile::kOpenTruncate | + ZipFile::kOpenReadWrite; + +bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outTable, + const std::shared_ptr<IResolver>& resolver) { + std::map<std::shared_ptr<ResourceTable>, StaticLibraryData> apkFiles; + std::unordered_set<std::u16string> linkedPackages; + + // Populate the linkedPackages with our own. + linkedPackages.insert(options.appInfo.package); + + // Load all APK files. + for (const Source& source : options.input) { + std::unique_ptr<ZipFile> zipFile = util::make_unique<ZipFile>(); + if (zipFile->open(source.path.data(), ZipFile::kOpenReadOnly) != android::NO_ERROR) { + Logger::error(source) << "failed to open: " << strerror(errno) << std::endl; + return false; + } + + std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); + + ZipEntry* entry = zipFile->getEntryByName("resources.arsc"); + if (!entry) { + Logger::error(source) << "missing 'resources.arsc'." << std::endl; + return false; + } + + std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( + zipFile->uncompress(entry)); + assert(uncompressedData); + + BinaryResourceParser parser(table, resolver, source, uncompressedData.get(), + entry->getUncompressedLen()); + if (!parser.parse()) { + return false; + } + + // Keep track of where this table came from. + apkFiles[table] = StaticLibraryData{ source, std::move(zipFile) }; + + // Add the package to the set of linked packages. + linkedPackages.insert(table->getPackage()); + } + + std::queue<LinkItem> linkQueue; + for (auto& p : apkFiles) { + const std::shared_ptr<ResourceTable>& inTable = p.first; + + // Collect all FileReferences and add them to the queue for processing. + addApkFilesToLinkQueue(options.appInfo.package, p.second.source, inTable, p.second.apk, + &linkQueue); + + // Merge the tables. + if (!outTable->merge(std::move(*inTable))) { + return false; + } + } + + // Version all styles referencing attributes outside of their specified SDK version. + if (options.versionStylesAndLayouts) { + versionStylesForCompat(outTable); + } + + { + // Now that everything is merged, let's link it. + Linker::Options linkerOptions; + if (options.packageType == AaptOptions::PackageType::StaticLibrary) { + linkerOptions.linkResourceIds = false; + } + Linker linker(outTable, resolver, linkerOptions); + if (!linker.linkAndValidate()) { + return false; + } + + // Verify that all symbols exist. + const auto& unresolvedRefs = linker.getUnresolvedReferences(); + if (!unresolvedRefs.empty()) { + for (const auto& entry : unresolvedRefs) { + for (const auto& source : entry.second) { + Logger::error(source) << "unresolved symbol '" << entry.first << "'." + << std::endl; + } + } + return false; + } + } + + // Open the output APK file for writing. + ZipFile outApk; + if (outApk.open(options.output.path.data(), kOpenFlags) != android::NO_ERROR) { + Logger::error(options.output) << "failed to open: " << strerror(errno) << std::endl; + return false; + } + + proguard::KeepSet keepSet; + + android::ResTable binTable; + if (!compileManifest(options, resolver, apkFiles, binTable, &outApk, &keepSet)) { + return false; + } + + for (; !linkQueue.empty(); linkQueue.pop()) { + const LinkItem& item = linkQueue.front(); + + assert(!item.originalPackage.empty()); + ZipEntry* entry = item.apk->getEntryByName(item.originalPath.data()); + if (!entry) { + Logger::error(item.source) << "failed to find '" << item.originalPath << "'." + << std::endl; + return false; + } + + if (util::stringEndsWith<char>(item.originalPath, ".xml")) { + void* uncompressedData = item.apk->uncompress(entry); + assert(uncompressedData); + + if (!linkXml(options, outTable, resolver, item, uncompressedData, + entry->getUncompressedLen(), &outApk, &linkQueue, &keepSet)) { + Logger::error(options.output) << "failed to link '" << item.originalPath << "'." + << std::endl; + return false; + } + } else { + if (outApk.add(item.apk, entry, buildFileReference(item).data(), 0, nullptr) != + android::NO_ERROR) { + Logger::error(options.output) << "failed to copy '" << item.originalPath << "'." + << std::endl; + return false; + } + } + } + + // Generate the Java class file. + if (options.generateJavaClass) { + JavaClassGenerator::Options javaOptions; + if (options.packageType == AaptOptions::PackageType::StaticLibrary) { + javaOptions.useFinal = false; + } + JavaClassGenerator generator(outTable, javaOptions); + + for (const std::u16string& package : linkedPackages) { + Source outPath = options.generateJavaClass.value(); + + // Build the output directory from the package name. + // Eg. com.android.app -> com/android/app + const std::string packageUtf8 = util::utf16ToUtf8(package); + for (StringPiece part : util::tokenize<char>(packageUtf8, '.')) { + appendPath(&outPath.path, part); + } + + if (!mkdirs(outPath.path)) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + appendPath(&outPath.path, "R.java"); + + if (options.verbose) { + Logger::note(outPath) << "writing Java symbols." << std::endl; + } + + std::ofstream fout(outPath.path); + if (!fout) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + if (!generator.generate(package, fout)) { + Logger::error(outPath) << generator.getError() << "." << std::endl; + return false; + } + } + } + + // Generate the Proguard rules file. + if (options.generateProguardRules) { + const Source& outPath = options.generateProguardRules.value(); + + if (options.verbose) { + Logger::note(outPath) << "writing proguard rules." << std::endl; + } + + std::ofstream fout(outPath.path); + if (!fout) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + if (!proguard::writeKeepSet(&fout, keepSet)) { + Logger::error(outPath) << "failed to write proguard rules." << std::endl; + return false; + } + } + + outTable->getValueStringPool().prune(); + outTable->getValueStringPool().sort( + [](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + if (a.context.priority < b.context.priority) { + return true; + } + + if (a.context.priority > b.context.priority) { + return false; + } + return a.value < b.value; + }); + + + // Flatten the resource table. + TableFlattener::Options flattenerOptions; + if (options.packageType != AaptOptions::PackageType::StaticLibrary) { + flattenerOptions.useExtendedChunks = false; + } + + if (!writeResourceTable(options, outTable, flattenerOptions, &outApk)) { + return false; + } + + outApk.flush(); + return true; +} + +bool compile(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<IResolver>& resolver) { + std::queue<CompileItem> compileQueue; + bool error = false; + + // Compile all the resource files passed in on the command line. + for (const Source& source : options.input) { + // Need to parse the resource type/config/filename. + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } + + const ResourcePathData& pathData = maybePathData.value(); + if (pathData.resourceDir == u"values") { + // The file is in the values directory, which means its contents will + // go into the resource table. + if (options.verbose) { + Logger::note(source) << "compiling values." << std::endl; + } + + error |= !compileValues(table, source, pathData.config); + } else { + // The file is in a directory like 'layout' or 'drawable'. Find out + // the type. + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) << "invalid resource type '" << pathData.resourceDir << "'." + << std::endl; + return false; + } + + compileQueue.push(CompileItem{ + ResourceName{ table->getPackage(), *type, pathData.name }, + pathData.config, + source, + pathData.extension + }); + } + } + + if (error) { + return false; + } + // Open the output APK file for writing. + ZipFile outApk; + if (outApk.open(options.output.path.data(), kOpenFlags) != android::NO_ERROR) { + Logger::error(options.output) << "failed to open: " << strerror(errno) << std::endl; + return false; + } + + // Compile each file. + for (; !compileQueue.empty(); compileQueue.pop()) { + const CompileItem& item = compileQueue.front(); + + // Add the file name to the resource table. + error |= !addFileReference(table, item); + + if (item.extension == "xml") { + error |= !compileXml(options, table, item, &outApk); + } else if (item.extension == "png" || item.extension == "9.png") { + error |= !compilePng(options, item, &outApk); + } else { + error |= !copyFile(options, item, &outApk); + } + } + + if (error) { + return false; + } + + // Link and assign resource IDs. + Linker linker(table, resolver, {}); + if (!linker.linkAndValidate()) { + return false; + } + + // Flatten the resource table. + if (!writeResourceTable(options, table, {}, &outApk)) { + return false; + } + + outApk.flush(); + return true; +} + +bool loadAppInfo(const Source& source, AppInfo* outInfo) { + std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); + if (!ifs) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + ManifestParser parser; + std::shared_ptr<XmlPullParser> pullParser = std::make_shared<SourceXmlPullParser>(ifs); + return parser.parse(source, pullParser, outInfo); +} + +static void printCommandsAndDie() { + std::cerr << "The following commands are supported:" << std::endl << std::endl; + std::cerr << "compile compiles a subset of resources" << std::endl; + std::cerr << "link links together compiled resources and libraries" << std::endl; + std::cerr << "dump dumps resource contents to to standard out" << std::endl; + std::cerr << std::endl; + std::cerr << "run aapt2 with one of the commands and the -h flag for extra details." + << std::endl; + exit(1); +} + +static AaptOptions prepareArgs(int argc, char** argv) { + if (argc < 2) { + std::cerr << "no command specified." << std::endl << std::endl; + printCommandsAndDie(); + } + + const StringPiece command(argv[1]); + argc -= 2; + argv += 2; + + AaptOptions options; + + if (command == "--version" || command == "version") { + std::cout << kAaptVersionStr << std::endl; + exit(0); + } else if (command == "link") { + options.phase = AaptOptions::Phase::Link; + } else if (command == "compile") { + options.phase = AaptOptions::Phase::Compile; + } else if (command == "dump") { + options.phase = AaptOptions::Phase::Dump; + } else if (command == "dump-style-graph") { + options.phase = AaptOptions::Phase::DumpStyleGraph; + } else { + std::cerr << "invalid command '" << command << "'." << std::endl << std::endl; + printCommandsAndDie(); + } + + bool isStaticLib = false; + if (options.phase == AaptOptions::Phase::Compile || + options.phase == AaptOptions::Phase::Link) { + if (options.phase == AaptOptions::Phase::Compile) { + flag::requiredFlag("--package", "Android package name", + [&options](const StringPiece& arg) { + options.appInfo.package = util::utf8ToUtf16(arg); + }); + } else if (options.phase == AaptOptions::Phase::Link) { + flag::requiredFlag("--manifest", "AndroidManifest.xml of your app", + [&options](const StringPiece& arg) { + options.manifest = Source{ arg.toString() }; + }); + + flag::optionalFlag("-I", "add an Android APK to link against", + [&options](const StringPiece& arg) { + options.libraries.push_back(Source{ arg.toString() }); + }); + + flag::optionalFlag("--java", "directory in which to generate R.java", + [&options](const StringPiece& arg) { + options.generateJavaClass = Source{ arg.toString() }; + }); + + flag::optionalFlag("--proguard", "file in which to output proguard rules", + [&options](const StringPiece& arg) { + options.generateProguardRules = Source{ arg.toString() }; + }); + + flag::optionalSwitch("--static-lib", "generate a static Android library", true, + &isStaticLib); + + flag::optionalFlag("--binding", "Output directory for binding XML files", + [&options](const StringPiece& arg) { + options.bindingOutput = Source{ arg.toString() }; + }); + flag::optionalSwitch("--no-version", "Disables automatic style and layout versioning", + false, &options.versionStylesAndLayouts); + } + + // Common flags for all steps. + flag::requiredFlag("-o", "Output path", [&options](const StringPiece& arg) { + options.output = Source{ arg.toString() }; + }); + } else if (options.phase == AaptOptions::Phase::DumpStyleGraph) { + flag::requiredFlag("--style", "Name of the style to dump", + [&options](const StringPiece& arg, std::string* outError) -> bool { + Reference styleReference; + if (!ResourceParser::parseStyleParentReference(util::utf8ToUtf16(arg), + &styleReference, outError)) { + return false; + } + options.dumpStyleTarget = styleReference.name; + return true; + }); + } + + bool help = false; + flag::optionalSwitch("-v", "enables verbose logging", true, &options.verbose); + flag::optionalSwitch("-h", "displays this help menu", true, &help); + + // Build the command string for output (eg. "aapt2 compile"). + std::string fullCommand = "aapt2"; + fullCommand += " "; + fullCommand += command.toString(); + + // Actually read the command line flags. + flag::parse(argc, argv, fullCommand); + + if (help) { + flag::usageAndDie(fullCommand); + } + + if (isStaticLib) { + options.packageType = AaptOptions::PackageType::StaticLibrary; + } + + // Copy all the remaining arguments. + for (const std::string& arg : flag::getArgs()) { + options.input.push_back(Source{ arg }); + } + return options; +} + +static bool doDump(const AaptOptions& options) { + for (const Source& source : options.input) { + std::unique_ptr<ZipFile> zipFile = util::make_unique<ZipFile>(); + if (zipFile->open(source.path.data(), ZipFile::kOpenReadOnly) != android::NO_ERROR) { + Logger::error(source) << "failed to open: " << strerror(errno) << std::endl; + return false; + } + + std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); + std::shared_ptr<ResourceTableResolver> resolver = + std::make_shared<ResourceTableResolver>( + table, std::vector<std::shared_ptr<const android::AssetManager>>()); + + ZipEntry* entry = zipFile->getEntryByName("resources.arsc"); + if (!entry) { + Logger::error(source) << "missing 'resources.arsc'." << std::endl; + return false; + } + + std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( + zipFile->uncompress(entry)); + assert(uncompressedData); + + BinaryResourceParser parser(table, resolver, source, uncompressedData.get(), + entry->getUncompressedLen()); + if (!parser.parse()) { + return false; + } + + if (options.phase == AaptOptions::Phase::Dump) { + Debug::printTable(table); + } else if (options.phase == AaptOptions::Phase::DumpStyleGraph) { + Debug::printStyleGraph(table, options.dumpStyleTarget); + } + } + return true; +} + +int main(int argc, char** argv) { + Logger::setLog(std::make_shared<Log>(std::cerr, std::cerr)); + AaptOptions options = prepareArgs(argc, argv); + + if (options.phase == AaptOptions::Phase::Dump || + options.phase == AaptOptions::Phase::DumpStyleGraph) { + if (!doDump(options)) { + return 1; + } + return 0; + } + + // If we specified a manifest, go ahead and load the package name from the manifest. + if (!options.manifest.path.empty()) { + if (!loadAppInfo(options.manifest, &options.appInfo)) { + return false; + } + } + + // Verify we have some common options set. + if (options.appInfo.package.empty()) { + Logger::error() << "no package name specified." << std::endl; + return false; + } + + // Every phase needs a resource table. + std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); + table->setPackage(options.appInfo.package); + if (options.appInfo.package == u"android") { + table->setPackageId(0x01); + } else { + table->setPackageId(0x7f); + } + + // Load the included libraries. + std::vector<std::shared_ptr<const android::AssetManager>> sources; + for (const Source& source : options.libraries) { + std::shared_ptr<android::AssetManager> assetManager = + std::make_shared<android::AssetManager>(); + int32_t cookie; + if (!assetManager->addAssetPath(android::String8(source.path.data()), &cookie)) { + Logger::error(source) << "failed to load library." << std::endl; + return false; + } + + if (cookie == 0) { + Logger::error(source) << "failed to load library." << std::endl; + return false; + } + sources.push_back(assetManager); + } + + // Make the resolver that will cache IDs for us. + std::shared_ptr<ResourceTableResolver> resolver = std::make_shared<ResourceTableResolver>( + table, sources); + + if (options.phase == AaptOptions::Phase::Compile) { + if (!compile(options, table, resolver)) { + Logger::error() << "aapt exiting with failures." << std::endl; + return 1; + } + } else if (options.phase == AaptOptions::Phase::Link) { + if (!link(options, table, resolver)) { + Logger::error() << "aapt exiting with failures." << std::endl; + return 1; + } + } + return 0; +} diff --git a/tools/aapt2/ManifestMerger.cpp b/tools/aapt2/ManifestMerger.cpp new file mode 100644 index 0000000..71d3424 --- /dev/null +++ b/tools/aapt2/ManifestMerger.cpp @@ -0,0 +1,376 @@ +#include "ManifestMerger.h" +#include "Maybe.h" +#include "ResourceParser.h" +#include "Source.h" +#include "Util.h" +#include "XmlPullParser.h" + +#include <iostream> +#include <memory> +#include <set> +#include <string> + +namespace aapt { + +constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; + +static xml::Element* findManifest(xml::Node* root) { + if (!root) { + return nullptr; + } + + while (root->type == xml::NodeType::kNamespace) { + if (root->children.empty()) { + break; + } + root = root->children[0].get(); + } + + if (root && root->type == xml::NodeType::kElement) { + xml::Element* el = static_cast<xml::Element*>(root); + if (el->namespaceUri.empty() && el->name == u"manifest") { + return el; + } + } + return nullptr; +} + +static xml::Element* findChildWithSameName(xml::Element* parent, xml::Element* src) { + xml::Attribute* attrKey = src->findAttribute(kSchemaAndroid, u"name"); + if (!attrKey) { + return nullptr; + } + return parent->findChildWithAttribute(src->namespaceUri, src->name, attrKey); +} + +static bool attrLess(const xml::Attribute& lhs, const xml::Attribute& rhs) { + return std::tie(lhs.namespaceUri, lhs.name, lhs.value) + < std::tie(rhs.namespaceUri, rhs.name, rhs.value); +} + +static int compare(xml::Element* lhs, xml::Element* rhs) { + int diff = lhs->attributes.size() - rhs->attributes.size(); + if (diff != 0) { + return diff; + } + + std::set<xml::Attribute, decltype(&attrLess)> lhsAttrs(&attrLess); + lhsAttrs.insert(lhs->attributes.begin(), lhs->attributes.end()); + for (auto& attr : rhs->attributes) { + if (lhsAttrs.erase(attr) == 0) { + // The rhs attribute is not in the left. + return -1; + } + } + + if (!lhsAttrs.empty()) { + // The lhs has attributes not in the rhs. + return 1; + } + return 0; +} + +ManifestMerger::ManifestMerger(const Options& options) : + mOptions(options), mAppLogger({}), mLogger({}) { +} + +bool ManifestMerger::setAppManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> root) { + + mAppLogger = SourceLogger{ source }; + mRoot = std::move(root); + return true; +} + +bool ManifestMerger::checkEqual(xml::Element* elA, xml::Element* elB) { + if (compare(elA, elB) != 0) { + mLogger.error(elB->lineNumber) + << "library tag '" << elB->name << "' conflicts with app tag." + << std::endl; + mAppLogger.note(elA->lineNumber) + << "app tag '" << elA->name << "' defined here." + << std::endl; + return false; + } + + std::vector<xml::Element*> childrenA = elA->getChildElements(); + std::vector<xml::Element*> childrenB = elB->getChildElements(); + + if (childrenA.size() != childrenB.size()) { + mLogger.error(elB->lineNumber) + << "library tag '" << elB->name << "' children conflict with app tag." + << std::endl; + mAppLogger.note(elA->lineNumber) + << "app tag '" << elA->name << "' defined here." + << std::endl; + return false; + } + + auto cmp = [](xml::Element* lhs, xml::Element* rhs) -> bool { + return compare(lhs, rhs) < 0; + }; + + std::sort(childrenA.begin(), childrenA.end(), cmp); + std::sort(childrenB.begin(), childrenB.end(), cmp); + + for (size_t i = 0; i < childrenA.size(); i++) { + if (!checkEqual(childrenA[i], childrenB[i])) { + return false; + } + } + return true; +} + +bool ManifestMerger::mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB) { + if (!elA) { + parentA->addChild(elB->clone()); + return true; + } + return checkEqual(elA, elB); +} + +bool ManifestMerger::mergePreferRequired(xml::Element* parentA, xml::Element* elA, + xml::Element* elB) { + if (!elA) { + parentA->addChild(elB->clone()); + return true; + } + + xml::Attribute* reqA = elA->findAttribute(kSchemaAndroid, u"required"); + xml::Attribute* reqB = elB->findAttribute(kSchemaAndroid, u"required"); + bool requiredA = !reqA || (reqA->value != u"false" && reqA->value != u"FALSE"); + bool requiredB = !reqB || (reqB->value != u"false" && reqB->value != u"FALSE"); + if (!requiredA && requiredB) { + if (reqA) { + *reqA = xml::Attribute{ kSchemaAndroid, u"required", u"true" }; + } else { + elA->attributes.push_back(xml::Attribute{ kSchemaAndroid, u"required", u"true" }); + } + } + return true; +} + +static int findIntegerValue(xml::Attribute* attr, int defaultValue) { + if (attr) { + std::unique_ptr<BinaryPrimitive> integer = ResourceParser::tryParseInt(attr->value); + if (integer) { + return integer->value.data; + } + } + return defaultValue; +} + +bool ManifestMerger::mergeUsesSdk(xml::Element* elA, xml::Element* elB) { + bool error = false; + xml::Attribute* minAttrA = nullptr; + xml::Attribute* minAttrB = nullptr; + if (elA) { + minAttrA = elA->findAttribute(kSchemaAndroid, u"minSdkVersion"); + } + + if (elB) { + minAttrB = elB->findAttribute(kSchemaAndroid, u"minSdkVersion"); + } + + int minSdkA = findIntegerValue(minAttrA, 1); + int minSdkB = findIntegerValue(minAttrB, 1); + + if (minSdkA < minSdkB) { + std::ostream* out; + if (minAttrA) { + out = &(mAppLogger.error(elA->lineNumber) << "app declares "); + } else if (elA) { + out = &(mAppLogger.error(elA->lineNumber) << "app has implied "); + } else { + out = &(mAppLogger.error() << "app has implied "); + } + + *out << "minSdkVersion=" << minSdkA << " but library expects a higher SDK version." + << std::endl; + + // elB is valid because minSdkB wouldn't be greater than minSdkA if it wasn't. + mLogger.note(elB->lineNumber) + << "library declares minSdkVersion=" << minSdkB << "." + << std::endl; + error = true; + } + + xml::Attribute* targetAttrA = nullptr; + xml::Attribute* targetAttrB = nullptr; + + if (elA) { + targetAttrA = elA->findAttribute(kSchemaAndroid, u"targetSdkVersion"); + } + + if (elB) { + targetAttrB = elB->findAttribute(kSchemaAndroid, u"targetSdkVersion"); + } + + int targetSdkA = findIntegerValue(targetAttrA, minSdkA); + int targetSdkB = findIntegerValue(targetAttrB, minSdkB); + + if (targetSdkA < targetSdkB) { + std::ostream* out; + if (targetAttrA) { + out = &(mAppLogger.warn(elA->lineNumber) << "app declares "); + } else if (elA) { + out = &(mAppLogger.warn(elA->lineNumber) << "app has implied "); + } else { + out = &(mAppLogger.warn() << "app has implied "); + } + + *out << "targetSdkVerion=" << targetSdkA << " but library expects target SDK " + << targetSdkB << "." << std::endl; + + mLogger.note(elB->lineNumber) + << "library declares targetSdkVersion=" << targetSdkB << "." + << std::endl; + error = true; + } + return !error; +} + +bool ManifestMerger::mergeApplication(xml::Element* applicationA, xml::Element* applicationB) { + if (!applicationA || !applicationB) { + return true; + } + + bool error = false; + + // First make sure that the names are identical. + xml::Attribute* nameA = applicationA->findAttribute(kSchemaAndroid, u"name"); + xml::Attribute* nameB = applicationB->findAttribute(kSchemaAndroid, u"name"); + if (nameB) { + if (!nameA) { + applicationA->attributes.push_back(*nameB); + } else if (nameA->value != nameB->value) { + mLogger.error(applicationB->lineNumber) + << "conflicting application name '" + << nameB->value + << "'." << std::endl; + mAppLogger.note(applicationA->lineNumber) + << "application defines application name '" + << nameA->value + << "'." << std::endl; + error = true; + } + } + + // Now we descend into the activity/receiver/service/provider tags + for (xml::Element* elB : applicationB->getChildElements()) { + if (!elB->namespaceUri.empty()) { + continue; + } + + if (elB->name == u"activity" || elB->name == u"activity-alias" + || elB->name == u"service" || elB->name == u"receiver" + || elB->name == u"provider" || elB->name == u"meta-data") { + xml::Element* elA = findChildWithSameName(applicationA, elB); + error |= !mergeNewOrEqual(applicationA, elA, elB); + } else if (elB->name == u"uses-library") { + xml::Element* elA = findChildWithSameName(applicationA, elB); + error |= !mergePreferRequired(applicationA, elA, elB); + } + } + return !error; +} + +bool ManifestMerger::mergeLibraryManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> libRoot) { + mLogger = SourceLogger{ source }; + xml::Element* manifestA = findManifest(mRoot.get()); + xml::Element* manifestB = findManifest(libRoot.get()); + if (!manifestA) { + mAppLogger.error() << "missing manifest tag." << std::endl; + return false; + } + + if (!manifestB) { + mLogger.error() << "library missing manifest tag." << std::endl; + return false; + } + + bool error = false; + + // Do <application> first. + xml::Element* applicationA = manifestA->findChild({}, u"application"); + xml::Element* applicationB = manifestB->findChild({}, u"application"); + error |= !mergeApplication(applicationA, applicationB); + + // Do <uses-sdk> next. + xml::Element* usesSdkA = manifestA->findChild({}, u"uses-sdk"); + xml::Element* usesSdkB = manifestB->findChild({}, u"uses-sdk"); + error |= !mergeUsesSdk(usesSdkA, usesSdkB); + + for (xml::Element* elB : manifestB->getChildElements()) { + if (!elB->namespaceUri.empty()) { + continue; + } + + if (elB->name == u"uses-permission" || elB->name == u"permission" + || elB->name == u"permission-group" || elB->name == u"permission-tree") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !mergeNewOrEqual(manifestA, elA, elB); + } else if (elB->name == u"uses-feature") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !mergePreferRequired(manifestA, elA, elB); + } else if (elB->name == u"uses-configuration" || elB->name == u"supports-screen" + || elB->name == u"compatible-screens" || elB->name == u"supports-gl-texture") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !checkEqual(elA, elB); + } + } + return !error; +} + +static void printMerged(xml::Node* node, int depth) { + std::string indent; + for (int i = 0; i < depth; i++) { + indent += " "; + } + + switch (node->type) { + case xml::NodeType::kNamespace: + std::cerr << indent << "N: " + << "xmlns:" << static_cast<xml::Namespace*>(node)->namespacePrefix + << "=\"" << static_cast<xml::Namespace*>(node)->namespaceUri + << "\"\n"; + break; + + case xml::NodeType::kElement: + std::cerr << indent << "E: " + << static_cast<xml::Element*>(node)->namespaceUri + << ":" << static_cast<xml::Element*>(node)->name + << "\n"; + for (const auto& attr : static_cast<xml::Element*>(node)->attributes) { + std::cerr << indent << " A: " + << attr.namespaceUri + << ":" << attr.name + << "=\"" << attr.value << "\"\n"; + } + break; + + case xml::NodeType::kText: + std::cerr << indent << "T: \"" << static_cast<xml::Text*>(node)->text << "\"\n"; + break; + } + + for (auto& child : node->children) { + printMerged(child.get(), depth + 1); + } +} + +xml::Node* ManifestMerger::getMergedXml() { + return mRoot.get(); +} + +bool ManifestMerger::printMerged() { + if (!mRoot) { + return false; + } + + ::aapt::printMerged(mRoot.get(), 0); + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestMerger.h b/tools/aapt2/ManifestMerger.h new file mode 100644 index 0000000..c6219db --- /dev/null +++ b/tools/aapt2/ManifestMerger.h @@ -0,0 +1,45 @@ +#ifndef AAPT_MANIFEST_MERGER_H +#define AAPT_MANIFEST_MERGER_H + +#include "Logger.h" +#include "Source.h" +#include "XmlDom.h" + +#include <memory> +#include <string> + +namespace aapt { + +class ManifestMerger { +public: + struct Options { + }; + + ManifestMerger(const Options& options); + + bool setAppManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> root); + + bool mergeLibraryManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> libRoot); + + xml::Node* getMergedXml(); + + bool printMerged(); + +private: + bool mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB); + bool mergePreferRequired(xml::Element* parentA, xml::Element* elA, xml::Element* elB); + bool checkEqual(xml::Element* elA, xml::Element* elB); + bool mergeApplication(xml::Element* applicationA, xml::Element* applicationB); + bool mergeUsesSdk(xml::Element* elA, xml::Element* elB); + + Options mOptions; + std::unique_ptr<xml::Node> mRoot; + SourceLogger mAppLogger; + SourceLogger mLogger; +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_MERGER_H diff --git a/tools/aapt2/ManifestMerger_test.cpp b/tools/aapt2/ManifestMerger_test.cpp new file mode 100644 index 0000000..6838253 --- /dev/null +++ b/tools/aapt2/ManifestMerger_test.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ManifestMerger.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kAppManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="21" /> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-feature android:name="android.hardware.GPS" android:required="false" /> + <application android:name="com.android.library.Application"> + <activity android:name="com.android.example.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC" /> + </intent-filter> + </service> + </application> +</manifest> +)EOF"; + +constexpr const char* kLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="21" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-feature android:name="android.hardware.GPS" /> + <uses-permission android:name="android.permission.GPS" /> + <application android:name="com.android.library.Application"> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC" /> + </intent-filter> + </service> + <provider android:name="com.android.library.DocumentProvider" + android:authorities="com.android.library.documents" + android:grantUriPermission="true" + android:exported="true" + android:permission="android.permission.MANAGE_DOCUMENTS" + android:enabled="@bool/atLeastKitKat"> + <intent-filter> + <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> + </intent-filter> + </provider> + </application> +</manifest> +)EOF"; + +constexpr const char* kBadLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="22" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-feature android:name="android.hardware.GPS" /> + <uses-permission android:name="android.permission.GPS" /> + <application android:name="com.android.library.Application2"> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC_ACTION" /> + </intent-filter> + </service> + </application> +</manifest> +)EOF"; + +TEST(ManifestMergerTest, MergeManifestsSuccess) { + std::stringstream inA(kAppManifest); + std::stringstream inB(kLibManifest); + + const Source sourceA = { "AndroidManifest.xml" }; + const Source sourceB = { "lib.apk/AndroidManifest.xml" }; + SourceLogger loggerA(sourceA); + SourceLogger loggerB(sourceB); + + ManifestMerger merger({}); + EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", + xml::inflate(&inA, &loggerA))); + EXPECT_TRUE(merger.mergeLibraryManifest(sourceB, u"com.android.library", + xml::inflate(&inB, &loggerB))); +} + +TEST(ManifestMergerTest, MergeManifestFail) { + std::stringstream inA(kAppManifest); + std::stringstream inB(kBadLibManifest); + + const Source sourceA = { "AndroidManifest.xml" }; + const Source sourceB = { "lib.apk/AndroidManifest.xml" }; + SourceLogger loggerA(sourceA); + SourceLogger loggerB(sourceB); + + ManifestMerger merger({}); + EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", + xml::inflate(&inA, &loggerA))); + EXPECT_FALSE(merger.mergeLibraryManifest(sourceB, u"com.android.library", + xml::inflate(&inB, &loggerB))); +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestParser.cpp b/tools/aapt2/ManifestParser.cpp new file mode 100644 index 0000000..b8f0a43 --- /dev/null +++ b/tools/aapt2/ManifestParser.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "AppInfo.h" +#include "Logger.h" +#include "ManifestParser.h" +#include "Source.h" +#include "XmlPullParser.h" + +#include <string> + +namespace aapt { + +bool ManifestParser::parse(const Source& source, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo) { + SourceLogger logger = { source }; + + int depth = 0; + while (XmlPullParser::isGoodEvent(parser->next())) { + XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kEndElement) { + depth--; + continue; + } else if (event != XmlPullParser::Event::kStartElement) { + continue; + } + + depth++; + + const std::u16string& element = parser->getElementName(); + if (depth == 1) { + if (element == u"manifest") { + if (!parseManifest(logger, parser, outInfo)) { + return false; + } + } else { + logger.error() + << "unexpected top-level element '" + << element + << "'." + << std::endl; + return false; + } + } else { + XmlPullParser::skipCurrentElement(parser.get()); + } + } + + if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { + logger.error(parser->getLineNumber()) + << "failed to parse manifest: " + << parser->getLastError() + << "." + << std::endl; + return false; + } + return true; +} + +bool ManifestParser::parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo) { + auto attrIter = parser->findAttribute(u"", u"package"); + if (attrIter == parser->endAttributes() || attrIter->value.empty()) { + logger.error() << "no 'package' attribute found for element <manifest>." << std::endl; + return false; + } + outInfo->package = attrIter->value; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestParser.h b/tools/aapt2/ManifestParser.h new file mode 100644 index 0000000..f2e43d4 --- /dev/null +++ b/tools/aapt2/ManifestParser.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_MANIFEST_PARSER_H +#define AAPT_MANIFEST_PARSER_H + +#include "AppInfo.h" +#include "Logger.h" +#include "Source.h" +#include "XmlPullParser.h" + +namespace aapt { + +/* + * Parses an AndroidManifest.xml file and fills in an AppInfo structure with + * app data. + */ +class ManifestParser { +public: + ManifestParser() = default; + ManifestParser(const ManifestParser&) = delete; + + bool parse(const Source& source, std::shared_ptr<XmlPullParser> parser, AppInfo* outInfo); + +private: + bool parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo); +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_PARSER_H diff --git a/tools/aapt2/ManifestParser_test.cpp b/tools/aapt2/ManifestParser_test.cpp new file mode 100644 index 0000000..be3a6fb --- /dev/null +++ b/tools/aapt2/ManifestParser_test.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "AppInfo.h" +#include "ManifestParser.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(ManifestParserTest, FindPackage) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + "package=\"android\">\n" + "</manifest>\n"; + + ManifestParser parser; + AppInfo info; + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(input); + ASSERT_TRUE(parser.parse(Source{ "AndroidManifest.xml" }, xmlParser, &info)); + + EXPECT_EQ(std::u16string(u"android"), info.package); +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.cpp b/tools/aapt2/ManifestValidator.cpp new file mode 100644 index 0000000..123b9fa --- /dev/null +++ b/tools/aapt2/ManifestValidator.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Logger.h" +#include "ManifestValidator.h" +#include "Maybe.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +ManifestValidator::ManifestValidator(const android::ResTable& table) +: mTable(table) { +} + +bool ManifestValidator::validate(const Source& source, android::ResXMLParser* parser) { + SourceLogger logger(source); + + android::ResXMLParser::event_code_t code; + while ((code = parser->next()) != android::ResXMLParser::END_DOCUMENT && + code != android::ResXMLParser::BAD_DOCUMENT) { + if (code != android::ResXMLParser::START_TAG) { + continue; + } + + size_t len = 0; + const StringPiece16 namespaceUri(parser->getElementNamespace(&len), len); + if (!namespaceUri.empty()) { + continue; + } + + const StringPiece16 name(parser->getElementName(&len), len); + if (name.empty()) { + logger.error(parser->getLineNumber()) + << "failed to get the element name." + << std::endl; + return false; + } + + if (name == u"manifest") { + if (!validateManifest(source, parser)) { + return false; + } + } + } + return true; +} + +Maybe<StringPiece16> ManifestValidator::getAttributeValue(android::ResXMLParser* parser, + size_t idx) { + android::Res_value value; + if (parser->getAttributeValue(idx, &value) < 0) { + return StringPiece16(); + } + + const android::ResStringPool* pool = &parser->getStrings(); + if (value.dataType == android::Res_value::TYPE_REFERENCE) { + ssize_t strIdx = mTable.resolveReference(&value, 0x10000000u); + if (strIdx < 0) { + return {}; + } + pool = mTable.getTableStringBlock(strIdx); + } + + if (value.dataType != android::Res_value::TYPE_STRING || !pool) { + return {}; + } + return util::getString(*pool, value.data); +} + +Maybe<StringPiece16> ManifestValidator::getAttributeInlineValue(android::ResXMLParser* parser, + size_t idx) { + android::Res_value value; + if (parser->getAttributeValue(idx, &value) < 0) { + return StringPiece16(); + } + + if (value.dataType != android::Res_value::TYPE_STRING) { + return {}; + } + return util::getString(parser->getStrings(), value.data); +} + +bool ManifestValidator::validateInlineAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, + const StringPiece16& charSet) { + size_t len = 0; + StringPiece16 element(parser->getElementName(&len), len); + StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); + Maybe<StringPiece16> result = getAttributeInlineValue(parser, idx); + if (!result) { + logger.error(parser->getLineNumber()) + << "<" + << element + << "> must have a '" + << attributeName + << "' attribute with a string literal value." + << std::endl; + return false; + } + return validateAttributeImpl(element, attributeName, result.value(), charSet, + parser->getLineNumber(), logger); +} + +bool ManifestValidator::validateAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, const StringPiece16& charSet) { + size_t len = 0; + StringPiece16 element(parser->getElementName(&len), len); + StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); + Maybe<StringPiece16> result = getAttributeValue(parser, idx); + if (!result) { + logger.error(parser->getLineNumber()) + << "<" + << element + << "> must have a '" + << attributeName + << "' attribute that points to a string." + << std::endl; + return false; + } + return validateAttributeImpl(element, attributeName, result.value(), charSet, + parser->getLineNumber(), logger); +} + +bool ManifestValidator::validateAttributeImpl(const StringPiece16& element, + const StringPiece16& attributeName, + const StringPiece16& attributeValue, + const StringPiece16& charSet, size_t lineNumber, + SourceLogger& logger) { + StringPiece16::const_iterator badIter = + util::findNonAlphaNumericAndNotInSet(attributeValue, charSet); + if (badIter != attributeValue.end()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' has invalid character '" + << StringPiece16(badIter, 1) + << "'." + << std::endl; + return false; + } + + if (!attributeValue.empty()) { + StringPiece16 trimmed = util::trimWhitespace(attributeValue); + if (attributeValue.begin() != trimmed.begin()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' can not start with whitespace." + << std::endl; + return false; + } + + if (attributeValue.end() != trimmed.end()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' can not end with whitespace." + << std::endl; + return false; + } + } + return true; +} + +constexpr const char16_t* kPackageIdentSet = u"._"; + +bool ManifestValidator::validateManifest(const Source& source, android::ResXMLParser* parser) { + bool error = false; + SourceLogger logger(source); + + const StringPiece16 kAndroid = u"android"; + const StringPiece16 kPackage = u"package"; + const StringPiece16 kSharedUserId = u"sharedUserId"; + + ssize_t idx; + + idx = parser->indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()); + if (idx < 0) { + logger.error(parser->getLineNumber()) + << "missing package attribute." + << std::endl; + error = true; + } else { + error |= !validateInlineAttribute(parser, idx, logger, kPackageIdentSet); + } + + idx = parser->indexOfAttribute(kAndroid.data(), kAndroid.size(), + kSharedUserId.data(), kSharedUserId.size()); + if (idx >= 0) { + error |= !validateInlineAttribute(parser, idx, logger, kPackageIdentSet); + } + return !error; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.h b/tools/aapt2/ManifestValidator.h new file mode 100644 index 0000000..3188784 --- /dev/null +++ b/tools/aapt2/ManifestValidator.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_MANIFEST_VALIDATOR_H +#define AAPT_MANIFEST_VALIDATOR_H + +#include "Logger.h" +#include "Maybe.h" +#include "Source.h" +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +class ManifestValidator { +public: + ManifestValidator(const android::ResTable& table); + ManifestValidator(const ManifestValidator&) = delete; + + bool validate(const Source& source, android::ResXMLParser* parser); + +private: + bool validateManifest(const Source& source, android::ResXMLParser* parser); + + Maybe<StringPiece16> getAttributeInlineValue(android::ResXMLParser* parser, size_t idx); + Maybe<StringPiece16> getAttributeValue(android::ResXMLParser* parser, size_t idx); + + bool validateInlineAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, const StringPiece16& charSet); + bool validateAttribute(android::ResXMLParser* parser, size_t idx, SourceLogger& logger, + const StringPiece16& charSet); + bool validateAttributeImpl(const StringPiece16& element, const StringPiece16& attributeName, + const StringPiece16& attributeValue, const StringPiece16& charSet, + size_t lineNumber, SourceLogger& logger); + + const android::ResTable& mTable; +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_VALIDATOR_H diff --git a/tools/aapt2/Maybe.h b/tools/aapt2/Maybe.h new file mode 100644 index 0000000..ff6625f --- /dev/null +++ b/tools/aapt2/Maybe.h @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_MAYBE_H +#define AAPT_MAYBE_H + +#include <cassert> +#include <type_traits> +#include <utility> + +namespace aapt { + +/** + * Either holds a valid value of type T, or holds Nothing. + * The value is stored inline in this structure, so no + * heap memory is used when creating a Maybe<T> object. + */ +template <typename T> +class Maybe { +public: + /** + * Construct Nothing. + */ + Maybe(); + + ~Maybe(); + + Maybe(const Maybe& rhs); + + template <typename U> + Maybe(const Maybe<U>& rhs); + + Maybe(Maybe&& rhs); + + template <typename U> + Maybe(Maybe<U>&& rhs); + + Maybe& operator=(const Maybe& rhs); + + template <typename U> + Maybe& operator=(const Maybe<U>& rhs); + + Maybe& operator=(Maybe&& rhs); + + template <typename U> + Maybe& operator=(Maybe<U>&& rhs); + + /** + * Construct a Maybe holding a value. + */ + Maybe(const T& value); + + /** + * Construct a Maybe holding a value. + */ + Maybe(T&& value); + + /** + * True if this holds a value, false if + * it holds Nothing. + */ + operator bool() const; + + /** + * Gets the value if one exists, or else + * panics. + */ + T& value(); + + /** + * Gets the value if one exists, or else + * panics. + */ + const T& value() const; + +private: + template <typename U> + friend class Maybe; + + template <typename U> + Maybe& copy(const Maybe<U>& rhs); + + template <typename U> + Maybe& move(Maybe<U>&& rhs); + + void destroy(); + + bool mNothing; + + typename std::aligned_storage<sizeof(T), alignof(T)>::type mStorage; +}; + +template <typename T> +Maybe<T>::Maybe() +: mNothing(true) { +} + +template <typename T> +Maybe<T>::~Maybe() { + if (!mNothing) { + destroy(); + } +} + +template <typename T> +Maybe<T>::Maybe(const Maybe& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + new (&mStorage) T(reinterpret_cast<const T&>(rhs.mStorage)); + } +} + +template <typename T> +template <typename U> +Maybe<T>::Maybe(const Maybe<U>& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + new (&mStorage) T(reinterpret_cast<const U&>(rhs.mStorage)); + } +} + +template <typename T> +Maybe<T>::Maybe(Maybe&& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + rhs.mNothing = true; + + // Move the value from rhs. + new (&mStorage) T(std::move(reinterpret_cast<T&>(rhs.mStorage))); + rhs.destroy(); + } +} + +template <typename T> +template <typename U> +Maybe<T>::Maybe(Maybe<U>&& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + rhs.mNothing = true; + + // Move the value from rhs. + new (&mStorage) T(std::move(reinterpret_cast<U&>(rhs.mStorage))); + rhs.destroy(); + } +} + +template <typename T> +inline Maybe<T>& Maybe<T>::operator=(const Maybe& rhs) { + // Delegate to the actual assignment. + return copy(rhs); +} + +template <typename T> +template <typename U> +inline Maybe<T>& Maybe<T>::operator=(const Maybe<U>& rhs) { + return copy(rhs); +} + +template <typename T> +template <typename U> +Maybe<T>& Maybe<T>::copy(const Maybe<U>& rhs) { + if (mNothing && rhs.mNothing) { + // Both are nothing, nothing to do. + return *this; + } else if (!mNothing && !rhs.mNothing) { + // We both are something, so assign rhs to us. + reinterpret_cast<T&>(mStorage) = reinterpret_cast<const U&>(rhs.mStorage); + } else if (mNothing) { + // We are nothing but rhs is something. + mNothing = rhs.mNothing; + + // Copy the value from rhs. + new (&mStorage) T(reinterpret_cast<const U&>(rhs.mStorage)); + } else { + // We are something but rhs is nothing, so destroy our value. + mNothing = rhs.mNothing; + destroy(); + } + return *this; +} + +template <typename T> +inline Maybe<T>& Maybe<T>::operator=(Maybe&& rhs) { + // Delegate to the actual assignment. + return move(std::forward<Maybe<T>>(rhs)); +} + +template <typename T> +template <typename U> +inline Maybe<T>& Maybe<T>::operator=(Maybe<U>&& rhs) { + return move(std::forward<Maybe<U>>(rhs)); +} + +template <typename T> +template <typename U> +Maybe<T>& Maybe<T>::move(Maybe<U>&& rhs) { + if (mNothing && rhs.mNothing) { + // Both are nothing, nothing to do. + return *this; + } else if (!mNothing && !rhs.mNothing) { + // We both are something, so move assign rhs to us. + rhs.mNothing = true; + reinterpret_cast<T&>(mStorage) = std::move(reinterpret_cast<U&>(rhs.mStorage)); + rhs.destroy(); + } else if (mNothing) { + // We are nothing but rhs is something. + mNothing = false; + rhs.mNothing = true; + + // Move the value from rhs. + new (&mStorage) T(std::move(reinterpret_cast<U&>(rhs.mStorage))); + rhs.destroy(); + } else { + // We are something but rhs is nothing, so destroy our value. + mNothing = true; + destroy(); + } + return *this; +} + +template <typename T> +Maybe<T>::Maybe(const T& value) +: mNothing(false) { + new (&mStorage) T(value); +} + +template <typename T> +Maybe<T>::Maybe(T&& value) +: mNothing(false) { + new (&mStorage) T(std::forward<T>(value)); +} + +template <typename T> +Maybe<T>::operator bool() const { + return !mNothing; +} + +template <typename T> +T& Maybe<T>::value() { + assert(!mNothing && "Maybe<T>::value() called on Nothing"); + return reinterpret_cast<T&>(mStorage); +} + +template <typename T> +const T& Maybe<T>::value() const { + assert(!mNothing && "Maybe<T>::value() called on Nothing"); + return reinterpret_cast<const T&>(mStorage); +} + +template <typename T> +void Maybe<T>::destroy() { + reinterpret_cast<T&>(mStorage).~T(); +} + +template <typename T> +inline Maybe<typename std::remove_reference<T>::type> make_value(T&& value) { + return Maybe<typename std::remove_reference<T>::type>(std::forward<T>(value)); +} + +template <typename T> +inline Maybe<T> make_nothing() { + return Maybe<T>(); +} + +} // namespace aapt + +#endif // AAPT_MAYBE_H diff --git a/tools/aapt2/Maybe_test.cpp b/tools/aapt2/Maybe_test.cpp new file mode 100644 index 0000000..71bbb94 --- /dev/null +++ b/tools/aapt2/Maybe_test.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <gtest/gtest.h> +#include <string> + +#include "Maybe.h" + +namespace aapt { + +struct Dummy { + Dummy() { + data = new int; + *data = 1; + std::cerr << "Construct Dummy{0x" << (void *) this + << "} with data=0x" << (void*) data + << std::endl; + } + + Dummy(const Dummy& rhs) { + data = nullptr; + if (rhs.data) { + data = new int; + *data = *rhs.data; + } + std::cerr << "CopyConstruct Dummy{0x" << (void *) this + << "} from Dummy{0x" << (const void*) &rhs + << "}" << std::endl; + } + + Dummy(Dummy&& rhs) { + data = rhs.data; + rhs.data = nullptr; + std::cerr << "MoveConstruct Dummy{0x" << (void *) this + << "} from Dummy{0x" << (const void*) &rhs + << "}" << std::endl; + } + + Dummy& operator=(const Dummy& rhs) { + delete data; + data = nullptr; + + if (rhs.data) { + data = new int; + *data = *rhs.data; + } + std::cerr << "CopyAssign Dummy{0x" << (void *) this + << "} from Dummy{0x" << (const void*) &rhs + << "}" << std::endl; + return *this; + } + + Dummy& operator=(Dummy&& rhs) { + delete data; + data = rhs.data; + rhs.data = nullptr; + std::cerr << "MoveAssign Dummy{0x" << (void *) this + << "} from Dummy{0x" << (const void*) &rhs + << "}" << std::endl; + return *this; + } + + ~Dummy() { + std::cerr << "Destruct Dummy{0x" << (void *) this + << "} with data=0x" << (void*) data + << std::endl; + delete data; + } + + int* data; +}; + +TEST(MaybeTest, MakeNothing) { + Maybe<int> val = make_nothing<int>(); + EXPECT_FALSE(val); + + Maybe<std::string> val2 = make_nothing<std::string>(); + EXPECT_FALSE(val2); + + val2 = make_nothing<std::string>(); + EXPECT_FALSE(val2); +} + +TEST(MaybeTest, MakeSomething) { + Maybe<int> val = make_value(23); + ASSERT_TRUE(val); + EXPECT_EQ(23, val.value()); + + Maybe<std::string> val2 = make_value(std::string("hey")); + ASSERT_TRUE(val2); + EXPECT_EQ(std::string("hey"), val2.value()); +} + +TEST(MaybeTest, Lifecycle) { + Maybe<Dummy> val = make_nothing<Dummy>(); + + Maybe<Dummy> val2 = make_value(Dummy()); +} + +TEST(MaybeTest, MoveAssign) { + Maybe<Dummy> val; + { + Maybe<Dummy> val2 = Dummy(); + val = std::move(val2); + } +} + +} // namespace aapt diff --git a/tools/aapt2/MockResolver.h b/tools/aapt2/MockResolver.h new file mode 100644 index 0000000..0c9b954 --- /dev/null +++ b/tools/aapt2/MockResolver.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_MOCK_RESOLVER_H +#define AAPT_MOCK_RESOLVER_H + +#include "Maybe.h" +#include "Resolver.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceTableResolver.h" +#include "ResourceValues.h" +#include "StringPiece.h" + +#include <map> +#include <string> + +namespace aapt { + +struct MockResolver : public IResolver { + MockResolver(const std::shared_ptr<ResourceTable>& table, + const std::map<ResourceName, ResourceId>& items) : + mResolver(std::make_shared<ResourceTableResolver>( + table, std::vector<std::shared_ptr<const android::AssetManager>>())), + mAttr(false, android::ResTable_map::TYPE_ANY), mItems(items) { + } + + virtual Maybe<ResourceId> findId(const ResourceName& name) override { + Maybe<ResourceId> result = mResolver->findId(name); + if (result) { + return result; + } + + const auto iter = mItems.find(name); + if (iter != mItems.end()) { + return iter->second; + } + return {}; + } + + virtual Maybe<Entry> findAttribute(const ResourceName& name) override { + Maybe<Entry> tableResult = mResolver->findAttribute(name); + if (tableResult) { + return tableResult; + } + + Maybe<ResourceId> result = findId(name); + if (result) { + if (name.type == ResourceType::kAttr) { + return Entry{ result.value(), &mAttr }; + } else { + return Entry{ result.value() }; + } + } + return {}; + } + + virtual Maybe<ResourceName> findName(ResourceId resId) override { + Maybe<ResourceName> result = mResolver->findName(resId); + if (result) { + return result; + } + + for (auto& p : mItems) { + if (p.second == resId) { + return p.first; + } + } + return {}; + } + +private: + std::shared_ptr<ResourceTableResolver> mResolver; + Attribute mAttr; + std::map<ResourceName, ResourceId> mItems; +}; + +} // namespace aapt + +#endif // AAPT_MOCK_RESOLVER_H diff --git a/tools/aapt2/NameMangler.h b/tools/aapt2/NameMangler.h new file mode 100644 index 0000000..1e15e20 --- /dev/null +++ b/tools/aapt2/NameMangler.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_NAME_MANGLER_H +#define AAPT_NAME_MANGLER_H + +#include <string> + +namespace aapt { + +struct NameMangler { + /** + * Mangles the name in `outName` with the `package` and stores the mangled + * result in `outName`. The mangled name should contain symbols that are + * illegal to define in XML, so that there will never be name mangling + * collisions. + */ + static void mangle(const std::u16string& package, std::u16string* outName) { + *outName = package + u"$" + *outName; + } + + /** + * Unmangles the name in `outName`, storing the correct name back in `outName` + * and the package in `outPackage`. Returns true if the name was unmangled or + * false if the name was never mangled to begin with. + */ + static bool unmangle(std::u16string* outName, std::u16string* outPackage) { + size_t pivot = outName->find(u'$'); + if (pivot == std::string::npos) { + return false; + } + + outPackage->assign(outName->data(), pivot); + outName->assign(outName->data() + pivot + 1, outName->size() - (pivot + 1)); + return true; + } +}; + +} // namespace aapt + +#endif // AAPT_NAME_MANGLER_H diff --git a/tools/aapt2/NameMangler_test.cpp b/tools/aapt2/NameMangler_test.cpp new file mode 100644 index 0000000..6103655 --- /dev/null +++ b/tools/aapt2/NameMangler_test.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "NameMangler.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +TEST(NameManglerTest, MangleName) { + std::u16string package = u"android.appcompat"; + std::u16string name = u"Platform.AppCompat"; + + NameMangler::mangle(package, &name); + EXPECT_EQ(name, u"android.appcompat$Platform.AppCompat"); + + std::u16string newPackage; + ASSERT_TRUE(NameMangler::unmangle(&name, &newPackage)); + EXPECT_EQ(name, u"Platform.AppCompat"); + EXPECT_EQ(newPackage, u"android.appcompat"); +} + +TEST(NameManglerTest, IgnoreUnmangledName) { + std::u16string package; + std::u16string name = u"foo_bar"; + + EXPECT_FALSE(NameMangler::unmangle(&name, &package)); + EXPECT_EQ(name, u"foo_bar"); +} + +} // namespace aapt diff --git a/tools/aapt2/Png.cpp b/tools/aapt2/Png.cpp new file mode 100644 index 0000000..4e9b68e --- /dev/null +++ b/tools/aapt2/Png.cpp @@ -0,0 +1,1280 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" +#include "Logger.h" +#include "Png.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <iostream> +#include <png.h> +#include <sstream> +#include <string> +#include <vector> +#include <zlib.h> + +namespace aapt { + +constexpr bool kDebug = false; +constexpr size_t kPngSignatureSize = 8u; + +struct PngInfo { + ~PngInfo() { + for (png_bytep row : rows) { + if (row != nullptr) { + delete[] row; + } + } + + delete[] xDivs; + delete[] yDivs; + } + + void* serialize9Patch() { + void* serialized = android::Res_png_9patch::serialize(info9Patch, xDivs, yDivs, + colors.data()); + reinterpret_cast<android::Res_png_9patch*>(serialized)->deviceToFile(); + return serialized; + } + + uint32_t width = 0; + uint32_t height = 0; + std::vector<png_bytep> rows; + + bool is9Patch = false; + android::Res_png_9patch info9Patch; + int32_t* xDivs = nullptr; + int32_t* yDivs = nullptr; + std::vector<uint32_t> colors; + + // Layout padding. + bool haveLayoutBounds = false; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + // Round rect outline description. + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + uint8_t outlineAlpha; +}; + +static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) { + std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr)); + if (!input->read(reinterpret_cast<char*>(data), length)) { + png_error(readPtr, strerror(errno)); + } +} + +static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) { + BigBuffer* outBuffer = reinterpret_cast<BigBuffer*>(png_get_io_ptr(writePtr)); + png_bytep buf = outBuffer->nextBlock<png_byte>(length); + memcpy(buf, data, length); +} + +static void flushDataToStream(png_structp /*writePtr*/) { +} + +static void logWarning(png_structp readPtr, png_const_charp warningMessage) { + SourceLogger* logger = reinterpret_cast<SourceLogger*>(png_get_error_ptr(readPtr)); + logger->warn() << warningMessage << "." << std::endl; +} + + +static bool readPng(png_structp readPtr, png_infop infoPtr, PngInfo* outInfo, + std::string* outError) { + if (setjmp(png_jmpbuf(readPtr))) { + *outError = "failed reading png"; + return false; + } + + png_set_sig_bytes(readPtr, kPngSignatureSize); + png_read_info(readPtr, infoPtr); + + int colorType, bitDepth, interlaceType, compressionType; + png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType, + &interlaceType, &compressionType, nullptr); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(readPtr); + } + + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(readPtr); + } + + if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(readPtr); + } + + if (bitDepth == 16) { + png_set_strip_16(readPtr); + } + + if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER); + } + + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(readPtr); + } + + png_set_interlace_handling(readPtr); + png_read_update_info(readPtr, infoPtr); + + const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr); + outInfo->rows.resize(outInfo->height); + for (size_t i = 0; i < outInfo->height; i++) { + outInfo->rows[i] = new png_byte[rowBytes]; + } + + png_read_image(readPtr, outInfo->rows.data()); + png_read_end(readPtr, infoPtr); + return true; +} + +static void checkNinePatchSerialization(android::Res_png_9patch* inPatch, void* data) { + size_t patchSize = inPatch->serializedSize(); + void* newData = malloc(patchSize); + memcpy(newData, data, patchSize); + android::Res_png_9patch* outPatch = inPatch->deserialize(newData); + outPatch->fileToDevice(); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); +/* for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->getColors()[i] == inPatch->getColors()[i]); + }*/ + free(newData); +} + +/*static void dump_image(int w, int h, const png_byte* const* rows, int color_type) { + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + const png_byte* row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + printf("\n"); + } + } + } +}*/ + +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define ABS(a) ((a)<0?-(a):(a)) + +static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, + int *paletteEntries, bool *hasTransparency, int *colorType, + png_bytepp outRows) { + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + if (kDebug) { + printf("Initial image data:\n"); + //dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA); + } + + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + if (kDebug) { + printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", + maxGrayDeviation, i, j, rr, gg, bb, aa); + } + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + if (kDebug) { + printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + if (kDebug) { + printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + if (kDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + if (kDebug) { + printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); + printf("isOpaque = %s\n", isOpaque ? "true" : "false"); + printf("isPalette = %s\n", isPalette ? "true" : "false"); + printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", + paletteSize, 2 * w * h, bpp * w * h); + printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance); + } + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + logger->note() << "forcing image to gray (max deviation = " << maxGrayDeviation + << ")." + << std::endl; + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte) (col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + +static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance, SourceLogger* logger, std::string* outError) { + if (setjmp(png_jmpbuf(writePtr))) { + *outError = "failed to write png"; + return false; + } + + uint32_t width, height; + int colorType, bitDepth, interlaceType, compressionType; + + png_unknown_chunk unknowns[3]; + unknowns[0].data = nullptr; + unknowns[1].data = nullptr; + unknowns[2].data = nullptr; + + png_bytepp outRows = (png_bytepp) malloc((int) info->height * sizeof(png_bytep)); + if (outRows == (png_bytepp) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (uint32_t i = 0; i < info->height; i++) { + outRows[i] = (png_bytep) malloc(2 * (int) info->width); + if (outRows[i] == (png_bytep) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(writePtr, Z_BEST_COMPRESSION); + + if (kDebug) { + logger->note() << "writing image: w = " << info->width + << ", h = " << info->height + << std::endl; + } + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette, + &paletteEntries, &hasTransparency, &colorType, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB || + colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_PALETTE)) { + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + } + + if (kDebug) { + switch (colorType) { + case PNG_COLOR_TYPE_PALETTE: + logger->note() << "has " << paletteEntries + << " colors" << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE." + << std::endl; + break; + case PNG_COLOR_TYPE_GRAY: + logger->note() << "is opaque gray, using PNG_COLOR_TYPE_GRAY." << std::endl; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + logger->note() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA." << std::endl; + break; + case PNG_COLOR_TYPE_RGB: + logger->note() << "is opaque RGB, using PNG_COLOR_TYPE_RGB." << std::endl; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + logger->note() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA." << std::endl; + break; + } + } + + png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p) 0); + } + png_set_filter(writePtr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(writePtr, 0, PNG_ALL_FILTERS); + } + + if (info->is9Patch) { + int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0); + int pIndex = info->haveLayoutBounds ? 2 : 1; + int bIndex = 1; + int oIndex = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch data being last + png_bytep chunkNames = info->haveLayoutBounds + ? (png_bytep)"npOl\0npLb\0npTc\0" + : (png_bytep)"npOl\0npTc"; + + // base 9 patch data + if (kDebug) { + logger->note() << "adding 9-patch info..." << std::endl; + } + strcpy((char*)unknowns[pIndex].name, "npTc"); + unknowns[pIndex].data = (png_byte*) info->serialize9Patch(); + unknowns[pIndex].size = info->info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data); + + // automatically generated 9 patch outline data + int chunkSize = sizeof(png_uint_32) * 6; + strcpy((char*)unknowns[oIndex].name, "npOl"); + unknowns[oIndex].data = (png_byte*) calloc(chunkSize, 1); + png_byte outputData[chunkSize]; + memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*) outputData)[4] = info->outlineRadius; + ((png_uint_32*) outputData)[5] = info->outlineAlpha; + memcpy(unknowns[oIndex].data, &outputData, chunkSize); + unknowns[oIndex].size = chunkSize; + + // optional optical inset / layout bounds data + if (info->haveLayoutBounds) { + int chunkSize = sizeof(png_uint_32) * 4; + strcpy((char*)unknowns[bIndex].name, "npLb"); + unknowns[bIndex].data = (png_byte*) calloc(chunkSize, 1); + memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize); + unknowns[bIndex].size = chunkSize; + } + + for (int i = 0; i < chunkCount; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, + chunkNames, chunkCount); + png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount); + +#if PNG_LIBPNG_VER < 10600 + // Deal with unknown chunk location bug in 1.5.x and earlier. + png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE); + if (info->haveLayoutBounds) { + png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE); + } +#endif + } + + png_write_info(writePtr, infoPtr); + + png_bytepp rows; + if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) { + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(writePtr, 0, PNG_FILLER_AFTER); + } + rows = info->rows.data(); + } else { + rows = outRows; + } + png_write_image(writePtr, rows); + + if (kDebug) { + printf("Final image data:\n"); + //dump_image(info->width, info->height, rows, colorType); + } + + png_write_end(writePtr, infoPtr); + + for (uint32_t i = 0; i < info->height; i++) { + free(outRows[i]); + } + free(outRows); + free(unknowns[0].data); + free(unknowns[1].data); + free(unknowns[2].data); + + png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType, + &compressionType, nullptr); + + if (kDebug) { + logger->note() << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType + << std::endl; + } + return true; +} + +constexpr uint32_t kColorWhite = 0xffffffffu; +constexpr uint32_t kColorTick = 0xff000000u; +constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu; + +enum class TickType { + kNone, + kTick, + kLayoutBounds, + kBoth +}; + +static TickType tickType(png_bytep p, bool transparent, const char** outError) { + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TickType::kNone; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + if (color == kColorTick) { + return TickType::kTick; + } + + // Error cases + if (p[3] != 0xff) { + *outError = "Frame pixels must be either solid or transparent " + "(not intermediate alphas)"; + return TickType::kNone; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TickType::kTick; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == kColorWhite) { + return TickType::kNone; + } + if (color == kColorTick) { + return TickType::kTick; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TickType::kNone; + } + return TickType::kTick; +} + +enum class TickState { + kStart, + kInside1, + kOutside1 +}; + +static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) { + *outLeft = *outRight = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < width - 1; i++) { + if (tickType(row+i*4, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outLeft = i-1; + *outRight = width-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outRight = i-1; + outRight += 2; + outLeft += 2; + state = TickState::kOutside1; + } + } else { + *outLeft = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return false; + } + return true; +} + +static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent, + bool required, int32_t* outTop, int32_t* outBottom, + const char** outError, uint8_t* outDivs, bool multipleAllowed) { + *outTop = *outBottom = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < height - 1; i++) { + if (tickType(rows[i]+offset, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outTop = i-1; + *outBottom = height-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outBottom = i-1; + outTop += 2; + outBottom += 2; + state = TickState::kOutside1; + } + } else { + *outTop = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return false; + } + return true; +} + +static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent, + bool /* required */, int32_t* outLeft, + int32_t* outRight, const char** outError) { + *outLeft = *outRight = 0; + + // Look for left tick + if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for right tick + if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + if (tickType(row+i*4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent, + bool /* required */, int32_t* outTop, int32_t* outBottom, + const char** outError) { + *outTop = *outBottom = 0; + + // Look for top tick + if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for bottom tick + if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY, + int dX, int dY, int* outInset) { + uint8_t maxOpacity = 0; + int inset = 0; + *outInset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + uint8_t opacity = color[3]; + if (opacity > maxOpacity) { + maxOpacity = opacity; + *outInset = inset; + } + if (opacity == 0xff) return; + } +} + +static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) { + uint8_t maxAlpha = 0; + for (int x = startX; x < endX; x++) { + uint8_t alpha = (row + x * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) { + uint8_t maxAlpha = 0; + for (int y = startY; y < endY; y++) { + uint8_t alpha = (rows[y] + offsetX * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static void getOutline(PngInfo* image) { + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0, + &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1, + &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineAlpha = std::max( + maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX), + maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY)); + + int diagonalInset = 0; + findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + image->outlineRadius = 3.4142f * diagonalInset; + + if (kDebug) { + printf("outline insets %d %d %d %d, rad %f, alpha %x\n", + image->outlineInsetsLeft, + image->outlineInsetsTop, + image->outlineInsetsRight, + image->outlineInsetsBottom, + image->outlineRadius, + image->outlineAlpha); + } +} + +static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) { + png_bytep color = rows[top] + left*4; + + if (left > right || top > bottom) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top]+i*4; + if (color[3] == 0) { + if (p[3] != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] || + p[2] != color[2] || p[3] != color[3]) { + return android::Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3]<<24) | (color[0]<<16) | (color[1]<<8) | color[2]; +} + +static bool do9Patch(PngInfo* image, std::string* outError) { + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + const int maxSizeXDivs = W * sizeof(int32_t); + const int maxSizeYDivs = H * sizeof(int32_t); + int32_t* xDivs = image->xDivs = new int32_t[W]; + int32_t* yDivs = image->yDivs = new int32_t[H]; + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1; + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + image->layoutBoundsLeft = image->layoutBoundsRight = 0; + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = nullptr; + int errorPixel = -1; + const char* errorEdge = nullptr; + + int colorIndex = 0; + std::vector<png_bytep> newRows; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && + (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs, + true)) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1], + &errorMsg, &numYDivs, true)) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Copy patch size data into image... + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + + // Find left and right of padding area... + if (!getHorizontalTicks(image->rows[H-1], W, transparent, false, + &image->info9Patch.paddingLeft, &image->info9Patch.paddingRight, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (!getVerticalTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Find left and right of layout padding... + getHorizontalLayoutBoundsTicks(image->rows[H-1], W, transparent, false, + &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg); + + getVerticalLayoutBoundsTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 + || image->layoutBoundsRight != 0 + || image->layoutBoundsTop != 0 + || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + if (kDebug) { + printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom); + } + } + + // use opacity of pixels to estimate the round rect outline + getOutline(image); + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + +/* if (kDebug) { + printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + xDivs[0], xDivs[1], + yDivs[0], yDivs[1]); + printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, image->info9Patch.paddingBottom); + }*/ + + // Remove frame from image. + newRows.resize(H - 2); + for (i = 0; i < H - 2; i++) { + newRows[i] = image->rows[i + 1]; + memmove(newRows[i], newRows[i] + 4, (W - 2) * 4); + } + image->rows.swap(newRows); + + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->colors.resize(numColors); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); j <= numYDivs && top < H; j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; i <= numXDivs && left < W; i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = getColor(image->rows.data(), left, top, right - 1, bottom - 1); + image->colors[colorIndex++] = c; + if (kDebug) { + if (c != android::Res_png_9patch::NO_COLOR) { + hasColor = true; + } + } + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + if (kDebug && hasColor) { + for (i = 0; i < numColors; i++) { + if (i == 0) printf("Colors:\n"); + printf(" #%08x", image->colors[i]); + if (i == numColors - 1) printf("\n"); + } + } +getout: + if (errorMsg) { + std::stringstream err; + err << "9-patch malformed: " << errorMsg; + if (!errorEdge) { + err << "." << std::endl; + if (errorPixel >= 0) { + err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge"; + } else { + err << "Found along " << errorEdge << " edge"; + } + } + *outError = err.str(); + return false; + } + return true; +} + + +bool Png::process(const Source& source, std::istream& input, BigBuffer* outBuffer, + const Options& options, std::string* outError) { + png_byte signature[kPngSignatureSize]; + + // Read the PNG signature first. + if (!input.read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + *outError = strerror(errno); + return false; + } + + // If the PNG signature doesn't match, bail early. + if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + *outError = "not a valid png file"; + return false; + } + + SourceLogger logger(source); + bool result = false; + png_structp readPtr = nullptr; + png_infop infoPtr = nullptr; + png_structp writePtr = nullptr; + png_infop writeInfoPtr = nullptr; + PngInfo pngInfo = {}; + + readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!readPtr) { + *outError = "failed to allocate read ptr"; + goto bail; + } + + infoPtr = png_create_info_struct(readPtr); + if (!infoPtr) { + *outError = "failed to allocate info ptr"; + goto bail; + } + + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(&logger), nullptr, logWarning); + + // Set the read function to read from std::istream. + png_set_read_fn(readPtr, (png_voidp)&input, readDataFromStream); + + if (!readPng(readPtr, infoPtr, &pngInfo, outError)) { + goto bail; + } + + if (util::stringEndsWith<char>(source.path, ".9.png")) { + if (!do9Patch(&pngInfo, outError)) { + goto bail; + } + } + + writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!writePtr) { + *outError = "failed to allocate write ptr"; + goto bail; + } + + writeInfoPtr = png_create_info_struct(writePtr); + if (!writeInfoPtr) { + *outError = "failed to allocate write info ptr"; + goto bail; + } + + png_set_error_fn(writePtr, nullptr, nullptr, logWarning); + + // Set the write function to write to std::ostream. + png_set_write_fn(writePtr, (png_voidp)outBuffer, writeDataToStream, flushDataToStream); + + if (!writePng(writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance, &logger, + outError)) { + goto bail; + } + + result = true; +bail: + if (readPtr) { + png_destroy_read_struct(&readPtr, &infoPtr, nullptr); + } + + if (writePtr) { + png_destroy_write_struct(&writePtr, &writeInfoPtr); + } + return result; +} + +} // namespace aapt diff --git a/tools/aapt2/Png.h b/tools/aapt2/Png.h new file mode 100644 index 0000000..4577ab8 --- /dev/null +++ b/tools/aapt2/Png.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_PNG_H +#define AAPT_PNG_H + +#include "BigBuffer.h" +#include "Source.h" + +#include <iostream> +#include <string> + +namespace aapt { + +struct Png { + struct Options { + int grayScaleTolerance = 0; + }; + + bool process(const Source& source, std::istream& input, BigBuffer* outBuffer, + const Options& options, std::string* outError); +}; + +} // namespace aapt + +#endif // AAPT_PNG_H diff --git a/tools/aapt2/ProguardRules.cpp b/tools/aapt2/ProguardRules.cpp new file mode 100644 index 0000000..e89fb7c --- /dev/null +++ b/tools/aapt2/ProguardRules.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ProguardRules.h" +#include "Util.h" +#include "XmlDom.h" + +#include <memory> +#include <string> + +namespace aapt { +namespace proguard { + +constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; + +class BaseVisitor : public xml::Visitor { +public: + BaseVisitor(const Source& source, KeepSet* keepSet) : mSource(source), mKeepSet(keepSet) { + } + + virtual void visit(xml::Text*) override {}; + + virtual void visit(xml::Namespace* node) override { + for (const auto& child : node->children) { + child->accept(this); + } + } + + virtual void visit(xml::Element* node) override { + if (!node->namespaceUri.empty()) { + Maybe<std::u16string> maybePackage = util::extractPackageFromNamespace( + node->namespaceUri); + if (maybePackage) { + // This is a custom view, let's figure out the class name from this. + std::u16string package = maybePackage.value() + u"." + node->name; + if (util::isJavaClassName(package)) { + addClass(node->lineNumber, package); + } + } + } else if (util::isJavaClassName(node->name)) { + addClass(node->lineNumber, node->name); + } + + for (const auto& child: node->children) { + child->accept(this); + } + } + +protected: + void addClass(size_t lineNumber, const std::u16string& className) { + mKeepSet->addClass(mSource.line(lineNumber), className); + } + + void addMethod(size_t lineNumber, const std::u16string& methodName) { + mKeepSet->addMethod(mSource.line(lineNumber), methodName); + } + +private: + Source mSource; + KeepSet* mKeepSet; +}; + +struct LayoutVisitor : public BaseVisitor { + LayoutVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkClass = false; + bool checkName = false; + if (node->namespaceUri.empty()) { + checkClass = node->name == u"view" || node->name == u"fragment"; + } else if (node->namespaceUri == kSchemaAndroid) { + checkName = node->name == u"fragment"; + } + + for (const auto& attr : node->attributes) { + if (checkClass && attr.namespaceUri.empty() && attr.name == u"class" && + util::isJavaClassName(attr.value)) { + addClass(node->lineNumber, attr.value); + } else if (checkName && attr.namespaceUri == kSchemaAndroid && attr.name == u"name" && + util::isJavaClassName(attr.value)) { + addClass(node->lineNumber, attr.value); + } else if (attr.namespaceUri == kSchemaAndroid && attr.name == u"onClick") { + addMethod(node->lineNumber, attr.value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct XmlResourceVisitor : public BaseVisitor { + XmlResourceVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkFragment = false; + if (node->namespaceUri.empty()) { + checkFragment = node->name == u"PreferenceScreen" || node->name == u"header"; + } + + if (checkFragment) { + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"fragment"); + if (attr && util::isJavaClassName(attr->value)) { + addClass(node->lineNumber, attr->value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct TransitionVisitor : public BaseVisitor { + TransitionVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkClass = node->namespaceUri.empty() && + (node->name == u"transition" || node->name == u"pathMotion"); + if (checkClass) { + xml::Attribute* attr = node->findAttribute({}, u"class"); + if (attr && util::isJavaClassName(attr->value)) { + addClass(node->lineNumber, attr->value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct ManifestVisitor : public BaseVisitor { + ManifestVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + if (node->namespaceUri.empty()) { + bool getName = false; + if (node->name == u"manifest") { + xml::Attribute* attr = node->findAttribute({}, u"package"); + if (attr) { + mPackage = attr->value; + } + } else if (node->name == u"application") { + getName = true; + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"backupAgent"); + if (attr) { + Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, + attr->value); + if (result) { + addClass(node->lineNumber, result.value()); + } + } + } else if (node->name == u"activity" || node->name == u"service" || + node->name == u"receiver" || node->name == u"provider" || + node->name == u"instrumentation") { + getName = true; + } + + if (getName) { + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"name"); + if (attr) { + Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, + attr->value); + if (result) { + addClass(node->lineNumber, result.value()); + } + } + } + } + BaseVisitor::visit(node); + } + + std::u16string mPackage; +}; + +bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet) { + ManifestVisitor visitor(source, keepSet); + node->accept(&visitor); + return true; +} + +bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, + KeepSet* keepSet) { + switch (type) { + case ResourceType::kLayout: { + LayoutVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + case ResourceType::kXml: { + XmlResourceVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + case ResourceType::kTransition: { + TransitionVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + default: + break; + } + return true; +} + +bool writeKeepSet(std::ostream* out, const KeepSet& keepSet) { + for (const auto& entry : keepSet.mKeepSet) { + for (const SourceLine& source : entry.second) { + *out << "// Referenced at " << source << "\n"; + } + *out << "-keep class " << entry.first << " { <init>(...); }\n" << std::endl; + } + + for (const auto& entry : keepSet.mKeepMethodSet) { + for (const SourceLine& source : entry.second) { + *out << "// Referenced at " << source << "\n"; + } + *out << "-keepclassmembers class * { *** " << entry.first << "(...); }\n" << std::endl; + } + return true; +} + +} // namespace proguard +} // namespace aapt diff --git a/tools/aapt2/ProguardRules.h b/tools/aapt2/ProguardRules.h new file mode 100644 index 0000000..bbb3e64 --- /dev/null +++ b/tools/aapt2/ProguardRules.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_PROGUARD_RULES_H +#define AAPT_PROGUARD_RULES_H + +#include "Resource.h" +#include "Source.h" +#include "XmlDom.h" + +#include <map> +#include <ostream> +#include <set> +#include <string> + +namespace aapt { +namespace proguard { + +class KeepSet { +public: + inline void addClass(const SourceLine& source, const std::u16string& className) { + mKeepSet[className].insert(source); + } + + inline void addMethod(const SourceLine& source, const std::u16string& methodName) { + mKeepMethodSet[methodName].insert(source); + } + +private: + friend bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); + + std::map<std::u16string, std::set<SourceLine>> mKeepSet; + std::map<std::u16string, std::set<SourceLine>> mKeepMethodSet; +}; + +bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet); +bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, + KeepSet* keepSet); + +bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); + +} // namespace proguard +} // namespace aapt + +#endif // AAPT_PROGUARD_RULES_H diff --git a/tools/aapt2/ResChunkPullParser.cpp b/tools/aapt2/ResChunkPullParser.cpp new file mode 100644 index 0000000..78ea60e --- /dev/null +++ b/tools/aapt2/ResChunkPullParser.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ResChunkPullParser.h" + +#include <androidfw/ResourceTypes.h> +#include <cstddef> + +namespace aapt { + +using android::ResChunk_header; + +ResChunkPullParser::Event ResChunkPullParser::next() { + if (!isGoodEvent(mEvent)) { + return mEvent; + } + + if (mEvent == Event::StartDocument) { + mCurrentChunk = mData; + } else { + mCurrentChunk = reinterpret_cast<const ResChunk_header*>( + reinterpret_cast<const char*>(mCurrentChunk) + mCurrentChunk->size); + } + + const std::ptrdiff_t diff = reinterpret_cast<const char*>(mCurrentChunk) + - reinterpret_cast<const char*>(mData); + assert(diff >= 0 && "diff is negative"); + const size_t offset = static_cast<const size_t>(diff); + + if (offset == mLen) { + mCurrentChunk = nullptr; + return (mEvent = Event::EndDocument); + } else if (offset + sizeof(ResChunk_header) > mLen) { + mLastError = "chunk is past the end of the document"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } + + if (mCurrentChunk->headerSize < sizeof(ResChunk_header)) { + mLastError = "chunk has too small header"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } else if (mCurrentChunk->size < mCurrentChunk->headerSize) { + mLastError = "chunk's total size is smaller than header"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } else if (offset + mCurrentChunk->size > mLen) { + mLastError = "chunk's data extends past the end of the document"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } + return (mEvent = Event::Chunk); +} + +} // namespace aapt diff --git a/tools/aapt2/ResChunkPullParser.h b/tools/aapt2/ResChunkPullParser.h new file mode 100644 index 0000000..1426ed2 --- /dev/null +++ b/tools/aapt2/ResChunkPullParser.h @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RES_CHUNK_PULL_PARSER_H +#define AAPT_RES_CHUNK_PULL_PARSER_H + +#include <androidfw/ResourceTypes.h> +#include <string> + +namespace aapt { + +/** + * A pull parser, modeled after XmlPullParser, that reads + * android::ResChunk_header structs from a block of data. + * + * An android::ResChunk_header specifies a type, headerSize, + * and size. The pull parser will verify that the chunk's size + * doesn't extend beyond the available data, and will iterate + * over each chunk in the given block of data. + * + * Processing nested chunks is done by creating a new ResChunkPullParser + * pointing to the data portion of a chunk. + */ +class ResChunkPullParser { +public: + enum class Event { + StartDocument, + EndDocument, + BadDocument, + + Chunk, + }; + + /** + * Returns false if the event is EndDocument or BadDocument. + */ + static bool isGoodEvent(Event event); + + /** + * Create a ResChunkPullParser to read android::ResChunk_headers + * from the memory pointed to by data, of len bytes. + */ + ResChunkPullParser(const void* data, size_t len); + + ResChunkPullParser(const ResChunkPullParser&) = delete; + + Event getEvent() const; + const std::string& getLastError() const; + const android::ResChunk_header* getChunk() const; + + /** + * Move to the next android::ResChunk_header. + */ + Event next(); + +private: + Event mEvent; + const android::ResChunk_header* mData; + size_t mLen; + const android::ResChunk_header* mCurrentChunk; + std::string mLastError; +}; + +template <typename T> +inline static const T* convertTo(const android::ResChunk_header* chunk) { + if (chunk->headerSize < sizeof(T)) { + return nullptr; + } + return reinterpret_cast<const T*>(chunk); +} + +inline static const uint8_t* getChunkData(const android::ResChunk_header& chunk) { + return reinterpret_cast<const uint8_t*>(&chunk) + chunk.headerSize; +} + +inline static size_t getChunkDataLen(const android::ResChunk_header& chunk) { + return chunk.size - chunk.headerSize; +} + +// +// Implementation +// + +inline bool ResChunkPullParser::isGoodEvent(ResChunkPullParser::Event event) { + return event != Event::EndDocument && event != Event::BadDocument; +} + +inline ResChunkPullParser::ResChunkPullParser(const void* data, size_t len) : + mEvent(Event::StartDocument), + mData(reinterpret_cast<const android::ResChunk_header*>(data)), + mLen(len), + mCurrentChunk(nullptr) { +} + +inline ResChunkPullParser::Event ResChunkPullParser::getEvent() const { + return mEvent; +} + +inline const std::string& ResChunkPullParser::getLastError() const { + return mLastError; +} + +inline const android::ResChunk_header* ResChunkPullParser::getChunk() const { + return mCurrentChunk; +} + +} // namespace aapt + +#endif // AAPT_RES_CHUNK_PULL_PARSER_H diff --git a/tools/aapt2/Resolver.h b/tools/aapt2/Resolver.h new file mode 100644 index 0000000..cb9318e --- /dev/null +++ b/tools/aapt2/Resolver.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOLVER_H +#define AAPT_RESOLVER_H + +#include "Maybe.h" +#include "Resource.h" +#include "ResourceValues.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +/** + * Resolves symbolic references (package:type/entry) into resource IDs/objects. + */ +class IResolver { +public: + virtual ~IResolver() {}; + + /** + * Holds the result of a resource name lookup. + */ + struct Entry { + /** + * The ID of the resource. ResourceId::isValid() may + * return false if the resource has not been assigned + * an ID. + */ + ResourceId id; + + /** + * If the resource is an attribute, this will point + * to a valid Attribute object, or else it will be + * nullptr. + */ + const Attribute* attr; + }; + + /** + * Returns a ResourceID if the name is found. The ResourceID + * may not be valid if the resource was not assigned an ID. + */ + virtual Maybe<ResourceId> findId(const ResourceName& name) = 0; + + /** + * Returns an Entry if the name is found. Entry::attr + * may be nullptr if the resource is not an attribute. + */ + virtual Maybe<Entry> findAttribute(const ResourceName& name) = 0; + + /** + * Find a resource by ID. Resolvers may contain resources without + * resource IDs assigned to them. + */ + virtual Maybe<ResourceName> findName(ResourceId resId) = 0; +}; + +} // namespace aapt + +#endif // AAPT_RESOLVER_H diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp new file mode 100644 index 0000000..287d8de --- /dev/null +++ b/tools/aapt2/Resource.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Resource.h" +#include "StringPiece.h" + +#include <map> +#include <string> + +namespace aapt { + +StringPiece16 toString(ResourceType type) { + switch (type) { + case ResourceType::kAnim: return u"anim"; + case ResourceType::kAnimator: return u"animator"; + case ResourceType::kArray: return u"array"; + case ResourceType::kAttr: return u"attr"; + case ResourceType::kAttrPrivate: return u"attr"; + case ResourceType::kBool: return u"bool"; + case ResourceType::kColor: return u"color"; + case ResourceType::kDimen: return u"dimen"; + case ResourceType::kDrawable: return u"drawable"; + case ResourceType::kFraction: return u"fraction"; + case ResourceType::kId: return u"id"; + case ResourceType::kInteger: return u"integer"; + case ResourceType::kIntegerArray: return u"integer-array"; + case ResourceType::kInterpolator: return u"interpolator"; + case ResourceType::kLayout: return u"layout"; + case ResourceType::kMenu: return u"menu"; + case ResourceType::kMipmap: return u"mipmap"; + case ResourceType::kPlurals: return u"plurals"; + case ResourceType::kRaw: return u"raw"; + case ResourceType::kString: return u"string"; + case ResourceType::kStyle: return u"style"; + case ResourceType::kStyleable: return u"styleable"; + case ResourceType::kTransition: return u"transition"; + case ResourceType::kXml: return u"xml"; + } + return {}; +} + +static const std::map<StringPiece16, ResourceType> sResourceTypeMap { + { u"anim", ResourceType::kAnim }, + { u"animator", ResourceType::kAnimator }, + { u"array", ResourceType::kArray }, + { u"attr", ResourceType::kAttr }, + { u"^attr-private", ResourceType::kAttrPrivate }, + { u"bool", ResourceType::kBool }, + { u"color", ResourceType::kColor }, + { u"dimen", ResourceType::kDimen }, + { u"drawable", ResourceType::kDrawable }, + { u"fraction", ResourceType::kFraction }, + { u"id", ResourceType::kId }, + { u"integer", ResourceType::kInteger }, + { u"integer-array", ResourceType::kIntegerArray }, + { u"interpolator", ResourceType::kInterpolator }, + { u"layout", ResourceType::kLayout }, + { u"menu", ResourceType::kMenu }, + { u"mipmap", ResourceType::kMipmap }, + { u"plurals", ResourceType::kPlurals }, + { u"raw", ResourceType::kRaw }, + { u"string", ResourceType::kString }, + { u"style", ResourceType::kStyle }, + { u"styleable", ResourceType::kStyleable }, + { u"transition", ResourceType::kTransition }, + { u"xml", ResourceType::kXml }, +}; + +const ResourceType* parseResourceType(const StringPiece16& str) { + auto iter = sResourceTypeMap.find(str); + if (iter == std::end(sResourceTypeMap)) { + return nullptr; + } + return &iter->second; +} + +} // namespace aapt diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h new file mode 100644 index 0000000..fa9ac07 --- /dev/null +++ b/tools/aapt2/Resource.h @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_H +#define AAPT_RESOURCE_H + +#include "StringPiece.h" + +#include <iomanip> +#include <limits> +#include <string> +#include <tuple> + +namespace aapt { + +/** + * The various types of resource types available. Corresponds + * to the 'type' in package:type/entry. + */ +enum class ResourceType { + kAnim, + kAnimator, + kArray, + kAttr, + kAttrPrivate, + kBool, + kColor, + kDimen, + kDrawable, + kFraction, + kId, + kInteger, + kIntegerArray, + kInterpolator, + kLayout, + kMenu, + kMipmap, + kPlurals, + kRaw, + kString, + kStyle, + kStyleable, + kTransition, + kXml, +}; + +StringPiece16 toString(ResourceType type); + +/** + * Returns a pointer to a valid ResourceType, or nullptr if + * the string was invalid. + */ +const ResourceType* parseResourceType(const StringPiece16& str); + +/** + * A resource's name. This can uniquely identify + * a resource in the ResourceTable. + */ +struct ResourceName { + std::u16string package; + ResourceType type; + std::u16string entry; + + bool isValid() const; + bool operator<(const ResourceName& rhs) const; + bool operator==(const ResourceName& rhs) const; + bool operator!=(const ResourceName& rhs) const; +}; + +/** + * Same as ResourceName, but uses StringPieces instead. + * Use this if you need to avoid copying and know that + * the lifetime of this object is shorter than that + * of the original string. + */ +struct ResourceNameRef { + StringPiece16 package; + ResourceType type; + StringPiece16 entry; + + ResourceNameRef() = default; + ResourceNameRef(const ResourceNameRef&) = default; + ResourceNameRef(ResourceNameRef&&) = default; + ResourceNameRef(const ResourceName& rhs); + ResourceNameRef(const StringPiece16& p, ResourceType t, const StringPiece16& e); + ResourceNameRef& operator=(const ResourceNameRef& rhs) = default; + ResourceNameRef& operator=(ResourceNameRef&& rhs) = default; + ResourceNameRef& operator=(const ResourceName& rhs); + + ResourceName toResourceName() const; + bool isValid() const; + + bool operator<(const ResourceNameRef& rhs) const; + bool operator==(const ResourceNameRef& rhs) const; + bool operator!=(const ResourceNameRef& rhs) const; +}; + +/** + * A binary identifier representing a resource. Internally it + * is a 32bit integer split as follows: + * + * 0xPPTTEEEE + * + * PP: 8 bit package identifier. 0x01 is reserved for system + * and 0x7f is reserved for the running app. + * TT: 8 bit type identifier. 0x00 is invalid. + * EEEE: 16 bit entry identifier. + */ +struct ResourceId { + uint32_t id; + + ResourceId(); + ResourceId(const ResourceId& rhs); + ResourceId(uint32_t resId); + ResourceId(size_t p, size_t t, size_t e); + + bool isValid() const; + uint8_t packageId() const; + uint8_t typeId() const; + uint16_t entryId() const; + bool operator<(const ResourceId& rhs) const; + bool operator==(const ResourceId& rhs) const; +}; + +// +// ResourceId implementation. +// + +inline ResourceId::ResourceId() : id(0) { +} + +inline ResourceId::ResourceId(const ResourceId& rhs) : id(rhs.id) { +} + +inline ResourceId::ResourceId(uint32_t resId) : id(resId) { +} + +inline ResourceId::ResourceId(size_t p, size_t t, size_t e) : id(0) { + if (p > std::numeric_limits<uint8_t>::max() || + t > std::numeric_limits<uint8_t>::max() || + e > std::numeric_limits<uint16_t>::max()) { + // This will leave the ResourceId in an invalid state. + return; + } + + id = (static_cast<uint8_t>(p) << 24) | + (static_cast<uint8_t>(t) << 16) | + static_cast<uint16_t>(e); +} + +inline bool ResourceId::isValid() const { + return (id & 0xff000000u) != 0 && (id & 0x00ff0000u) != 0; +} + +inline uint8_t ResourceId::packageId() const { + return static_cast<uint8_t>(id >> 24); +} + +inline uint8_t ResourceId::typeId() const { + return static_cast<uint8_t>(id >> 16); +} + +inline uint16_t ResourceId::entryId() const { + return static_cast<uint16_t>(id); +} + +inline bool ResourceId::operator<(const ResourceId& rhs) const { + return id < rhs.id; +} + +inline bool ResourceId::operator==(const ResourceId& rhs) const { + return id == rhs.id; +} + +inline ::std::ostream& operator<<(::std::ostream& out, + const ResourceId& resId) { + std::ios_base::fmtflags oldFlags = out.flags(); + char oldFill = out.fill(); + out << "0x" << std::internal << std::setfill('0') << std::setw(8) + << std::hex << resId.id; + out.flags(oldFlags); + out.fill(oldFill); + return out; +} + +// +// ResourceType implementation. +// + +inline ::std::ostream& operator<<(::std::ostream& out, const ResourceType& val) { + return out << toString(val); +} + +// +// ResourceName implementation. +// + +inline bool ResourceName::isValid() const { + return !package.empty() && !entry.empty(); +} + +inline bool ResourceName::operator<(const ResourceName& rhs) const { + return std::tie(package, type, entry) + < std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceName::operator==(const ResourceName& rhs) const { + return std::tie(package, type, entry) + == std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceName::operator!=(const ResourceName& rhs) const { + return std::tie(package, type, entry) + != std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline ::std::ostream& operator<<(::std::ostream& out, const ResourceName& name) { + if (!name.package.empty()) { + out << name.package << ":"; + } + return out << name.type << "/" << name.entry; +} + + +// +// ResourceNameRef implementation. +// + +inline ResourceNameRef::ResourceNameRef(const ResourceName& rhs) : + package(rhs.package), type(rhs.type), entry(rhs.entry) { +} + +inline ResourceNameRef::ResourceNameRef(const StringPiece16& p, ResourceType t, + const StringPiece16& e) : + package(p), type(t), entry(e) { +} + +inline ResourceNameRef& ResourceNameRef::operator=(const ResourceName& rhs) { + package = rhs.package; + type = rhs.type; + entry = rhs.entry; + return *this; +} + +inline ResourceName ResourceNameRef::toResourceName() const { + return { package.toString(), type, entry.toString() }; +} + +inline bool ResourceNameRef::isValid() const { + return !package.empty() && !entry.empty(); +} + +inline bool ResourceNameRef::operator<(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + < std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceNameRef::operator==(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + == std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceNameRef::operator!=(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + != std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline ::std::ostream& operator<<(::std::ostream& out, const ResourceNameRef& name) { + if (!name.package.empty()) { + out << name.package << ":"; + } + return out << name.type << "/" << name.entry; +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_H diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp new file mode 100644 index 0000000..13f916b --- /dev/null +++ b/tools/aapt2/ResourceParser.cpp @@ -0,0 +1,1401 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Logger.h" +#include "ResourceParser.h" +#include "ResourceValues.h" +#include "ScopedXmlPullParser.h" +#include "SourceXmlPullParser.h" +#include "Util.h" +#include "XliffXmlPullParser.h" + +#include <sstream> + +namespace aapt { + +void ResourceParser::extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry) { + const char16_t* start = str.data(); + const char16_t* end = start + str.size(); + const char16_t* current = start; + while (current != end) { + if (outType->size() == 0 && *current == u'/') { + outType->assign(start, current - start); + start = current + 1; + } else if (outPackage->size() == 0 && *current == u':') { + outPackage->assign(start, current - start); + start = current + 1; + } + current++; + } + outEntry->assign(start, end - start); +} + +bool ResourceParser::tryParseReference(const StringPiece16& str, ResourceNameRef* outRef, + bool* outCreate, bool* outPrivate) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + if (trimmedStr.data()[0] == u'@') { + size_t offset = 1; + *outCreate = false; + if (trimmedStr.data()[1] == u'+') { + *outCreate = true; + offset += 1; + } else if (trimmedStr.data()[1] == u'*') { + *outPrivate = true; + offset += 1; + } + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + extractResourceName(trimmedStr.substr(offset, trimmedStr.size() - offset), + &package, &type, &entry); + + const ResourceType* parsedType = parseResourceType(type); + if (!parsedType) { + return false; + } + + if (*outCreate && *parsedType != ResourceType::kId) { + return false; + } + + outRef->package = package; + outRef->type = *parsedType; + outRef->entry = entry; + return true; + } + return false; +} + +bool ResourceParser::tryParseAttributeReference(const StringPiece16& str, + ResourceNameRef* outRef) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + if (*trimmedStr.data() == u'?') { + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + extractResourceName(trimmedStr.substr(1, trimmedStr.size() - 1), &package, &type, &entry); + + if (!type.empty() && type != u"attr") { + return false; + } + + outRef->package = package; + outRef->type = ResourceType::kAttr; + outRef->entry = entry; + return true; + } + return false; +} + +/* + * Style parent's are a bit different. We accept the following formats: + * + * @[package:]style/<entry> + * ?[package:]style/<entry> + * <package>:[style/]<entry> + * [package:style/]<entry> + */ +bool ResourceParser::parseStyleParentReference(const StringPiece16& str, Reference* outReference, + std::string* outError) { + if (str.empty()) { + return true; + } + + StringPiece16 name = str; + + bool hasLeadingIdentifiers = false; + bool privateRef = false; + + // Skip over these identifiers. A style's parent is a normal reference. + if (name.data()[0] == u'@' || name.data()[0] == u'?') { + hasLeadingIdentifiers = true; + name = name.substr(1, name.size() - 1); + if (name.data()[0] == u'*') { + privateRef = true; + name = name.substr(1, name.size() - 1); + } + } + + ResourceNameRef ref; + ref.type = ResourceType::kStyle; + + StringPiece16 typeStr; + extractResourceName(name, &ref.package, &typeStr, &ref.entry); + if (!typeStr.empty()) { + // If we have a type, make sure it is a Style. + const ResourceType* parsedType = parseResourceType(typeStr); + if (!parsedType || *parsedType != ResourceType::kStyle) { + std::stringstream err; + err << "invalid resource type '" << typeStr << "' for parent of style"; + *outError = err.str(); + return false; + } + } else { + // No type was defined, this should not have a leading identifier. + if (hasLeadingIdentifiers) { + std::stringstream err; + err << "invalid parent reference '" << str << "'"; + *outError = err.str(); + return false; + } + } + + if (!hasLeadingIdentifiers && ref.package.empty() && !typeStr.empty()) { + std::stringstream err; + err << "invalid parent reference '" << str << "'"; + *outError = err.str(); + return false; + } + + outReference->name = ref.toResourceName(); + outReference->privateReference = privateRef; + return true; +} + +std::unique_ptr<Reference> ResourceParser::tryParseReference(const StringPiece16& str, + bool* outCreate) { + ResourceNameRef ref; + bool privateRef = false; + if (tryParseReference(str, &ref, outCreate, &privateRef)) { + std::unique_ptr<Reference> value = util::make_unique<Reference>(ref); + value->privateReference = privateRef; + return value; + } + + if (tryParseAttributeReference(str, &ref)) { + *outCreate = false; + return util::make_unique<Reference>(ref, Reference::Type::kAttribute); + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseNullOrEmpty(const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + android::Res_value value = {}; + if (trimmedStr == u"@null") { + // TYPE_NULL with data set to 0 is interpreted by the runtime as an error. + // Instead we set the data type to TYPE_REFERENCE with a value of 0. + value.dataType = android::Res_value::TYPE_REFERENCE; + } else if (trimmedStr == u"@empty") { + // TYPE_NULL with value of DATA_NULL_EMPTY is handled fine by the runtime. + value.dataType = android::Res_value::TYPE_NULL; + value.data = android::Res_value::DATA_NULL_EMPTY; + } else { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseEnumSymbol(const Attribute& enumAttr, + const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + for (const auto& entry : enumAttr.symbols) { + // Enum symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& enumSymbolResourceName = entry.symbol.name; + if (trimmedStr == enumSymbolResourceName.entry) { + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_INT_DEC; + value.data = entry.value; + return util::make_unique<BinaryPrimitive>(value); + } + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFlagSymbol(const Attribute& flagAttr, + const StringPiece16& str) { + android::Res_value flags = {}; + flags.dataType = android::Res_value::TYPE_INT_DEC; + + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + + bool flagSet = false; + for (const auto& entry : flagAttr.symbols) { + // Flag symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& flagSymbolResourceName = entry.symbol.name; + if (trimmedPart == flagSymbolResourceName.entry) { + flags.data |= entry.value; + flagSet = true; + break; + } + } + + if (!flagSet) { + return {}; + } + } + return util::make_unique<BinaryPrimitive>(flags); +} + +static uint32_t parseHex(char16_t c, bool* outError) { + if (c >= u'0' && c <= u'9') { + return c - u'0'; + } else if (c >= u'a' && c <= u'f') { + return c - u'a' + 0xa; + } else if (c >= u'A' && c <= u'F') { + return c - u'A' + 0xa; + } else { + *outError = true; + return 0xffffffffu; + } +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseColor(const StringPiece16& str) { + StringPiece16 colorStr(util::trimWhitespace(str)); + const char16_t* start = colorStr.data(); + const size_t len = colorStr.size(); + if (len == 0 || start[0] != u'#') { + return {}; + } + + android::Res_value value = {}; + bool error = false; + if (len == 4) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB4; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[1], &error) << 16; + value.data |= parseHex(start[2], &error) << 12; + value.data |= parseHex(start[2], &error) << 8; + value.data |= parseHex(start[3], &error) << 4; + value.data |= parseHex(start[3], &error); + } else if (len == 5) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB4; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[1], &error) << 24; + value.data |= parseHex(start[2], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[3], &error) << 8; + value.data |= parseHex(start[4], &error) << 4; + value.data |= parseHex(start[4], &error); + } else if (len == 7) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB8; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[4], &error) << 8; + value.data |= parseHex(start[5], &error) << 4; + value.data |= parseHex(start[6], &error); + } else if (len == 9) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB8; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[2], &error) << 24; + value.data |= parseHex(start[3], &error) << 20; + value.data |= parseHex(start[4], &error) << 16; + value.data |= parseHex(start[5], &error) << 12; + value.data |= parseHex(start[6], &error) << 8; + value.data |= parseHex(start[7], &error) << 4; + value.data |= parseHex(start[8], &error); + } else { + return {}; + } + return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseBool(const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + uint32_t data = 0; + if (trimmedStr == u"true" || trimmedStr == u"TRUE") { + data = 0xffffffffu; + } else if (trimmedStr != u"false" && trimmedStr != u"FALSE") { + return {}; + } + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_INT_BOOLEAN; + value.data = data; + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseInt(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToInt(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFloat(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToFloat(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +uint32_t ResourceParser::androidTypeToAttributeTypeMask(uint16_t type) { + switch (type) { + case android::Res_value::TYPE_NULL: + case android::Res_value::TYPE_REFERENCE: + case android::Res_value::TYPE_ATTRIBUTE: + case android::Res_value::TYPE_DYNAMIC_REFERENCE: + return android::ResTable_map::TYPE_REFERENCE; + + case android::Res_value::TYPE_STRING: + return android::ResTable_map::TYPE_STRING; + + case android::Res_value::TYPE_FLOAT: + return android::ResTable_map::TYPE_FLOAT; + + case android::Res_value::TYPE_DIMENSION: + return android::ResTable_map::TYPE_DIMENSION; + + case android::Res_value::TYPE_FRACTION: + return android::ResTable_map::TYPE_FRACTION; + + case android::Res_value::TYPE_INT_DEC: + case android::Res_value::TYPE_INT_HEX: + return android::ResTable_map::TYPE_INTEGER | + android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_FLAGS; + + case android::Res_value::TYPE_INT_BOOLEAN: + return android::ResTable_map::TYPE_BOOLEAN; + + case android::Res_value::TYPE_INT_COLOR_ARGB8: + case android::Res_value::TYPE_INT_COLOR_RGB8: + case android::Res_value::TYPE_INT_COLOR_ARGB4: + case android::Res_value::TYPE_INT_COLOR_RGB4: + return android::ResTable_map::TYPE_COLOR; + + default: + return 0; + }; +} + +std::unique_ptr<Item> ResourceParser::parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, + std::function<void(const ResourceName&)> onCreateReference) { + std::unique_ptr<BinaryPrimitive> nullOrEmpty = tryParseNullOrEmpty(value); + if (nullOrEmpty) { + return std::move(nullOrEmpty); + } + + bool create = false; + std::unique_ptr<Reference> reference = tryParseReference(value, &create); + if (reference) { + if (create && onCreateReference) { + onCreateReference(reference->name); + } + return std::move(reference); + } + + if (typeMask & android::ResTable_map::TYPE_COLOR) { + // Try parsing this as a color. + std::unique_ptr<BinaryPrimitive> color = tryParseColor(value); + if (color) { + return std::move(color); + } + } + + if (typeMask & android::ResTable_map::TYPE_BOOLEAN) { + // Try parsing this as a boolean. + std::unique_ptr<BinaryPrimitive> boolean = tryParseBool(value); + if (boolean) { + return std::move(boolean); + } + } + + if (typeMask & android::ResTable_map::TYPE_INTEGER) { + // Try parsing this as an integer. + std::unique_ptr<BinaryPrimitive> integer = tryParseInt(value); + if (integer) { + return std::move(integer); + } + } + + const uint32_t floatMask = android::ResTable_map::TYPE_FLOAT | + android::ResTable_map::TYPE_DIMENSION | + android::ResTable_map::TYPE_FRACTION; + if (typeMask & floatMask) { + // Try parsing this as a float. + std::unique_ptr<BinaryPrimitive> floatingPoint = tryParseFloat(value); + if (floatingPoint) { + if (typeMask & androidTypeToAttributeTypeMask(floatingPoint->value.dataType)) { + return std::move(floatingPoint); + } + } + } + return {}; +} + +/** + * We successively try to parse the string as a resource type that the Attribute + * allows. + */ +std::unique_ptr<Item> ResourceParser::parseItemForAttribute( + const StringPiece16& str, const Attribute& attr, + std::function<void(const ResourceName&)> onCreateReference) { + const uint32_t typeMask = attr.typeMask; + std::unique_ptr<Item> value = parseItemForAttribute(str, typeMask, onCreateReference); + if (value) { + return value; + } + + if (typeMask & android::ResTable_map::TYPE_ENUM) { + // Try parsing this as an enum. + std::unique_ptr<BinaryPrimitive> enumValue = tryParseEnumSymbol(attr, str); + if (enumValue) { + return std::move(enumValue); + } + } + + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + // Try parsing this as a flag. + std::unique_ptr<BinaryPrimitive> flagValue = tryParseFlagSymbol(attr, str); + if (flagValue) { + return std::move(flagValue); + } + } + return {}; +} + +ResourceParser::ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config, + const std::shared_ptr<XmlPullParser>& parser) : + mTable(table), mSource(source), mConfig(config), mLogger(source), + mParser(std::make_shared<XliffXmlPullParser>(parser)) { +} + +/** + * Build a string from XML that converts nested elements into Span objects. + */ +bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString, + StyleString* outStyleString) { + std::vector<Span> spanStack; + + outRawString->clear(); + outStyleString->spans.clear(); + util::StringBuilder builder; + size_t depth = 1; + while (XmlPullParser::isGoodEvent(parser->next())) { + const XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kEndElement) { + depth--; + if (depth == 0) { + break; + } + + spanStack.back().lastChar = builder.str().size(); + outStyleString->spans.push_back(spanStack.back()); + spanStack.pop_back(); + + } else if (event == XmlPullParser::Event::kText) { + // TODO(adamlesinski): Verify format strings. + outRawString->append(parser->getText()); + builder.append(parser->getText()); + + } else if (event == XmlPullParser::Event::kStartElement) { + if (parser->getElementNamespace().size() > 0) { + mLogger.warn(parser->getLineNumber()) + << "skipping element '" + << parser->getElementName() + << "' with unknown namespace '" + << parser->getElementNamespace() + << "'." + << std::endl; + XmlPullParser::skipCurrentElement(parser); + continue; + } + depth++; + + // Build a span object out of the nested element. + std::u16string spanName = parser->getElementName(); + const auto endAttrIter = parser->endAttributes(); + for (auto attrIter = parser->beginAttributes(); attrIter != endAttrIter; ++attrIter) { + spanName += u";"; + spanName += attrIter->name; + spanName += u"="; + spanName += attrIter->value; + } + + if (builder.str().size() > std::numeric_limits<uint32_t>::max()) { + mLogger.error(parser->getLineNumber()) + << "style string '" + << builder.str() + << "' is too long." + << std::endl; + return false; + } + spanStack.push_back(Span{ spanName, static_cast<uint32_t>(builder.str().size()) }); + + } else if (event == XmlPullParser::Event::kComment) { + // Skip + } else { + mLogger.warn(parser->getLineNumber()) + << "unknown event " + << event + << "." + << std::endl; + } + } + assert(spanStack.empty() && "spans haven't been fully processed"); + + outStyleString->str = builder.str(); + return true; +} + +bool ResourceParser::parse() { + while (XmlPullParser::isGoodEvent(mParser->next())) { + if (mParser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser parser(mParser.get()); + if (!parser.getElementNamespace().empty() || + parser.getElementName() != u"resources") { + mLogger.error(parser.getLineNumber()) + << "root element must be <resources> in the global namespace." + << std::endl; + return false; + } + + if (!parseResources(&parser)) { + return false; + } + } + + if (mParser->getEvent() == XmlPullParser::Event::kBadDocument) { + mLogger.error(mParser->getLineNumber()) + << mParser->getLastError() + << std::endl; + return false; + } + return true; +} + +bool ResourceParser::parseResources(XmlPullParser* parser) { + bool success = true; + + std::u16string comment; + while (XmlPullParser::isGoodEvent(parser->next())) { + const XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kComment) { + comment = parser->getComment(); + continue; + } + + if (event == XmlPullParser::Event::kText) { + if (!util::trimWhitespace(parser->getText()).empty()) { + comment = u""; + } + continue; + } + + if (event != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (!childParser.getElementNamespace().empty()) { + // Skip unknown namespace. + continue; + } + + StringPiece16 name = childParser.getElementName(); + if (name == u"skip" || name == u"eat-comment") { + continue; + } + + if (name == u"private-symbols") { + // Handle differently. + mLogger.note(childParser.getLineNumber()) + << "got a <private-symbols> tag." + << std::endl; + continue; + } + + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"name"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<" << name << "> tag must have a 'name' attribute." + << std::endl; + success = false; + continue; + } + + // Copy because our iterator will go out of scope when + // we parse more XML. + std::u16string attributeName = attrIter->value; + + if (name == u"item") { + // Items simply have their type encoded in the type attribute. + auto typeIter = childParser.findAttribute(u"", u"type"); + if (typeIter == endAttrIter || typeIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<item> must have a 'type' attribute." + << std::endl; + success = false; + continue; + } + name = typeIter->value; + } + + if (name == u"id") { + success &= mTable->addResource(ResourceNameRef{ {}, ResourceType::kId, attributeName }, + {}, mSource.line(childParser.getLineNumber()), + util::make_unique<Id>()); + } else if (name == u"string") { + success &= parseString(&childParser, + ResourceNameRef{ {}, ResourceType::kString, attributeName }); + } else if (name == u"color") { + success &= parseColor(&childParser, + ResourceNameRef{ {}, ResourceType::kColor, attributeName }); + } else if (name == u"drawable") { + success &= parseColor(&childParser, + ResourceNameRef{ {}, ResourceType::kDrawable, attributeName }); + } else if (name == u"bool") { + success &= parsePrimitive(&childParser, + ResourceNameRef{ {}, ResourceType::kBool, attributeName }); + } else if (name == u"integer") { + success &= parsePrimitive( + &childParser, + ResourceNameRef{ {}, ResourceType::kInteger, attributeName }); + } else if (name == u"dimen") { + success &= parsePrimitive(&childParser, + ResourceNameRef{ {}, ResourceType::kDimen, attributeName }); + } else if (name == u"fraction") { +// success &= parsePrimitive( +// &childParser, +// ResourceNameRef{ {}, ResourceType::kFraction, attributeName }); + } else if (name == u"style") { + success &= parseStyle(&childParser, + ResourceNameRef{ {}, ResourceType::kStyle, attributeName }); + } else if (name == u"plurals") { + success &= parsePlural(&childParser, + ResourceNameRef{ {}, ResourceType::kPlurals, attributeName }); + } else if (name == u"array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_ANY); + } else if (name == u"string-array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_STRING); + } else if (name == u"integer-array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_INTEGER); + } else if (name == u"public") { + success &= parsePublic(&childParser, attributeName); + } else if (name == u"declare-styleable") { + success &= parseDeclareStyleable( + &childParser, + ResourceNameRef{ {}, ResourceType::kStyleable, attributeName }); + } else if (name == u"attr") { + success &= parseAttr(&childParser, + ResourceNameRef{ {}, ResourceType::kAttr, attributeName }); + } else if (name == u"bag") { + } else if (name == u"public-padding") { + } else if (name == u"java-symbol") { + } else if (name == u"add-resource") { + } + } + + if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { + mLogger.error(parser->getLineNumber()) + << parser->getLastError() + << std::endl; + return false; + } + return success; +} + + + +enum { + kAllowRawString = true, + kNoRawString = false +}; + +/** + * Reads the entire XML subtree and attempts to parse it as some Item, + * with typeMask denoting which items it can be. If allowRawValue is + * true, a RawString is returned if the XML couldn't be parsed as + * an Item. If allowRawValue is false, nullptr is returned in this + * case. + */ +std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, uint32_t typeMask, + bool allowRawValue) { + const size_t beginXmlLine = parser->getLineNumber(); + + std::u16string rawValue; + StyleString styleString; + if (!flattenXmlSubtree(parser, &rawValue, &styleString)) { + return {}; + } + + StringPool& pool = mTable->getValueStringPool(); + + if (!styleString.spans.empty()) { + // This can only be a StyledString. + return util::make_unique<StyledString>( + pool.makeRef(styleString, StringPool::Context{ 1, mConfig })); + } + + auto onCreateReference = [&](const ResourceName& name) { + // name.package can be empty here, as it will assume the package name of the table. + mTable->addResource(name, {}, mSource.line(beginXmlLine), util::make_unique<Id>()); + }; + + // Process the raw value. + std::unique_ptr<Item> processedItem = parseItemForAttribute(rawValue, typeMask, + onCreateReference); + if (processedItem) { + // Fix up the reference. + visitFunc<Reference>(*processedItem, [&](Reference& ref) { + if (!ref.name.package.empty()) { + // The package name was set, so lookup its alias. + parser->applyPackageAlias(&ref.name.package, mTable->getPackage()); + } else { + // The package name was left empty, so it assumes the default package + // without alias lookup. + ref.name.package = mTable->getPackage(); + } + }); + return processedItem; + } + + // Try making a regular string. + if (typeMask & android::ResTable_map::TYPE_STRING) { + // Use the trimmed, escaped string. + return util::make_unique<String>( + pool.makeRef(styleString.str, StringPool::Context{ 1, mConfig })); + } + + // We can't parse this so return a RawString if we are allowed. + if (allowRawValue) { + return util::make_unique<RawString>( + pool.makeRef(rawValue, StringPool::Context{ 1, mConfig })); + } + return {}; +} + +bool ResourceParser::parseString(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + // Mark the string as untranslateable if needed. + const auto endAttrIter = parser->endAttributes(); + auto attrIter = parser->findAttribute(u"", u"untranslateable"); + // bool untranslateable = attrIter != endAttrIter; + // TODO(adamlesinski): Do something with this (mark the string). + + // Deal with the product. + attrIter = parser->findAttribute(u"", u"product"); + if (attrIter != endAttrIter) { + if (attrIter->value != u"default" && attrIter->value != u"phone") { + // TODO(adamlesinski): Match products. + return true; + } + } + + std::unique_ptr<Item> processedItem = parseXml(parser, android::ResTable_map::TYPE_STRING, + kNoRawString); + if (!processedItem) { + mLogger.error(source.line) + << "not a valid string." + << std::endl; + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(processedItem)); +} + +bool ResourceParser::parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + std::unique_ptr<Item> item = parseXml(parser, android::ResTable_map::TYPE_COLOR, kNoRawString); + if (!item) { + mLogger.error(source.line) << "invalid color." << std::endl; + return false; + } + return mTable->addResource(resourceName, mConfig, source, std::move(item)); +} + +bool ResourceParser::parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + uint32_t typeMask = 0; + switch (resourceName.type) { + case ResourceType::kInteger: + typeMask |= android::ResTable_map::TYPE_INTEGER; + break; + + case ResourceType::kDimen: + typeMask |= android::ResTable_map::TYPE_DIMENSION + | android::ResTable_map::TYPE_FLOAT + | android::ResTable_map::TYPE_FRACTION; + break; + + case ResourceType::kBool: + typeMask |= android::ResTable_map::TYPE_BOOLEAN; + break; + + default: + assert(false); + break; + } + + std::unique_ptr<Item> item = parseXml(parser, typeMask, kNoRawString); + if (!item) { + mLogger.error(source.line) + << "invalid " + << resourceName.type + << "." + << std::endl; + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(item)); +} + +bool ResourceParser::parsePublic(XmlPullParser* parser, const StringPiece16& name) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + const auto endAttrIter = parser->endAttributes(); + const auto typeAttrIter = parser->findAttribute(u"", u"type"); + if (typeAttrIter == endAttrIter || typeAttrIter->value.empty()) { + mLogger.error(source.line) + << "<public> must have a 'type' attribute." + << std::endl; + return false; + } + + const ResourceType* parsedType = parseResourceType(typeAttrIter->value); + if (!parsedType) { + mLogger.error(source.line) + << "invalid resource type '" + << typeAttrIter->value + << "' in <public>." + << std::endl; + return false; + } + + ResourceNameRef resourceName { {}, *parsedType, name }; + ResourceId resourceId; + + const auto idAttrIter = parser->findAttribute(u"", u"id"); + if (idAttrIter != endAttrIter && !idAttrIter->value.empty()) { + android::Res_value val; + bool result = android::ResTable::stringToInt(idAttrIter->value.data(), + idAttrIter->value.size(), &val); + resourceId.id = val.data; + if (!result || !resourceId.isValid()) { + mLogger.error(source.line) + << "invalid resource ID '" + << idAttrIter->value + << "' in <public>." + << std::endl; + return false; + } + } + + if (*parsedType == ResourceType::kId) { + // An ID marked as public is also the definition of an ID. + mTable->addResource(resourceName, {}, source, util::make_unique<Id>()); + } + + return mTable->markPublic(resourceName, resourceId, source); +} + +static uint32_t parseFormatType(const StringPiece16& piece) { + if (piece == u"reference") return android::ResTable_map::TYPE_REFERENCE; + else if (piece == u"string") return android::ResTable_map::TYPE_STRING; + else if (piece == u"integer") return android::ResTable_map::TYPE_INTEGER; + else if (piece == u"boolean") return android::ResTable_map::TYPE_BOOLEAN; + else if (piece == u"color") return android::ResTable_map::TYPE_COLOR; + else if (piece == u"float") return android::ResTable_map::TYPE_FLOAT; + else if (piece == u"dimension") return android::ResTable_map::TYPE_DIMENSION; + else if (piece == u"fraction") return android::ResTable_map::TYPE_FRACTION; + else if (piece == u"enum") return android::ResTable_map::TYPE_ENUM; + else if (piece == u"flags") return android::ResTable_map::TYPE_FLAGS; + return 0; +} + +static uint32_t parseFormatAttribute(const StringPiece16& str) { + uint32_t mask = 0; + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + uint32_t type = parseFormatType(trimmedPart); + if (type == 0) { + return 0; + } + mask |= type; + } + return mask; +} + +bool ResourceParser::parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + ResourceName actualName = resourceName.toResourceName(); + std::unique_ptr<Attribute> attr = parseAttrImpl(parser, &actualName, false); + if (!attr) { + return false; + } + return mTable->addResource(actualName, mConfig, source, std::move(attr)); +} + +std::unique_ptr<Attribute> ResourceParser::parseAttrImpl(XmlPullParser* parser, + ResourceName* resourceName, + bool weak) { + uint32_t typeMask = 0; + + const auto endAttrIter = parser->endAttributes(); + const auto formatAttrIter = parser->findAttribute(u"", u"format"); + if (formatAttrIter != endAttrIter) { + typeMask = parseFormatAttribute(formatAttrIter->value); + if (typeMask == 0) { + mLogger.error(parser->getLineNumber()) + << "invalid attribute format '" + << formatAttrIter->value + << "'." + << std::endl; + return {}; + } + } + + // If this is a declaration, the package name may be in the name. Separate these out. + // Eg. <attr name="android:text" /> + // No format attribute is allowed. + if (weak && formatAttrIter == endAttrIter) { + StringPiece16 package, type, name; + extractResourceName(resourceName->entry, &package, &type, &name); + if (type.empty() && !package.empty()) { + resourceName->package = package.toString(); + resourceName->entry = name.toString(); + } + } + + std::vector<Attribute::Symbol> items; + + bool error = false; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + const std::u16string& name = childParser.getElementName(); + if (!childParser.getElementNamespace().empty() + || (name != u"flag" && name != u"enum")) { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << name + << "> in <attr>." + << std::endl; + error = true; + continue; + } + + if (name == u"enum") { + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + mLogger.error(childParser.getLineNumber()) + << "can not define an <enum>; already defined a <flag>." + << std::endl; + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_ENUM; + } else if (name == u"flag") { + if (typeMask & android::ResTable_map::TYPE_ENUM) { + mLogger.error(childParser.getLineNumber()) + << "can not define a <flag>; already defined an <enum>." + << std::endl; + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_FLAGS; + } + + Attribute::Symbol item; + if (parseEnumOrFlagItem(&childParser, name, &item)) { + if (!mTable->addResource(item.symbol.name, mConfig, + mSource.line(childParser.getLineNumber()), + util::make_unique<Id>())) { + error = true; + } else { + items.push_back(std::move(item)); + } + } else { + error = true; + } + } + + if (error) { + return {}; + } + + std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(weak); + attr->symbols.swap(items); + attr->typeMask = typeMask ? typeMask : uint32_t(android::ResTable_map::TYPE_ANY); + return attr; +} + +bool ResourceParser::parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, + Attribute::Symbol* outSymbol) { + const auto attrIterEnd = parser->endAttributes(); + const auto nameAttrIter = parser->findAttribute(u"", u"name"); + if (nameAttrIter == attrIterEnd || nameAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "no attribute 'name' found for tag <" << tag << ">." + << std::endl; + return false; + } + + const auto valueAttrIter = parser->findAttribute(u"", u"value"); + if (valueAttrIter == attrIterEnd || valueAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "no attribute 'value' found for tag <" << tag << ">." + << std::endl; + return false; + } + + android::Res_value val; + if (!android::ResTable::stringToInt(valueAttrIter->value.data(), + valueAttrIter->value.size(), &val)) { + mLogger.error(parser->getLineNumber()) + << "invalid value '" + << valueAttrIter->value + << "' for <" << tag << ">; must be an integer." + << std::endl; + return false; + } + + outSymbol->symbol.name = ResourceName { + mTable->getPackage(), ResourceType::kId, nameAttrIter->value }; + outSymbol->value = val.data; + return true; +} + +static bool parseXmlAttributeName(StringPiece16 str, ResourceName* outName) { + str = util::trimWhitespace(str); + const char16_t* const start = str.data(); + const char16_t* const end = start + str.size(); + const char16_t* p = start; + + StringPiece16 package; + StringPiece16 name; + while (p != end) { + if (*p == u':') { + package = StringPiece16(start, p - start); + name = StringPiece16(p + 1, end - (p + 1)); + break; + } + p++; + } + + outName->package = package.toString(); + outName->type = ResourceType::kAttr; + if (name.size() == 0) { + outName->entry = str.toString(); + } else { + outName->entry = name.toString(); + } + return true; +} + +bool ResourceParser::parseUntypedItem(XmlPullParser* parser, Style& style) { + const auto endAttrIter = parser->endAttributes(); + const auto nameAttrIter = parser->findAttribute(u"", u"name"); + if (nameAttrIter == endAttrIter || nameAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "<item> must have a 'name' attribute." + << std::endl; + return false; + } + + ResourceName key; + if (!parseXmlAttributeName(nameAttrIter->value, &key)) { + mLogger.error(parser->getLineNumber()) + << "invalid attribute name '" + << nameAttrIter->value + << "'." + << std::endl; + return false; + } + + if (!key.package.empty()) { + // We have a package name set, so lookup its alias. + parser->applyPackageAlias(&key.package, mTable->getPackage()); + } else { + // The package name was omitted, so use the default package name with + // no alias lookup. + key.package = mTable->getPackage(); + } + + std::unique_ptr<Item> value = parseXml(parser, 0, kAllowRawString); + if (!value) { + return false; + } + + style.entries.push_back(Style::Entry{ Reference(key), std::move(value) }); + return true; +} + +bool ResourceParser::parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Style> style = util::make_unique<Style>(); + + const auto endAttrIter = parser->endAttributes(); + const auto parentAttrIter = parser->findAttribute(u"", u"parent"); + if (parentAttrIter != endAttrIter) { + std::string errStr; + if (!parseStyleParentReference(parentAttrIter->value, &style->parent, &errStr)) { + mLogger.error(source.line) << errStr << "." << std::endl; + return false; + } + + if (!style->parent.name.package.empty()) { + // Try to interpret the package name as an alias. These take precedence. + parser->applyPackageAlias(&style->parent.name.package, mTable->getPackage()); + } else { + // If no package is specified, this can not be an alias and is the local package. + style->parent.name.package = mTable->getPackage(); + } + } else { + // No parent was specified, so try inferring it from the style name. + std::u16string styleName = resourceName.entry.toString(); + size_t pos = styleName.find_last_of(u'.'); + if (pos != std::string::npos) { + style->parentInferred = true; + style->parent.name.package = mTable->getPackage(); + style->parent.name.type = ResourceType::kStyle; + style->parent.name.entry = styleName.substr(0, pos); + } + } + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + const std::u16string& name = childParser.getElementName(); + if (name == u"item") { + success &= parseUntypedItem(&childParser, *style); + } else { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << name + << "> in <style> resource." + << std::endl; + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(style)); +} + +bool ResourceParser::parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, + uint32_t typeMask) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Array> array = util::make_unique<Array>(); + + bool error = false; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (childParser.getElementName() != u"item") { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << childParser.getElementName() + << "> in <array> resource." + << std::endl; + error = true; + continue; + } + + std::unique_ptr<Item> item = parseXml(&childParser, typeMask, kNoRawString); + if (!item) { + error = true; + continue; + } + array->items.emplace_back(std::move(item)); + } + + if (error) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(array)); +} + +bool ResourceParser::parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (!childParser.getElementNamespace().empty() || + childParser.getElementName() != u"item") { + success = false; + continue; + } + + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"quantity"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<item> in <plurals> requires attribute 'quantity'." + << std::endl; + success = false; + continue; + } + + StringPiece16 trimmedQuantity = util::trimWhitespace(attrIter->value); + size_t index = 0; + if (trimmedQuantity == u"zero") { + index = Plural::Zero; + } else if (trimmedQuantity == u"one") { + index = Plural::One; + } else if (trimmedQuantity == u"two") { + index = Plural::Two; + } else if (trimmedQuantity == u"few") { + index = Plural::Few; + } else if (trimmedQuantity == u"many") { + index = Plural::Many; + } else if (trimmedQuantity == u"other") { + index = Plural::Other; + } else { + mLogger.error(childParser.getLineNumber()) + << "<item> in <plural> has invalid value '" + << trimmedQuantity + << "' for attribute 'quantity'." + << std::endl; + success = false; + continue; + } + + if (plural->values[index]) { + mLogger.error(childParser.getLineNumber()) + << "duplicate quantity '" + << trimmedQuantity + << "'." + << std::endl; + success = false; + continue; + } + + if (!(plural->values[index] = parseXml(&childParser, android::ResTable_map::TYPE_STRING, + kNoRawString))) { + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(plural)); +} + +bool ResourceParser::parseDeclareStyleable(XmlPullParser* parser, + const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + const std::u16string& elementName = childParser.getElementName(); + if (elementName == u"attr") { + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"name"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<attr> tag must have a 'name' attribute." + << std::endl; + success = false; + continue; + } + + // Copy because our iterator will be invalidated. + ResourceName attrResourceName = { + mTable->getPackage(), + ResourceType::kAttr, + attrIter->value + }; + + std::unique_ptr<Attribute> attr = parseAttrImpl(&childParser, &attrResourceName, true); + if (!attr) { + success = false; + continue; + } + + styleable->entries.emplace_back(attrResourceName); + + // The package may have been corrected to another package. If that is so, + // we don't add the declaration. + if (attrResourceName.package == mTable->getPackage()) { + success &= mTable->addResource(attrResourceName, mConfig, + mSource.line(childParser.getLineNumber()), + std::move(attr)); + } + + } else if (elementName != u"eat-comment" && elementName != u"skip") { + mLogger.error(childParser.getLineNumber()) + << "<" + << elementName + << "> is not allowed inside <declare-styleable>." + << std::endl; + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(styleable)); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h new file mode 100644 index 0000000..7618999 --- /dev/null +++ b/tools/aapt2/ResourceParser.h @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_PARSER_H +#define AAPT_RESOURCE_PARSER_H + +#include "ConfigDescription.h" +#include "Logger.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" +#include "StringPool.h" +#include "XmlPullParser.h" + +#include <istream> +#include <memory> + +namespace aapt { + +/* + * Parses an XML file for resources and adds them to a ResourceTable. + */ +class ResourceParser { +public: + /* + * Extracts the package, type, and name from a string of the format: + * + * [package:]type/name + * + * where the package can be empty. Validation must be performed on each + * individual extracted piece to verify that the pieces are valid. + */ + static void extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry); + + /* + * Returns true if the string was parsed as a reference (@[+][package:]type/name), with + * `outReference` set to the parsed reference. + * + * If '+' was present in the reference, `outCreate` is set to true. + * If '*' was present in the reference, `outPrivate` is set to true. + */ + static bool tryParseReference(const StringPiece16& str, ResourceNameRef* outReference, + bool* outCreate, bool* outPrivate); + + /* + * Returns true if the string was parsed as an attribute reference (?[package:]type/name), + * with `outReference` set to the parsed reference. + */ + static bool tryParseAttributeReference(const StringPiece16& str, + ResourceNameRef* outReference); + + /* + * Returns true if the string `str` was parsed as a valid reference to a style. + * The format for a style parent is slightly more flexible than a normal reference: + * + * @[package:]style/<entry> or + * ?[package:]style/<entry> or + * <package>:[style/]<entry> + */ + static bool parseStyleParentReference(const StringPiece16& str, Reference* outReference, + std::string* outError); + + /* + * Returns a Reference object if the string was parsed as a resource or attribute reference, + * ( @[+][package:]type/name | ?[package:]type/name ) setting outCreate to true if + * the '+' was present in the string. + */ + static std::unique_ptr<Reference> tryParseReference(const StringPiece16& str, + bool* outCreate); + + /* + * Returns a BinaryPrimitve object representing @null or @empty if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseNullOrEmpty(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a color if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a boolean if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing an integer if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a floating point number + * (float, dimension, etc) if the string was parsed as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseFloat(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing an enum symbol if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseEnumSymbol(const Attribute& enumAttr, + const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a flag symbol if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute& enumAttr, + const StringPiece16& str); + /* + * Try to convert a string to an Item for the given attribute. The attribute will + * restrict what values the string can be converted to. + * The callback function onCreateReference is called when the parsed item is a + * reference to an ID that must be created (@+id/foo). + */ + static std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, const Attribute& attr, + std::function<void(const ResourceName&)> onCreateReference = {}); + + static std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, + std::function<void(const ResourceName&)> onCreateReference = {}); + + static uint32_t androidTypeToAttributeTypeMask(uint16_t type); + + ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config, const std::shared_ptr<XmlPullParser>& parser); + + ResourceParser(const ResourceParser&) = delete; // No copy. + + bool parse(); + +private: + /* + * Parses the XML subtree as a StyleString (flattened XML representation for strings + * with formatting). If successful, `outStyleString` + * contains the escaped and whitespace trimmed text, while `outRawString` + * contains the unescaped text. Returns true on success. + */ + bool flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString,\ + StyleString* outStyleString); + + /* + * Parses the XML subtree and converts it to an Item. The type of Item that can be + * parsed is denoted by the `typeMask`. If `allowRawValue` is true and the subtree + * can not be parsed as a regular Item, then a RawString is returned. Otherwise + * this returns nullptr. + */ + std::unique_ptr<Item> parseXml(XmlPullParser* parser, uint32_t typeMask, bool allowRawValue); + + bool parseResources(XmlPullParser* parser); + bool parseString(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parsePublic(XmlPullParser* parser, const StringPiece16& name); + bool parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName); + std::unique_ptr<Attribute> parseAttrImpl(XmlPullParser* parser, + ResourceName* resourceName, + bool weak); + bool parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, + Attribute::Symbol* outSymbol); + bool parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseUntypedItem(XmlPullParser* parser, Style& style); + bool parseDeclareStyleable(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, uint32_t typeMask); + bool parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName); + + std::shared_ptr<ResourceTable> mTable; + Source mSource; + ConfigDescription mConfig; + SourceLogger mLogger; + std::shared_ptr<XmlPullParser> mParser; +}; + +} // namespace aapt + +#endif // AAPT_RESOURCE_PARSER_H diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp new file mode 100644 index 0000000..a93d0ff --- /dev/null +++ b/tools/aapt2/ResourceParser_test.cpp @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + +TEST(ResourceParserReferenceTest, ParseReferenceWithNoPackage) { + ResourceNameRef expected = { {}, ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@color/foo", &actual, &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseReferenceWithPackage) { + ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@android:color/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseReferenceWithSurroundingWhitespace) { + ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"\t @android:color/foo\n \n\t", &actual, + &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseAutoCreateIdReference) { + ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@+android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_TRUE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParsePrivateReference) { + ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@*android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_TRUE(privateRef); +} + +TEST(ResourceParserReferenceTest, FailToParseAutoCreateNonIdReference) { + bool create = false; + bool privateRef = false; + ResourceNameRef actual; + EXPECT_FALSE(ResourceParser::tryParseReference(u"@+android:color/foo", &actual, &create, + &privateRef)); +} + +TEST(ResourceParserReferenceTest, ParseStyleParentReference) { + Reference ref; + std::string errStr; + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"@android:style/foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"@style/foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"?android:style/foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"?style/foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"android:style/foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"android:foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + + EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"foo", &ref, &errStr)); + EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); +} + +struct ResourceParserTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + } + + ::testing::AssertionResult testParse(const StringPiece& str) { + std::stringstream input(kXmlPreamble); + input << "<resources>\n" << str << "\n</resources>" << std::endl; + ResourceParser parser(mTable, Source{ "test" }, {}, + std::make_shared<SourceXmlPullParser>(input)); + if (parser.parse()) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure(); + } + + template <typename T> + const T* findResource(const ResourceNameRef& name, const ConfigDescription& config) { + using std::begin; + using std::end; + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(name); + if (!type || !entry) { + return nullptr; + } + + for (const auto& configValue : entry->values) { + if (configValue.config == config) { + return dynamic_cast<const T*>(configValue.value.get()); + } + } + return nullptr; + } + + template <typename T> + const T* findResource(const ResourceNameRef& name) { + return findResource<T>(name, {}); + } + + std::shared_ptr<ResourceTable> mTable; +}; + +TEST_F(ResourceParserTest, FailToParseWithNoRootResourcesElement) { + std::stringstream input(kXmlPreamble); + input << "<attr name=\"foo\"/>" << std::endl; + ResourceParser parser(mTable, {}, {}, std::make_shared<SourceXmlPullParser>(input)); + ASSERT_FALSE(parser.parse()); +} + +TEST_F(ResourceParserTest, ParseQuotedString) { + std::string input = "<string name=\"foo\"> \" hey there \" </string>"; + ASSERT_TRUE(testParse(input)); + + const String* str = findResource<String>(ResourceName{ + u"android", ResourceType::kString, u"foo"}); + ASSERT_NE(nullptr, str); + EXPECT_EQ(std::u16string(u" hey there "), *str->value); +} + +TEST_F(ResourceParserTest, ParseEscapedString) { + std::string input = "<string name=\"foo\">\\?123</string>"; + ASSERT_TRUE(testParse(input)); + + const String* str = findResource<String>(ResourceName{ + u"android", ResourceType::kString, u"foo" }); + ASSERT_NE(nullptr, str); + EXPECT_EQ(std::u16string(u"?123"), *str->value); +} + +TEST_F(ResourceParserTest, ParseNull) { + std::string input = "<integer name=\"foo\">@null</integer>"; + ASSERT_TRUE(testParse(input)); + + // The Android runtime treats a value of android::Res_value::TYPE_NULL as + // a non-existing value, and this causes problems in styles when trying to resolve + // an attribute. Null values must be encoded as android::Res_value::TYPE_REFERENCE + // with a data value of 0. + const BinaryPrimitive* integer = findResource<BinaryPrimitive>(ResourceName{ + u"android", ResourceType::kInteger, u"foo" }); + ASSERT_NE(nullptr, integer); + EXPECT_EQ(uint16_t(android::Res_value::TYPE_REFERENCE), integer->value.dataType); + EXPECT_EQ(0u, integer->value.data); +} + +TEST_F(ResourceParserTest, ParseEmpty) { + std::string input = "<integer name=\"foo\">@empty</integer>"; + ASSERT_TRUE(testParse(input)); + + const BinaryPrimitive* integer = findResource<BinaryPrimitive>(ResourceName{ + u"android", ResourceType::kInteger, u"foo" }); + ASSERT_NE(nullptr, integer); + EXPECT_EQ(uint16_t(android::Res_value::TYPE_NULL), integer->value.dataType); + EXPECT_EQ(uint32_t(android::Res_value::DATA_NULL_EMPTY), integer->value.data); +} + +TEST_F(ResourceParserTest, ParseAttr) { + std::string input = "<attr name=\"foo\" format=\"string\"/>\n" + "<attr name=\"bar\"/>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + EXPECT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); + + attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"bar"}); + EXPECT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_ANY), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseUseAndDeclOfAttr) { + std::string input = "<declare-styleable name=\"Styleable\">\n" + " <attr name=\"foo\" />\n" + "</declare-styleable>\n" + "<attr name=\"foo\" format=\"string\"/>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseDoubleUseOfAttr) { + std::string input = "<declare-styleable name=\"Theme\">" + " <attr name=\"foo\" />\n" + "</declare-styleable>\n" + "<declare-styleable name=\"Window\">\n" + " <attr name=\"foo\" format=\"boolean\"/>\n" + "</declare-styleable>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_BOOLEAN), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseEnumAttr) { + std::string input = "<attr name=\"foo\">\n" + " <enum name=\"bar\" value=\"0\"/>\n" + " <enum name=\"bat\" value=\"1\"/>\n" + " <enum name=\"baz\" value=\"2\"/>\n" + "</attr>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* enumAttr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(enumAttr, nullptr); + EXPECT_EQ(enumAttr->typeMask, android::ResTable_map::TYPE_ENUM); + ASSERT_EQ(enumAttr->symbols.size(), 3u); + + EXPECT_EQ(enumAttr->symbols[0].symbol.name.entry, u"bar"); + EXPECT_EQ(enumAttr->symbols[0].value, 0u); + + EXPECT_EQ(enumAttr->symbols[1].symbol.name.entry, u"bat"); + EXPECT_EQ(enumAttr->symbols[1].value, 1u); + + EXPECT_EQ(enumAttr->symbols[2].symbol.name.entry, u"baz"); + EXPECT_EQ(enumAttr->symbols[2].value, 2u); +} + +TEST_F(ResourceParserTest, ParseFlagAttr) { + std::string input = "<attr name=\"foo\">\n" + " <flag name=\"bar\" value=\"0\"/>\n" + " <flag name=\"bat\" value=\"1\"/>\n" + " <flag name=\"baz\" value=\"2\"/>\n" + "</attr>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* flagAttr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(flagAttr, nullptr); + EXPECT_EQ(flagAttr->typeMask, android::ResTable_map::TYPE_FLAGS); + ASSERT_EQ(flagAttr->symbols.size(), 3u); + + EXPECT_EQ(flagAttr->symbols[0].symbol.name.entry, u"bar"); + EXPECT_EQ(flagAttr->symbols[0].value, 0u); + + EXPECT_EQ(flagAttr->symbols[1].symbol.name.entry, u"bat"); + EXPECT_EQ(flagAttr->symbols[1].value, 1u); + + EXPECT_EQ(flagAttr->symbols[2].symbol.name.entry, u"baz"); + EXPECT_EQ(flagAttr->symbols[2].value, 2u); + + std::unique_ptr<BinaryPrimitive> flagValue = + ResourceParser::tryParseFlagSymbol(*flagAttr, u"baz|bat"); + ASSERT_NE(flagValue, nullptr); + EXPECT_EQ(flagValue->value.data, 1u | 2u); +} + +TEST_F(ResourceParserTest, FailToParseEnumAttrWithNonUniqueKeys) { + std::string input = "<attr name=\"foo\">\n" + " <enum name=\"bar\" value=\"0\"/>\n" + " <enum name=\"bat\" value=\"1\"/>\n" + " <enum name=\"bat\" value=\"2\"/>\n" + "</attr>"; + ASSERT_FALSE(testParse(input)); +} + +TEST_F(ResourceParserTest, ParseStyle) { + std::string input = "<style name=\"foo\" parent=\"@style/fu\">\n" + " <item name=\"bar\">#ffffffff</item>\n" + " <item name=\"bat\">@string/hey</item>\n" + " <item name=\"baz\"><b>hey</b></item>\n" + "</style>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo"}); + ASSERT_NE(style, nullptr); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kStyle, u"fu"), style->parent.name); + ASSERT_EQ(style->entries.size(), 3u); + + EXPECT_EQ(style->entries[0].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"bar" })); + EXPECT_EQ(style->entries[1].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"bat" })); + EXPECT_EQ(style->entries[2].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"baz" })); +} + +TEST_F(ResourceParserTest, ParseStyleWithShorthandParent) { + std::string input = "<style name=\"foo\" parent=\"com.app:Theme\"/>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>( + ResourceName{ u"android", ResourceType::kStyle, u"foo" }); + ASSERT_NE(style, nullptr); + EXPECT_EQ(ResourceNameRef(u"com.app", ResourceType::kStyle, u"Theme"), style->parent.name); +} + +TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedParent) { + std::string input = "<style xmlns:app=\"http://schemas.android.com/apk/res/android\"\n" + " name=\"foo\" parent=\"app:Theme\"/>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo" }); + ASSERT_NE(style, nullptr); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kStyle, u"Theme"), style->parent.name); +} + +TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedItems) { + std::string input = + "<style xmlns:app=\"http://schemas.android.com/apk/res/android\" name=\"foo\">\n" + " <item name=\"app:bar\">0</item>\n" + "</style>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo" }); + ASSERT_NE(style, nullptr); + ASSERT_EQ(1u, style->entries.size()); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kAttr, u"bar"), + style->entries[0].key.name); +} + +TEST_F(ResourceParserTest, ParseStyleWithInferredParent) { + std::string input = "<style name=\"foo.bar\"/>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo.bar" }); + ASSERT_NE(style, nullptr); + EXPECT_EQ(style->parent.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + EXPECT_TRUE(style->parentInferred); +} + +TEST_F(ResourceParserTest, ParseStyleWithInferredParentOverridenByEmptyParentAttribute) { + std::string input = "<style name=\"foo.bar\" parent=\"\"/>"; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo.bar" }); + ASSERT_NE(style, nullptr); + EXPECT_FALSE(style->parent.name.isValid()); + EXPECT_FALSE(style->parentInferred); +} + +TEST_F(ResourceParserTest, ParseAutoGeneratedIdReference) { + std::string input = "<string name=\"foo\">@+id/bar</string>"; + ASSERT_TRUE(testParse(input)); + + const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"bar"}); + ASSERT_NE(id, nullptr); +} + +TEST_F(ResourceParserTest, ParseAttributesDeclareStyleable) { + std::string input = "<declare-styleable name=\"foo\">\n" + " <attr name=\"bar\" />\n" + " <attr name=\"bat\" format=\"string|reference\"/>\n" + "</declare-styleable>"; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"bar"}); + ASSERT_NE(attr, nullptr); + EXPECT_TRUE(attr->isWeak()); + + attr = findResource<Attribute>(ResourceName{ u"android", ResourceType::kAttr, u"bat"}); + ASSERT_NE(attr, nullptr); + EXPECT_TRUE(attr->isWeak()); + + const Styleable* styleable = findResource<Styleable>(ResourceName{ + u"android", ResourceType::kStyleable, u"foo" }); + ASSERT_NE(styleable, nullptr); + ASSERT_EQ(2u, styleable->entries.size()); + + EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bar"}), styleable->entries[0].name); + EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bat"}), styleable->entries[1].name); +} + +TEST_F(ResourceParserTest, ParseArray) { + std::string input = "<array name=\"foo\">\n" + " <item>@string/ref</item>\n" + " <item>hey</item>\n" + " <item>23</item>\n" + "</array>"; + ASSERT_TRUE(testParse(input)); + + const Array* array = findResource<Array>(ResourceName{ + u"android", ResourceType::kArray, u"foo" }); + ASSERT_NE(array, nullptr); + ASSERT_EQ(3u, array->items.size()); + + EXPECT_NE(nullptr, dynamic_cast<const Reference*>(array->items[0].get())); + EXPECT_NE(nullptr, dynamic_cast<const String*>(array->items[1].get())); + EXPECT_NE(nullptr, dynamic_cast<const BinaryPrimitive*>(array->items[2].get())); +} + +TEST_F(ResourceParserTest, ParsePlural) { + std::string input = "<plurals name=\"foo\">\n" + " <item quantity=\"other\">apples</item>\n" + " <item quantity=\"one\">apple</item>\n" + "</plurals>"; + ASSERT_TRUE(testParse(input)); +} + +TEST_F(ResourceParserTest, ParseCommentsWithResource) { + std::string input = "<!-- This is a comment -->\n" + "<string name=\"foo\">Hi</string>"; + ASSERT_TRUE(testParse(input)); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(ResourceName{ + u"android", ResourceType::kString, u"foo"}); + ASSERT_NE(type, nullptr); + ASSERT_NE(entry, nullptr); + ASSERT_FALSE(entry->values.empty()); + EXPECT_EQ(entry->values.front().comment, u"This is a comment"); +} + +/* + * Declaring an ID as public should not require a separate definition + * (as an ID has no value). + */ +TEST_F(ResourceParserTest, ParsePublicIdAsDefinition) { + std::string input = "<public type=\"id\" name=\"foo\"/>"; + ASSERT_TRUE(testParse(input)); + + const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"foo" }); + ASSERT_NE(nullptr, id); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp new file mode 100644 index 0000000..c93ecc7 --- /dev/null +++ b/tools/aapt2/ResourceTable.cpp @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ConfigDescription.h" +#include "Logger.h" +#include "NameMangler.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <algorithm> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <string> +#include <tuple> + +namespace aapt { + +static bool compareConfigs(const ResourceConfigValue& lhs, const ConfigDescription& rhs) { + return lhs.config < rhs; +} + +static bool lessThanType(const std::unique_ptr<ResourceTableType>& lhs, ResourceType rhs) { + return lhs->type < rhs; +} + +static bool lessThanEntry(const std::unique_ptr<ResourceEntry>& lhs, const StringPiece16& rhs) { + return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0; +} + +ResourceTable::ResourceTable() : mPackageId(kUnsetPackageId) { + // Make sure attrs always have type ID 1. + findOrCreateType(ResourceType::kAttr)->typeId = 1; +} + +std::unique_ptr<ResourceTableType>& ResourceTable::findOrCreateType(ResourceType type) { + auto last = mTypes.end(); + auto iter = std::lower_bound(mTypes.begin(), last, type, lessThanType); + if (iter != last) { + if ((*iter)->type == type) { + return *iter; + } + } + return *mTypes.emplace(iter, new ResourceTableType{ type }); +} + +std::unique_ptr<ResourceEntry>& ResourceTable::findOrCreateEntry( + std::unique_ptr<ResourceTableType>& type, const StringPiece16& name) { + auto last = type->entries.end(); + auto iter = std::lower_bound(type->entries.begin(), last, name, lessThanEntry); + if (iter != last) { + if (name == (*iter)->name) { + return *iter; + } + } + return *type->entries.emplace(iter, new ResourceEntry{ name }); +} + +struct IsAttributeVisitor : ConstValueVisitor { + bool isAttribute = false; + + void visit(const Attribute&, ValueVisitorArgs&) override { + isAttribute = true; + } + + operator bool() { + return isAttribute; + } +}; + +/** + * The default handler for collisions. A return value of -1 means keep the + * existing value, 0 means fail, and +1 means take the incoming value. + */ +static int defaultCollisionHandler(const Value& existing, const Value& incoming) { + IsAttributeVisitor existingIsAttr, incomingIsAttr; + existing.accept(existingIsAttr, {}); + incoming.accept(incomingIsAttr, {}); + + if (!incomingIsAttr) { + if (incoming.isWeak()) { + // We're trying to add a weak resource but a resource + // already exists. Keep the existing. + return -1; + } else if (existing.isWeak()) { + // Override the weak resource with the new strong resource. + return 1; + } + // The existing and incoming values are strong, this is an error + // if the values are not both attributes. + return 0; + } + + if (!existingIsAttr) { + if (existing.isWeak()) { + // The existing value is not an attribute and it is weak, + // so take the incoming attribute value. + return 1; + } + // The existing value is not an attribute and it is strong, + // so the incoming attribute value is an error. + return 0; + } + + // + // Attribute specific handling. At this point we know both + // values are attributes. Since we can declare and define + // attributes all-over, we do special handling to see + // which definition sticks. + // + const Attribute& existingAttr = static_cast<const Attribute&>(existing); + const Attribute& incomingAttr = static_cast<const Attribute&>(incoming); + if (existingAttr.typeMask == incomingAttr.typeMask) { + // The two attributes are both DECLs, but they are plain attributes + // with the same formats. + // Keep the strongest one. + return existingAttr.isWeak() ? 1 : -1; + } + + if (existingAttr.isWeak() && existingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + // Any incoming attribute is better than this. + return 1; + } + + if (incomingAttr.isWeak() && incomingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + // The incoming attribute may be a USE instead of a DECL. + // Keep the existing attribute. + return -1; + } + return 0; +} + +static constexpr const char16_t* kValidNameChars = u"._-"; +static constexpr const char16_t* kValidNameMangledChars = u"._-$"; + +bool ResourceTable::addResource(const ResourceNameRef& name, const ConfigDescription& config, + const SourceLine& source, std::unique_ptr<Value> value) { + return addResourceImpl(name, ResourceId{}, config, source, std::move(value), kValidNameChars); +} + +bool ResourceTable::addResource(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value) { + return addResourceImpl(name, resId, config, source, std::move(value), kValidNameChars); +} + +bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name, + const ConfigDescription& config, + const SourceLine& source, + std::unique_ptr<Value> value) { + return addResourceImpl(name, ResourceId{}, config, source, std::move(value), + kValidNameMangledChars); +} + +bool ResourceTable::addResourceImpl(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value, const char16_t* validChars) { + if (!name.package.empty() && name.package != mPackage) { + Logger::error(source) + << "resource '" + << name + << "' has incompatible package. Must be '" + << mPackage + << "'." + << std::endl; + return false; + } + + auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, validChars); + if (badCharIter != name.entry.end()) { + Logger::error(source) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << StringPiece16(badCharIter, 1) + << "'." + << std::endl; + return false; + } + + std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); + if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && + type->typeId != resId.typeId()) { + Logger::error(source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << type->typeId << std::dec + << "." + << std::endl; + return false; + } + + std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); + if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && + entry->entryId != resId.entryId()) { + Logger::error(source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but resource already has ID " + << ResourceId(mPackageId, type->typeId, entry->entryId) + << "." + << std::endl; + return false; + } + + const auto endIter = std::end(entry->values); + auto iter = std::lower_bound(std::begin(entry->values), endIter, config, compareConfigs); + if (iter == endIter || iter->config != config) { + // This resource did not exist before, add it. + entry->values.insert(iter, ResourceConfigValue{ config, source, {}, std::move(value) }); + } else { + int collisionResult = defaultCollisionHandler(*iter->value, *value); + if (collisionResult > 0) { + // Take the incoming value. + *iter = ResourceConfigValue{ config, source, {}, std::move(value) }; + } else if (collisionResult == 0) { + Logger::error(source) + << "duplicate value for resource '" << name << "' " + << "with config '" << iter->config << "'." + << std::endl; + + Logger::error(iter->source) + << "resource previously defined here." + << std::endl; + return false; + } + } + + if (resId.isValid()) { + type->typeId = resId.typeId(); + entry->entryId = resId.entryId(); + } + return true; +} + +bool ResourceTable::markPublic(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source) { + return markPublicImpl(name, resId, source, kValidNameChars); +} + +bool ResourceTable::markPublicAllowMangled(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source) { + return markPublicImpl(name, resId, source, kValidNameMangledChars); +} + +bool ResourceTable::markPublicImpl(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source, const char16_t* validChars) { + if (!name.package.empty() && name.package != mPackage) { + Logger::error(source) + << "resource '" + << name + << "' has incompatible package. Must be '" + << mPackage + << "'." + << std::endl; + return false; + } + + auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, validChars); + if (badCharIter != name.entry.end()) { + Logger::error(source) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << StringPiece16(badCharIter, 1) + << "'." + << std::endl; + return false; + } + + std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); + if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && + type->typeId != resId.typeId()) { + Logger::error(source) + << "trying to make resource '" + << name + << "' public with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << type->typeId << std::dec + << "." + << std::endl; + return false; + } + + std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); + if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && + entry->entryId != resId.entryId()) { + Logger::error(source) + << "trying to make resource '" + << name + << "' public with ID " + << resId + << " but resource already has ID " + << ResourceId(mPackageId, type->typeId, entry->entryId) + << "." + << std::endl; + return false; + } + + type->publicStatus.isPublic = true; + entry->publicStatus.isPublic = true; + entry->publicStatus.source = source; + + if (resId.isValid()) { + type->typeId = resId.typeId(); + entry->entryId = resId.entryId(); + } + return true; +} + +bool ResourceTable::merge(ResourceTable&& other) { + const bool mangleNames = mPackage != other.getPackage(); + std::u16string mangledName; + + for (auto& otherType : other) { + std::unique_ptr<ResourceTableType>& type = findOrCreateType(otherType->type); + if (otherType->publicStatus.isPublic) { + if (type->publicStatus.isPublic && type->typeId != otherType->typeId) { + Logger::error() << "can not merge type '" << type->type + << "': conflicting public IDs " + << "(" << type->typeId << " vs " << otherType->typeId << ")." + << std::endl; + return false; + } + type->publicStatus = std::move(otherType->publicStatus); + type->typeId = otherType->typeId; + } + + for (auto& otherEntry : otherType->entries) { + const std::u16string* nameToAdd = &otherEntry->name; + if (mangleNames) { + mangledName = otherEntry->name; + NameMangler::mangle(other.getPackage(), &mangledName); + nameToAdd = &mangledName; + } + + std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, *nameToAdd); + if (otherEntry->publicStatus.isPublic) { + if (entry->publicStatus.isPublic && entry->entryId != otherEntry->entryId) { + Logger::error() << "can not merge entry '" << type->type << "/" << entry->name + << "': conflicting public IDs " + << "(" << entry->entryId << " vs " << entry->entryId << ")." + << std::endl; + return false; + } + entry->publicStatus = std::move(otherEntry->publicStatus); + entry->entryId = otherEntry->entryId; + } + + for (ResourceConfigValue& otherValue : otherEntry->values) { + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), + otherValue.config, compareConfigs); + if (iter != entry->values.end() && iter->config == otherValue.config) { + int collisionResult = defaultCollisionHandler(*iter->value, *otherValue.value); + if (collisionResult > 0) { + // Take the incoming value. + iter->source = std::move(otherValue.source); + iter->comment = std::move(otherValue.comment); + iter->value = std::unique_ptr<Value>(otherValue.value->clone(&mValuePool)); + } else if (collisionResult == 0) { + ResourceNameRef resourceName = { mPackage, type->type, entry->name }; + Logger::error(otherValue.source) + << "resource '" << resourceName << "' has a conflicting value for " + << "configuration (" << otherValue.config << ")." + << std::endl; + Logger::note(iter->source) << "originally defined here." << std::endl; + return false; + } + } else { + entry->values.insert(iter, ResourceConfigValue{ + otherValue.config, + std::move(otherValue.source), + std::move(otherValue.comment), + std::unique_ptr<Value>(otherValue.value->clone(&mValuePool)), + }); + } + } + } + } + return true; +} + +std::tuple<const ResourceTableType*, const ResourceEntry*> +ResourceTable::findResource(const ResourceNameRef& name) const { + if (name.package != mPackage) { + return {}; + } + + auto iter = std::lower_bound(mTypes.begin(), mTypes.end(), name.type, lessThanType); + if (iter == mTypes.end() || (*iter)->type != name.type) { + return {}; + } + + const std::unique_ptr<ResourceTableType>& type = *iter; + auto iter2 = std::lower_bound(type->entries.begin(), type->entries.end(), name.entry, + lessThanEntry); + if (iter2 == type->entries.end() || name.entry != (*iter2)->name) { + return {}; + } + return std::make_tuple(iter->get(), iter2->get()); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h new file mode 100644 index 0000000..706f56a --- /dev/null +++ b/tools/aapt2/ResourceTable.h @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_TABLE_H +#define AAPT_RESOURCE_TABLE_H + +#include "ConfigDescription.h" +#include "Resource.h" +#include "ResourceValues.h" +#include "Source.h" +#include "StringPool.h" + +#include <memory> +#include <string> +#include <tuple> +#include <vector> + +namespace aapt { + +/** + * The Public status of a resource. + */ +struct Public { + bool isPublic = false; + SourceLine source; + std::u16string comment; +}; + +/** + * The resource value for a specific configuration. + */ +struct ResourceConfigValue { + ConfigDescription config; + SourceLine source; + std::u16string comment; + std::unique_ptr<Value> value; +}; + +/** + * Represents a resource entry, which may have + * varying values for each defined configuration. + */ +struct ResourceEntry { + enum { + kUnsetEntryId = 0xffffffffu + }; + + /** + * The name of the resource. Immutable, as + * this determines the order of this resource + * when doing lookups. + */ + const std::u16string name; + + /** + * The entry ID for this resource. + */ + size_t entryId; + + /** + * Whether this resource is public (and must maintain the same + * entry ID across builds). + */ + Public publicStatus; + + /** + * The resource's values for each configuration. + */ + std::vector<ResourceConfigValue> values; + + inline ResourceEntry(const StringPiece16& _name); + inline ResourceEntry(const ResourceEntry* rhs); +}; + +/** + * Represents a resource type, which holds entries defined + * for this type. + */ +struct ResourceTableType { + enum { + kUnsetTypeId = 0xffffffffu + }; + + /** + * The logical type of resource (string, drawable, layout, etc.). + */ + const ResourceType type; + + /** + * The type ID for this resource. + */ + size_t typeId; + + /** + * Whether this type is public (and must maintain the same + * type ID across builds). + */ + Public publicStatus; + + /** + * List of resources for this type. + */ + std::vector<std::unique_ptr<ResourceEntry>> entries; + + ResourceTableType(const ResourceType _type); + ResourceTableType(const ResourceTableType* rhs); +}; + +/** + * The container and index for all resources defined for an app. This gets + * flattened into a binary resource table (resources.arsc). + */ +class ResourceTable { +public: + using iterator = std::vector<std::unique_ptr<ResourceTableType>>::iterator; + using const_iterator = std::vector<std::unique_ptr<ResourceTableType>>::const_iterator; + + enum { + kUnsetPackageId = 0xffffffff + }; + + ResourceTable(); + + size_t getPackageId() const; + void setPackageId(size_t packageId); + + const std::u16string& getPackage() const; + void setPackage(const StringPiece16& package); + + bool addResource(const ResourceNameRef& name, const ConfigDescription& config, + const SourceLine& source, std::unique_ptr<Value> value); + + /** + * Same as addResource, but doesn't verify the validity of the name. This is used + * when loading resources from an existing binary resource table that may have mangled + * names. + */ + bool addResourceAllowMangled(const ResourceNameRef& name, const ConfigDescription& config, + const SourceLine& source, std::unique_ptr<Value> value); + + bool addResource(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value); + + bool markPublic(const ResourceNameRef& name, const ResourceId resId, const SourceLine& source); + bool markPublicAllowMangled(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source); + + /* + * Merges the resources from `other` into this table, mangling the names of the resources + * if `other` has a different package name. + */ + bool merge(ResourceTable&& other); + + /** + * Returns the string pool used by this ResourceTable. + * Values that reference strings should use this pool to create + * their strings. + */ + StringPool& getValueStringPool(); + const StringPool& getValueStringPool() const; + + std::tuple<const ResourceTableType*, const ResourceEntry*> + findResource(const ResourceNameRef& name) const; + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + +private: + std::unique_ptr<ResourceTableType>& findOrCreateType(ResourceType type); + std::unique_ptr<ResourceEntry>& findOrCreateEntry(std::unique_ptr<ResourceTableType>& type, + const StringPiece16& name); + + bool addResourceImpl(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value, const char16_t* validChars); + bool markPublicImpl(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source, const char16_t* validChars); + + std::u16string mPackage; + size_t mPackageId; + + // StringPool must come before mTypes so that it is destroyed after. + // When StringPool references are destroyed (as they will be when mTypes + // is destroyed), they decrement a refCount, which would cause invalid + // memory access if the pool was already destroyed. + StringPool mValuePool; + + std::vector<std::unique_ptr<ResourceTableType>> mTypes; +}; + +// +// ResourceEntry implementation. +// + +inline ResourceEntry::ResourceEntry(const StringPiece16& _name) : + name(_name.toString()), entryId(kUnsetEntryId) { +} + +inline ResourceEntry::ResourceEntry(const ResourceEntry* rhs) : + name(rhs->name), entryId(rhs->entryId), publicStatus(rhs->publicStatus) { +} + +// +// ResourceTableType implementation. +// + +inline ResourceTableType::ResourceTableType(const ResourceType _type) : + type(_type), typeId(kUnsetTypeId) { +} + +inline ResourceTableType::ResourceTableType(const ResourceTableType* rhs) : + type(rhs->type), typeId(rhs->typeId), publicStatus(rhs->publicStatus) { +} + +// +// ResourceTable implementation. +// + +inline StringPool& ResourceTable::getValueStringPool() { + return mValuePool; +} + +inline const StringPool& ResourceTable::getValueStringPool() const { + return mValuePool; +} + +inline ResourceTable::iterator ResourceTable::begin() { + return mTypes.begin(); +} + +inline ResourceTable::iterator ResourceTable::end() { + return mTypes.end(); +} + +inline ResourceTable::const_iterator ResourceTable::begin() const { + return mTypes.begin(); +} + +inline ResourceTable::const_iterator ResourceTable::end() const { + return mTypes.end(); +} + +inline const std::u16string& ResourceTable::getPackage() const { + return mPackage; +} + +inline size_t ResourceTable::getPackageId() const { + return mPackageId; +} + +inline void ResourceTable::setPackage(const StringPiece16& package) { + mPackage = package.toString(); +} + +inline void ResourceTable::setPackageId(size_t packageId) { + mPackageId = packageId; +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_TABLE_H diff --git a/tools/aapt2/ResourceTableResolver.cpp b/tools/aapt2/ResourceTableResolver.cpp new file mode 100644 index 0000000..910c2c0 --- /dev/null +++ b/tools/aapt2/ResourceTableResolver.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Maybe.h" +#include "NameMangler.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceTableResolver.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <vector> + +namespace aapt { + +ResourceTableResolver::ResourceTableResolver( + std::shared_ptr<const ResourceTable> table, + const std::vector<std::shared_ptr<const android::AssetManager>>& sources) : + mTable(table), mSources(sources) { + for (const auto& assetManager : mSources) { + const android::ResTable& resTable = assetManager->getResources(false); + const size_t packageCount = resTable.getBasePackageCount(); + for (size_t i = 0; i < packageCount; i++) { + std::u16string packageName = resTable.getBasePackageName(i).string(); + mIncludedPackages.insert(std::move(packageName)); + } + } +} + +Maybe<ResourceId> ResourceTableResolver::findId(const ResourceName& name) { + Maybe<Entry> result = findAttribute(name); + if (result) { + return result.value().id; + } + return {}; +} + +Maybe<IResolver::Entry> ResourceTableResolver::findAttribute(const ResourceName& name) { + auto cacheIter = mCache.find(name); + if (cacheIter != std::end(mCache)) { + return Entry{ cacheIter->second.id, cacheIter->second.attr.get() }; + } + + ResourceName mangledName; + const ResourceName* nameToSearch = &name; + if (name.package != mTable->getPackage()) { + // This may be a reference to an included resource or + // to a mangled resource. + if (mIncludedPackages.find(name.package) == mIncludedPackages.end()) { + // This is not in our included set, so mangle the name and + // check for that. + mangledName.entry = name.entry; + NameMangler::mangle(name.package, &mangledName.entry); + mangledName.package = mTable->getPackage(); + mangledName.type = name.type; + nameToSearch = &mangledName; + } else { + const CacheEntry* cacheEntry = buildCacheEntry(name); + if (cacheEntry) { + return Entry{ cacheEntry->id, cacheEntry->attr.get() }; + } + return {}; + } + } + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(*nameToSearch); + if (type && entry) { + Entry result = {}; + if (mTable->getPackageId() != ResourceTable::kUnsetPackageId && + type->typeId != ResourceTableType::kUnsetTypeId && + entry->entryId != ResourceEntry::kUnsetEntryId) { + result.id = ResourceId(mTable->getPackageId(), type->typeId, entry->entryId); + } + + if (!entry->values.empty()) { + visitFunc<Attribute>(*entry->values.front().value, [&result](Attribute& attr) { + result.attr = &attr; + }); + } + return result; + } + return {}; +} + +Maybe<ResourceName> ResourceTableResolver::findName(ResourceId resId) { + for (const auto& assetManager : mSources) { + const android::ResTable& table = assetManager->getResources(false); + + android::ResTable::resource_name resourceName; + if (!table.getResourceName(resId.id, false, &resourceName)) { + continue; + } + + const ResourceType* type = parseResourceType(StringPiece16(resourceName.type, + resourceName.typeLen)); + assert(type); + return ResourceName{ + { resourceName.package, resourceName.packageLen }, + *type, + { resourceName.name, resourceName.nameLen } }; + } + return {}; +} + +/** + * This is called when we need to lookup a resource name in the AssetManager. + * Since the values in the AssetManager are not parsed like in a ResourceTable, + * we must create Attribute objects here if we find them. + */ +const ResourceTableResolver::CacheEntry* ResourceTableResolver::buildCacheEntry( + const ResourceName& name) { + for (const auto& assetManager : mSources) { + const android::ResTable& table = assetManager->getResources(false); + + const StringPiece16 type16 = toString(name.type); + ResourceId resId { + table.identifierForName( + name.entry.data(), name.entry.size(), + type16.data(), type16.size(), + name.package.data(), name.package.size()) + }; + + if (!resId.isValid()) { + continue; + } + + CacheEntry& entry = mCache[name]; + entry.id = resId; + + // + // Now check to see if this resource is an Attribute. + // + + const android::ResTable::bag_entry* bagBegin; + ssize_t bags = table.lockBag(resId.id, &bagBegin); + if (bags < 1) { + table.unlockBag(bagBegin); + return &entry; + } + + // Look for the ATTR_TYPE key in the bag and check the types it supports. + uint32_t attrTypeMask = 0; + for (ssize_t i = 0; i < bags; i++) { + if (bagBegin[i].map.name.ident == android::ResTable_map::ATTR_TYPE) { + attrTypeMask = bagBegin[i].map.value.data; + } + } + + entry.attr = util::make_unique<Attribute>(false); + + if (attrTypeMask & android::ResTable_map::TYPE_ENUM || + attrTypeMask & android::ResTable_map::TYPE_FLAGS) { + for (ssize_t i = 0; i < bags; i++) { + if (Res_INTERNALID(bagBegin[i].map.name.ident)) { + // Internal IDs are special keys, which are not enum/flag symbols, so skip. + continue; + } + + android::ResTable::resource_name symbolName; + bool result = table.getResourceName(bagBegin[i].map.name.ident, false, + &symbolName); + assert(result); + const ResourceType* type = parseResourceType( + StringPiece16(symbolName.type, symbolName.typeLen)); + assert(type); + + entry.attr->symbols.push_back(Attribute::Symbol{ + Reference(ResourceNameRef( + StringPiece16(symbolName.package, symbolName.packageLen), + *type, + StringPiece16(symbolName.name, symbolName.nameLen))), + bagBegin[i].map.value.data + }); + } + } + + entry.attr->typeMask |= attrTypeMask; + table.unlockBag(bagBegin); + return &entry; + } + return nullptr; +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTableResolver.h b/tools/aapt2/ResourceTableResolver.h new file mode 100644 index 0000000..8f6b0b5 --- /dev/null +++ b/tools/aapt2/ResourceTableResolver.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_TABLE_RESOLVER_H +#define AAPT_RESOURCE_TABLE_RESOLVER_H + +#include "Maybe.h" +#include "Resolver.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" + +#include <androidfw/AssetManager.h> +#include <memory> +#include <vector> +#include <unordered_set> + +namespace aapt { + +/** + * Encapsulates the search of library sources as well as the local ResourceTable. + */ +class ResourceTableResolver : public IResolver { +public: + /** + * Creates a resolver with a local ResourceTable and an AssetManager + * loaded with library packages. + */ + ResourceTableResolver( + std::shared_ptr<const ResourceTable> table, + const std::vector<std::shared_ptr<const android::AssetManager>>& sources); + + ResourceTableResolver(const ResourceTableResolver&) = delete; // Not copyable. + + virtual Maybe<ResourceId> findId(const ResourceName& name) override; + + virtual Maybe<Entry> findAttribute(const ResourceName& name) override; + + virtual Maybe<ResourceName> findName(ResourceId resId) override; + +private: + struct CacheEntry { + ResourceId id; + std::unique_ptr<Attribute> attr; + }; + + const CacheEntry* buildCacheEntry(const ResourceName& name); + + std::shared_ptr<const ResourceTable> mTable; + std::vector<std::shared_ptr<const android::AssetManager>> mSources; + std::map<ResourceName, CacheEntry> mCache; + std::unordered_set<std::u16string> mIncludedPackages; +}; + +} // namespace aapt + +#endif // AAPT_RESOURCE_TABLE_RESOLVER_H diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp new file mode 100644 index 0000000..06d8699 --- /dev/null +++ b/tools/aapt2/ResourceTable_test.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <algorithm> +#include <gtest/gtest.h> +#include <ostream> +#include <string> + +namespace aapt { + +struct TestValue : public Value { + std::u16string value; + + TestValue(StringPiece16 str) : value(str.toString()) { + } + + TestValue* clone(StringPool* /*newPool*/) const override { + return new TestValue(value); + } + + void print(std::ostream& out) const override { + out << "(test) " << value; + } + + virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} + virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} +}; + +struct TestWeakValue : public Value { + bool isWeak() const override { + return true; + } + + TestWeakValue* clone(StringPool* /*newPool*/) const override { + return new TestWeakValue(); + } + + void print(std::ostream& out) const override { + out << "(test) [weak]"; + } + + virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} + virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} +}; + +TEST(ResourceTableTest, FailToAddResourceWithBadName) { + ResourceTable table; + table.setPackage(u"android"); + + EXPECT_FALSE(table.addResource( + ResourceNameRef{ u"android", ResourceType::kId, u"hey,there" }, + {}, SourceLine{ "test.xml", 21 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_FALSE(table.addResource( + ResourceNameRef{ u"android", ResourceType::kId, u"hey:there" }, + {}, SourceLine{ "test.xml", 21 }, + util::make_unique<TestValue>(u"rawValue"))); +} + +TEST(ResourceTableTest, AddOneResource) { + const std::u16string kAndroidPackage = u"android"; + + ResourceTable table; + table.setPackage(kAndroidPackage); + + const ResourceName name = { kAndroidPackage, ResourceType::kAttr, u"id" }; + + EXPECT_TRUE(table.addResource(name, {}, SourceLine{ "test/path/file.xml", 23 }, + util::make_unique<TestValue>(u"rawValue"))); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table.findResource(name); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + EXPECT_EQ(name.entry, entry->name); + + ASSERT_NE(std::end(entry->values), + std::find_if(std::begin(entry->values), std::end(entry->values), + [](const ResourceConfigValue& val) -> bool { + return val.config == ConfigDescription{}; + })); +} + +TEST(ResourceTableTest, AddMultipleResources) { + const std::u16string kAndroidPackage = u"android"; + ResourceTable table; + table.setPackage(kAndroidPackage); + + ConfigDescription config; + ConfigDescription languageConfig; + memcpy(languageConfig.language, "pl", sizeof(languageConfig.language)); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kAttr, u"layout_width" }, + config, SourceLine{ "test/path/file.xml", 10 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kAttr, u"id" }, + config, SourceLine{ "test/path/file.xml", 12 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, + config, SourceLine{ "test/path/file.xml", 14 }, + util::make_unique<TestValue>(u"Ok"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, + languageConfig, SourceLine{ "test/path/file.xml", 20 }, + util::make_unique<TestValue>(u"Tak"))); + + const auto endTypeIter = std::end(table); + auto typeIter = std::begin(table); + + ASSERT_NE(endTypeIter, typeIter); + EXPECT_EQ(ResourceType::kAttr, (*typeIter)->type); + + { + const std::unique_ptr<ResourceTableType>& type = *typeIter; + const auto endEntryIter = std::end(type->entries); + auto entryIter = std::begin(type->entries); + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"id"), (*entryIter)->name); + + ++entryIter; + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"layout_width"), (*entryIter)->name); + + ++entryIter; + ASSERT_EQ(endEntryIter, entryIter); + } + + ++typeIter; + ASSERT_NE(endTypeIter, typeIter); + EXPECT_EQ(ResourceType::kString, (*typeIter)->type); + + { + const std::unique_ptr<ResourceTableType>& type = *typeIter; + const auto endEntryIter = std::end(type->entries); + auto entryIter = std::begin(type->entries); + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"ok"), (*entryIter)->name); + + { + const std::unique_ptr<ResourceEntry>& entry = *entryIter; + const auto endConfigIter = std::end(entry->values); + auto configIter = std::begin(entry->values); + + ASSERT_NE(endConfigIter, configIter); + EXPECT_EQ(config, configIter->config); + const TestValue* value = + dynamic_cast<const TestValue*>(configIter->value.get()); + ASSERT_NE(nullptr, value); + EXPECT_EQ(std::u16string(u"Ok"), value->value); + + ++configIter; + ASSERT_NE(endConfigIter, configIter); + EXPECT_EQ(languageConfig, configIter->config); + EXPECT_NE(nullptr, configIter->value); + + value = dynamic_cast<const TestValue*>(configIter->value.get()); + ASSERT_NE(nullptr, value); + EXPECT_EQ(std::u16string(u"Tak"), value->value); + + ++configIter; + EXPECT_EQ(endConfigIter, configIter); + } + + ++entryIter; + ASSERT_EQ(endEntryIter, entryIter); + } + + ++typeIter; + EXPECT_EQ(endTypeIter, typeIter); +} + +TEST(ResourceTableTest, OverrideWeakResourceValue) { + const std::u16string kAndroid = u"android"; + + ResourceTable table; + table.setPackage(kAndroid); + table.setPackageId(0x01); + + ASSERT_TRUE(table.addResource( + ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, + {}, {}, util::make_unique<TestWeakValue>())); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table.findResource( + ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + ASSERT_EQ(entry->values.size(), 1u); + EXPECT_TRUE(entry->values.front().value->isWeak()); + + ASSERT_TRUE(table.addResource(ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, {}, {}, + util::make_unique<TestValue>(u"bar"))); + + std::tie(type, entry) = table.findResource( + ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + ASSERT_EQ(entry->values.size(), 1u); + EXPECT_FALSE(entry->values.front().value->isWeak()); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTypeExtensions.h b/tools/aapt2/ResourceTypeExtensions.h new file mode 100644 index 0000000..dcbe923 --- /dev/null +++ b/tools/aapt2/ResourceTypeExtensions.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_TYPE_EXTENSIONS_H +#define AAPT_RESOURCE_TYPE_EXTENSIONS_H + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +/** + * New android::ResChunk_header types defined + * for AAPT to use. + * + * TODO(adamlesinski): Consider reserving these + * enums in androidfw/ResourceTypes.h to avoid + * future collisions. + */ +enum { + RES_TABLE_PUBLIC_TYPE = 0x000d, + + /** + * A chunk that holds the string pool + * for source entries (path/to/source:line). + */ + RES_TABLE_SOURCE_POOL_TYPE = 0x000e, + + /** + * A chunk holding names of externally + * defined symbols and offsets to where + * they are referenced in the table. + */ + RES_TABLE_SYMBOL_TABLE_TYPE = 0x000f, +}; + +/** + * New resource types that are meant to only be used + * by AAPT and will not end up on the device. + */ +struct ExtendedTypes { + enum { + /** + * A raw string value that hasn't had its escape sequences + * processed nor whitespace removed. + */ + TYPE_RAW_STRING = 0xfe + }; +}; + +struct Public_header { + android::ResChunk_header header; + + /** + * The ID of the type this structure refers to. + */ + uint8_t typeId; + + /** + * Reserved. Must be 0. + */ + uint8_t res0; + + /** + * Reserved. Must be 0. + */ + uint16_t res1; + + /** + * Number of public entries. + */ + uint32_t count; +}; + +struct Public_entry { + uint16_t entryId; + uint16_t res0; + android::ResStringPool_ref key; + android::ResStringPool_ref source; + uint32_t sourceLine; +}; + +/** + * A chunk with type RES_TABLE_SYMBOL_TABLE_TYPE. + * Following the header are count number of SymbolTable_entry + * structures, followed by an android::ResStringPool_header. + */ +struct SymbolTable_header { + android::ResChunk_header header; + + /** + * Number of SymbolTable_entry structures following + * this header. + */ + uint32_t count; +}; + +struct SymbolTable_entry { + /** + * Offset from the beginning of the resource table + * where the symbol entry is referenced. + */ + uint32_t offset; + + /** + * The index into the string pool where the name of this + * symbol exists. + */ + uint32_t stringIndex; +}; + +/** + * A structure representing the source of a resourc entry. + * Appears after an android::ResTable_entry or android::ResTable_map_entry. + * + * TODO(adamlesinski): This causes some issues when runtime code checks + * the size of an android::ResTable_entry. It assumes it is an + * android::ResTable_map_entry if the size is bigger than an android::ResTable_entry + * which may not be true if this structure is present. + */ +struct ResTable_entry_source { + /** + * Index into the source string pool. + */ + uint32_t pathIndex; + + /** + * Line number this resource was defined on. + */ + uint32_t line; +}; + +} // namespace aapt + +#endif // AAPT_RESOURCE_TYPE_EXTENSIONS_H diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp new file mode 100644 index 0000000..aabb375 --- /dev/null +++ b/tools/aapt2/ResourceValues.cpp @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Resource.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <limits> + +namespace aapt { + +bool Value::isItem() const { + return false; +} + +bool Value::isWeak() const { + return false; +} + +bool Item::isItem() const { + return true; +} + +RawString::RawString(const StringPool::Ref& ref) : value(ref) { +} + +RawString* RawString::clone(StringPool* newPool) const { + return new RawString(newPool->makeRef(*value)); +} + +bool RawString::flatten(android::Res_value& outValue) const { + outValue.dataType = ExtendedTypes::TYPE_RAW_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +void RawString::print(std::ostream& out) const { + out << "(raw string) " << *value; +} + +Reference::Reference() : referenceType(Reference::Type::kResource) { +} + +Reference::Reference(const ResourceNameRef& n, Type t) : + name(n.toResourceName()), referenceType(t) { +} + +Reference::Reference(const ResourceId& i, Type type) : id(i), referenceType(type) { +} + +bool Reference::flatten(android::Res_value& outValue) const { + outValue.dataType = (referenceType == Reference::Type::kResource) + ? android::Res_value::TYPE_REFERENCE + : android::Res_value::TYPE_ATTRIBUTE; + outValue.data = id.id; + return true; +} + +Reference* Reference::clone(StringPool* /*newPool*/) const { + Reference* ref = new Reference(); + ref->referenceType = referenceType; + ref->name = name; + ref->id = id; + return ref; +} + +void Reference::print(std::ostream& out) const { + out << "(reference) "; + if (referenceType == Reference::Type::kResource) { + out << "@"; + } else { + out << "?"; + } + + if (name.isValid()) { + out << name; + } + + if (id.isValid() || Res_INTERNALID(id.id)) { + out << " " << id; + } +} + +bool Id::isWeak() const { + return true; +} + +bool Id::flatten(android::Res_value& out) const { + out.dataType = android::Res_value::TYPE_INT_BOOLEAN; + out.data = 0; + return true; +} + +Id* Id::clone(StringPool* /*newPool*/) const { + return new Id(); +} + +void Id::print(std::ostream& out) const { + out << "(id)"; +} + +String::String(const StringPool::Ref& ref) : value(ref) { +} + +bool String::flatten(android::Res_value& outValue) const { + // Verify that our StringPool index is within encodeable limits. + if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +String* String::clone(StringPool* newPool) const { + return new String(newPool->makeRef(*value)); +} + +void String::print(std::ostream& out) const { + out << "(string) \"" << *value << "\""; +} + +StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) { +} + +bool StyledString::flatten(android::Res_value& outValue) const { + if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +StyledString* StyledString::clone(StringPool* newPool) const { + return new StyledString(newPool->makeRef(value)); +} + +void StyledString::print(std::ostream& out) const { + out << "(styled string) \"" << *value->str << "\""; +} + +FileReference::FileReference(const StringPool::Ref& _path) : path(_path) { +} + +bool FileReference::flatten(android::Res_value& outValue) const { + if (path.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(path.getIndex()); + return true; +} + +FileReference* FileReference::clone(StringPool* newPool) const { + return new FileReference(newPool->makeRef(*path)); +} + +void FileReference::print(std::ostream& out) const { + out << "(file) " << *path; +} + +BinaryPrimitive::BinaryPrimitive(const android::Res_value& val) : value(val) { +} + +bool BinaryPrimitive::flatten(android::Res_value& outValue) const { + outValue = value; + return true; +} + +BinaryPrimitive* BinaryPrimitive::clone(StringPool* /*newPool*/) const { + return new BinaryPrimitive(value); +} + +void BinaryPrimitive::print(std::ostream& out) const { + switch (value.dataType) { + case android::Res_value::TYPE_NULL: + out << "(null)"; + break; + case android::Res_value::TYPE_INT_DEC: + out << "(integer) " << value.data; + break; + case android::Res_value::TYPE_INT_HEX: + out << "(integer) " << std::hex << value.data << std::dec; + break; + case android::Res_value::TYPE_INT_BOOLEAN: + out << "(boolean) " << (value.data != 0 ? "true" : "false"); + break; + case android::Res_value::TYPE_INT_COLOR_ARGB8: + case android::Res_value::TYPE_INT_COLOR_RGB8: + case android::Res_value::TYPE_INT_COLOR_ARGB4: + case android::Res_value::TYPE_INT_COLOR_RGB4: + out << "(color) #" << std::hex << value.data << std::dec; + break; + default: + out << "(unknown 0x" << std::hex << (int) value.dataType << ") 0x" + << std::hex << value.data << std::dec; + break; + } +} + +Attribute::Attribute(bool w, uint32_t t) : weak(w), typeMask(t) { +} + +bool Attribute::isWeak() const { + return weak; +} + +Attribute* Attribute::clone(StringPool* /*newPool*/) const { + Attribute* attr = new Attribute(weak); + attr->typeMask = typeMask; + std::copy(symbols.begin(), symbols.end(), std::back_inserter(attr->symbols)); + return attr; +} + +void Attribute::printMask(std::ostream& out) const { + if (typeMask == android::ResTable_map::TYPE_ANY) { + out << "any"; + return; + } + + bool set = false; + if ((typeMask & android::ResTable_map::TYPE_REFERENCE) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "reference"; + } + + if ((typeMask & android::ResTable_map::TYPE_STRING) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "string"; + } + + if ((typeMask & android::ResTable_map::TYPE_INTEGER) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "integer"; + } + + if ((typeMask & android::ResTable_map::TYPE_BOOLEAN) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "boolean"; + } + + if ((typeMask & android::ResTable_map::TYPE_COLOR) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "color"; + } + + if ((typeMask & android::ResTable_map::TYPE_FLOAT) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "float"; + } + + if ((typeMask & android::ResTable_map::TYPE_DIMENSION) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "dimension"; + } + + if ((typeMask & android::ResTable_map::TYPE_FRACTION) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "fraction"; + } + + if ((typeMask & android::ResTable_map::TYPE_ENUM) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "enum"; + } + + if ((typeMask & android::ResTable_map::TYPE_FLAGS) != 0) { + if (!set) { + set = true; + } else { + out << "|"; + } + out << "flags"; + } +} + +void Attribute::print(std::ostream& out) const { + out << "(attr) "; + printMask(out); + + out << " [" + << util::joiner(symbols.begin(), symbols.end(), ", ") + << "]"; + + if (weak) { + out << " [weak]"; + } +} + +Style* Style::clone(StringPool* newPool) const { + Style* style = new Style(); + style->parent = parent; + style->parentInferred = parentInferred; + for (auto& entry : entries) { + style->entries.push_back(Entry{ + entry.key, + std::unique_ptr<Item>(entry.value->clone(newPool)) + }); + } + return style; +} + +void Style::print(std::ostream& out) const { + out << "(style) "; + if (!parent.name.entry.empty()) { + out << parent.name; + } + out << " [" + << util::joiner(entries.begin(), entries.end(), ", ") + << "]"; +} + +static ::std::ostream& operator<<(::std::ostream& out, const Style::Entry& value) { + out << value.key.name << " = "; + value.value->print(out); + return out; +} + +Array* Array::clone(StringPool* newPool) const { + Array* array = new Array(); + for (auto& item : items) { + array->items.emplace_back(std::unique_ptr<Item>(item->clone(newPool))); + } + return array; +} + +void Array::print(std::ostream& out) const { + out << "(array) [" + << util::joiner(items.begin(), items.end(), ", ") + << "]"; +} + +Plural* Plural::clone(StringPool* newPool) const { + Plural* p = new Plural(); + const size_t count = values.size(); + for (size_t i = 0; i < count; i++) { + if (values[i]) { + p->values[i] = std::unique_ptr<Item>(values[i]->clone(newPool)); + } + } + return p; +} + +void Plural::print(std::ostream& out) const { + out << "(plural)"; +} + +static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Item>& item) { + return out << *item; +} + +Styleable* Styleable::clone(StringPool* /*newPool*/) const { + Styleable* styleable = new Styleable(); + std::copy(entries.begin(), entries.end(), std::back_inserter(styleable->entries)); + return styleable; +} + +void Styleable::print(std::ostream& out) const { + out << "(styleable) " << " [" + << util::joiner(entries.begin(), entries.end(), ", ") + << "]"; +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h new file mode 100644 index 0000000..ef6594e --- /dev/null +++ b/tools/aapt2/ResourceValues.h @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_RESOURCE_VALUES_H +#define AAPT_RESOURCE_VALUES_H + +#include "Resource.h" +#include "StringPool.h" + +#include <array> +#include <androidfw/ResourceTypes.h> +#include <ostream> +#include <vector> + +namespace aapt { + +struct ValueVisitor; +struct ConstValueVisitor; +struct ValueVisitorArgs; + +/** + * A resource value. This is an all-encompassing representation + * of Item and Map and their subclasses. The way to do + * type specific operations is to check the Value's type() and + * cast it to the appropriate subclass. This isn't super clean, + * but it is the simplest strategy. + */ +struct Value { + /** + * Whether or not this is an Item. + */ + virtual bool isItem() const; + + /** + * Whether this value is weak and can be overriden without + * warning or error. Default for base class is false. + */ + virtual bool isWeak() const; + + /** + * Calls the appropriate overload of ValueVisitor. + */ + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) = 0; + + /** + * Const version of accept(). + */ + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const = 0; + + /** + * Clone the value. + */ + virtual Value* clone(StringPool* newPool) const = 0; + + /** + * Human readable printout of this value. + */ + virtual void print(std::ostream& out) const = 0; +}; + +/** + * Inherit from this to get visitor accepting implementations for free. + */ +template <typename Derived> +struct BaseValue : public Value { + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; +}; + +/** + * A resource item with a single value. This maps to android::ResTable_entry. + */ +struct Item : public Value { + /** + * An Item is, of course, an Item. + */ + virtual bool isItem() const override; + + /** + * Clone the Item. + */ + virtual Item* clone(StringPool* newPool) const override = 0; + + /** + * Fills in an android::Res_value structure with this Item's binary representation. + * Returns false if an error ocurred. + */ + virtual bool flatten(android::Res_value& outValue) const = 0; +}; + +/** + * Inherit from this to get visitor accepting implementations for free. + */ +template <typename Derived> +struct BaseItem : public Item { + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; +}; + +/** + * A reference to another resource. This maps to android::Res_value::TYPE_REFERENCE. + * + * A reference can be symbolic (with the name set to a valid resource name) or be + * numeric (the id is set to a valid resource ID). + */ +struct Reference : public BaseItem<Reference> { + enum class Type { + kResource, + kAttribute, + }; + + ResourceName name; + ResourceId id; + Reference::Type referenceType; + bool privateReference = false; + + Reference(); + Reference(const ResourceNameRef& n, Type type = Type::kResource); + Reference(const ResourceId& i, Type type = Type::kResource); + + bool flatten(android::Res_value& outValue) const override; + Reference* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +/** + * An ID resource. Has no real value, just a place holder. + */ +struct Id : public BaseItem<Id> { + bool isWeak() const override; + bool flatten(android::Res_value& out) const override; + Id* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +/** + * A raw, unprocessed string. This may contain quotations, + * escape sequences, and whitespace. This shall *NOT* + * end up in the final resource table. + */ +struct RawString : public BaseItem<RawString> { + StringPool::Ref value; + + RawString(const StringPool::Ref& ref); + + bool flatten(android::Res_value& outValue) const override; + RawString* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct String : public BaseItem<String> { + StringPool::Ref value; + + String(const StringPool::Ref& ref); + + bool flatten(android::Res_value& outValue) const override; + String* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct StyledString : public BaseItem<StyledString> { + StringPool::StyleRef value; + + StyledString(const StringPool::StyleRef& ref); + + bool flatten(android::Res_value& outValue) const override; + StyledString* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct FileReference : public BaseItem<FileReference> { + StringPool::Ref path; + + FileReference() = default; + FileReference(const StringPool::Ref& path); + + bool flatten(android::Res_value& outValue) const override; + FileReference* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +/** + * Represents any other android::Res_value. + */ +struct BinaryPrimitive : public BaseItem<BinaryPrimitive> { + android::Res_value value; + + BinaryPrimitive() = default; + BinaryPrimitive(const android::Res_value& val); + + bool flatten(android::Res_value& outValue) const override; + BinaryPrimitive* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct Attribute : public BaseValue<Attribute> { + struct Symbol { + Reference symbol; + uint32_t value; + }; + + bool weak; + uint32_t typeMask; + uint32_t minInt; + uint32_t maxInt; + std::vector<Symbol> symbols; + + Attribute(bool w, uint32_t t = 0u); + + bool isWeak() const override; + virtual Attribute* clone(StringPool* newPool) const override; + void printMask(std::ostream& out) const; + virtual void print(std::ostream& out) const override; +}; + +struct Style : public BaseValue<Style> { + struct Entry { + Reference key; + std::unique_ptr<Item> value; + }; + + Reference parent; + + /** + * If set to true, the parent was auto inferred from the + * style's name. + */ + bool parentInferred = false; + + std::vector<Entry> entries; + + Style* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct Array : public BaseValue<Array> { + std::vector<std::unique_ptr<Item>> items; + + Array* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct Plural : public BaseValue<Plural> { + enum { + Zero = 0, + One, + Two, + Few, + Many, + Other, + Count + }; + + std::array<std::unique_ptr<Item>, Count> values; + + Plural* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +struct Styleable : public BaseValue<Styleable> { + std::vector<Reference> entries; + + Styleable* clone(StringPool* newPool) const override; + void print(std::ostream& out) const override; +}; + +/** + * Stream operator for printing Value objects. + */ +inline ::std::ostream& operator<<(::std::ostream& out, const Value& value) { + value.print(out); + return out; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const Attribute::Symbol& s) { + return out << s.symbol.name.entry << "=" << s.value; +} + +/** + * The argument object that gets passed through the value + * back to the ValueVisitor. Subclasses of ValueVisitor should + * subclass ValueVisitorArgs to contain the data they need + * to operate. + */ +struct ValueVisitorArgs {}; + +/** + * Visits a value and runs the appropriate method based on its type. + */ +struct ValueVisitor { + virtual void visit(Reference& reference, ValueVisitorArgs& args) { + visitItem(reference, args); + } + + virtual void visit(RawString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(String& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(StyledString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(FileReference& file, ValueVisitorArgs& args) { + visitItem(file, args); + } + + virtual void visit(Id& id, ValueVisitorArgs& args) { + visitItem(id, args); + } + + virtual void visit(BinaryPrimitive& primitive, ValueVisitorArgs& args) { + visitItem(primitive, args); + } + + virtual void visit(Attribute& attr, ValueVisitorArgs& args) {} + virtual void visit(Style& style, ValueVisitorArgs& args) {} + virtual void visit(Array& array, ValueVisitorArgs& args) {} + virtual void visit(Plural& array, ValueVisitorArgs& args) {} + virtual void visit(Styleable& styleable, ValueVisitorArgs& args) {} + + virtual void visitItem(Item& item, ValueVisitorArgs& args) {} +}; + +/** + * Const version of ValueVisitor. + */ +struct ConstValueVisitor { + virtual void visit(const Reference& reference, ValueVisitorArgs& args) { + visitItem(reference, args); + } + + virtual void visit(const RawString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const String& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const StyledString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const FileReference& file, ValueVisitorArgs& args) { + visitItem(file, args); + } + + virtual void visit(const Id& id, ValueVisitorArgs& args) { + visitItem(id, args); + } + + virtual void visit(const BinaryPrimitive& primitive, ValueVisitorArgs& args) { + visitItem(primitive, args); + } + + virtual void visit(const Attribute& attr, ValueVisitorArgs& args) {} + virtual void visit(const Style& style, ValueVisitorArgs& args) {} + virtual void visit(const Array& array, ValueVisitorArgs& args) {} + virtual void visit(const Plural& array, ValueVisitorArgs& args) {} + virtual void visit(const Styleable& styleable, ValueVisitorArgs& args) {} + + virtual void visitItem(const Item& item, ValueVisitorArgs& args) {} +}; + +/** + * Convenience Visitor that forwards a specific type to a function. + * Args are not used as the function can bind variables. Do not use + * directly, use the wrapper visitFunc() method. + */ +template <typename T, typename TFunc> +struct ValueVisitorFunc : ValueVisitor { + TFunc func; + + ValueVisitorFunc(TFunc f) : func(f) { + } + + void visit(T& value, ValueVisitorArgs&) override { + func(value); + } +}; + +/** + * Const version of ValueVisitorFunc. + */ +template <typename T, typename TFunc> +struct ConstValueVisitorFunc : ConstValueVisitor { + TFunc func; + + ConstValueVisitorFunc(TFunc f) : func(f) { + } + + void visit(const T& value, ValueVisitorArgs&) override { + func(value); + } +}; + +template <typename T, typename TFunc> +void visitFunc(Value& value, TFunc f) { + ValueVisitorFunc<T, TFunc> visitor(f); + value.accept(visitor, ValueVisitorArgs{}); +} + +template <typename T, typename TFunc> +void visitFunc(const Value& value, TFunc f) { + ConstValueVisitorFunc<T, TFunc> visitor(f); + value.accept(visitor, ValueVisitorArgs{}); +} + +template <typename Derived> +void BaseValue<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { + visitor.visit(static_cast<Derived&>(*this), args); +} + +template <typename Derived> +void BaseValue<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { + visitor.visit(static_cast<const Derived&>(*this), args); +} + +template <typename Derived> +void BaseItem<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { + visitor.visit(static_cast<Derived&>(*this), args); +} + +template <typename Derived> +void BaseItem<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { + visitor.visit(static_cast<const Derived&>(*this), args); +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_VALUES_H diff --git a/tools/aapt2/Resource_test.cpp b/tools/aapt2/Resource_test.cpp new file mode 100644 index 0000000..d957999 --- /dev/null +++ b/tools/aapt2/Resource_test.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <gtest/gtest.h> + +#include "Resource.h" + +namespace aapt { + +TEST(ResourceTypeTest, ParseResourceTypes) { + const ResourceType* type = parseResourceType(u"anim"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAnim); + + type = parseResourceType(u"animator"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAnimator); + + type = parseResourceType(u"array"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kArray); + + type = parseResourceType(u"attr"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAttr); + + type = parseResourceType(u"^attr-private"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAttrPrivate); + + type = parseResourceType(u"bool"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kBool); + + type = parseResourceType(u"color"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kColor); + + type = parseResourceType(u"dimen"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kDimen); + + type = parseResourceType(u"drawable"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kDrawable); + + type = parseResourceType(u"fraction"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kFraction); + + type = parseResourceType(u"id"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kId); + + type = parseResourceType(u"integer"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kInteger); + + type = parseResourceType(u"integer-array"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kIntegerArray); + + type = parseResourceType(u"interpolator"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kInterpolator); + + type = parseResourceType(u"layout"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kLayout); + + type = parseResourceType(u"menu"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kMenu); + + type = parseResourceType(u"mipmap"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kMipmap); + + type = parseResourceType(u"plurals"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kPlurals); + + type = parseResourceType(u"raw"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kRaw); + + type = parseResourceType(u"string"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kString); + + type = parseResourceType(u"style"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kStyle); + + type = parseResourceType(u"transition"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kTransition); + + type = parseResourceType(u"xml"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kXml); + + type = parseResourceType(u"blahaha"); + EXPECT_EQ(type, nullptr); +} + +} // namespace aapt diff --git a/tools/aapt2/ScopedXmlPullParser.cpp b/tools/aapt2/ScopedXmlPullParser.cpp new file mode 100644 index 0000000..48da93e --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ScopedXmlPullParser.h" + +#include <string> + +namespace aapt { + +ScopedXmlPullParser::ScopedXmlPullParser(XmlPullParser* parser) : + mParser(parser), mDepth(parser->getDepth()), mDone(false) { +} + +ScopedXmlPullParser::~ScopedXmlPullParser() { + while (isGoodEvent(next())); +} + +XmlPullParser::Event ScopedXmlPullParser::next() { + if (mDone) { + return Event::kEndDocument; + } + + const Event event = mParser->next(); + if (mParser->getDepth() <= mDepth) { + mDone = true; + } + return event; +} + +XmlPullParser::Event ScopedXmlPullParser::getEvent() const { + return mParser->getEvent(); +} + +const std::string& ScopedXmlPullParser::getLastError() const { + return mParser->getLastError(); +} + +const std::u16string& ScopedXmlPullParser::getComment() const { + return mParser->getComment(); +} + +size_t ScopedXmlPullParser::getLineNumber() const { + return mParser->getLineNumber(); +} + +size_t ScopedXmlPullParser::getDepth() const { + const size_t depth = mParser->getDepth(); + if (depth < mDepth) { + return 0; + } + return depth - mDepth; +} + +const std::u16string& ScopedXmlPullParser::getText() const { + return mParser->getText(); +} + +const std::u16string& ScopedXmlPullParser::getNamespacePrefix() const { + return mParser->getNamespacePrefix(); +} + +const std::u16string& ScopedXmlPullParser::getNamespaceUri() const { + return mParser->getNamespaceUri(); +} + +bool ScopedXmlPullParser::applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const { + return mParser->applyPackageAlias(package, defaultPackage); +} + +const std::u16string& ScopedXmlPullParser::getElementNamespace() const { + return mParser->getElementNamespace(); +} + +const std::u16string& ScopedXmlPullParser::getElementName() const { + return mParser->getElementName(); +} + +size_t ScopedXmlPullParser::getAttributeCount() const { + return mParser->getAttributeCount(); +} + +XmlPullParser::const_iterator ScopedXmlPullParser::beginAttributes() const { + return mParser->beginAttributes(); +} + +XmlPullParser::const_iterator ScopedXmlPullParser::endAttributes() const { + return mParser->endAttributes(); +} + +} // namespace aapt diff --git a/tools/aapt2/ScopedXmlPullParser.h b/tools/aapt2/ScopedXmlPullParser.h new file mode 100644 index 0000000..a040f60 --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_SCOPED_XML_PULL_PARSER_H +#define AAPT_SCOPED_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <string> + +namespace aapt { + +/** + * An XmlPullParser that will not read past the depth + * of the underlying parser. When this parser is destroyed, + * it moves the underlying parser to the same depth it + * started with. + * + * You can write code like this: + * + * while (XmlPullParser::isGoodEvent(parser.next())) { + * if (parser.getEvent() != XmlPullParser::Event::StartElement) { + * continue; + * } + * + * ScopedXmlPullParser scoped(parser); + * if (parser.getElementName() == u"id") { + * // do work. + * } else { + * // do nothing, as all the sub elements will be skipped + * // when scoped goes out of scope. + * } + * } + */ +class ScopedXmlPullParser : public XmlPullParser { +public: + ScopedXmlPullParser(XmlPullParser* parser); + ScopedXmlPullParser(const ScopedXmlPullParser&) = delete; + ScopedXmlPullParser& operator=(const ScopedXmlPullParser&) = delete; + ~ScopedXmlPullParser(); + + Event getEvent() const override; + const std::string& getLastError() const override; + Event next() override; + + const std::u16string& getComment() const override; + size_t getLineNumber() const override; + size_t getDepth() const override; + + const std::u16string& getText() const override; + + const std::u16string& getNamespacePrefix() const override; + const std::u16string& getNamespaceUri() const override; + bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) + const override; + + const std::u16string& getElementNamespace() const override; + const std::u16string& getElementName() const override; + + const_iterator beginAttributes() const override; + const_iterator endAttributes() const override; + size_t getAttributeCount() const override; + +private: + XmlPullParser* mParser; + size_t mDepth; + bool mDone; +}; + +} // namespace aapt + +#endif // AAPT_SCOPED_XML_PULL_PARSER_H diff --git a/tools/aapt2/ScopedXmlPullParser_test.cpp b/tools/aapt2/ScopedXmlPullParser_test.cpp new file mode 100644 index 0000000..342f305 --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser_test.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "ScopedXmlPullParser.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(ScopedXmlPullParserTest, StopIteratingAtNoNZeroDepth) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(XmlPullParser::Event::kEndElement, scopedParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, scopedParser.next()); + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +TEST(ScopedXmlPullParserTest, FinishCurrentElementOnDestruction) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +TEST(ScopedXmlPullParserTest, NestedParsersOperateCorrectly) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string><foo></foo></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(std::u16string(u"string"), scopedParser.getElementName()); + while (XmlPullParser::isGoodEvent(scopedParser.next())) { + if (scopedParser.getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser subScopedParser(&scopedParser); + EXPECT_EQ(std::u16string(u"foo"), subScopedParser.getElementName()); + } + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +} // namespace aapt diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp new file mode 100644 index 0000000..9bdae49 --- /dev/null +++ b/tools/aapt2/SdkConstants.cpp @@ -0,0 +1,737 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "SdkConstants.h" + +#include <algorithm> +#include <string> +#include <unordered_map> +#include <vector> + +namespace aapt { + +static const std::vector<std::pair<uint16_t, size_t>> sAttrIdMap = { + { 0x021c, 1 }, + { 0x021d, 2 }, + { 0x0269, SDK_CUPCAKE }, + { 0x028d, SDK_DONUT }, + { 0x02ad, SDK_ECLAIR }, + { 0x02b3, SDK_ECLAIR_0_1 }, + { 0x02b5, SDK_ECLAIR_MR1 }, + { 0x02bd, SDK_FROYO }, + { 0x02cb, SDK_GINGERBREAD }, + { 0x0361, SDK_HONEYCOMB }, + { 0x0366, SDK_HONEYCOMB_MR1 }, + { 0x03a6, SDK_HONEYCOMB_MR2 }, + { 0x03ae, SDK_JELLY_BEAN }, + { 0x03cc, SDK_JELLY_BEAN_MR1 }, + { 0x03da, SDK_JELLY_BEAN_MR2 }, + { 0x03f1, SDK_KITKAT }, + { 0x03f6, SDK_KITKAT_WATCH }, + { 0x04ce, SDK_LOLLIPOP }, +}; + +static bool lessEntryId(const std::pair<uint16_t, size_t>& p, uint16_t entryId) { + return p.first < entryId; +} + +size_t findAttributeSdkLevel(ResourceId id) { + if (id.packageId() != 0x01 && id.typeId() != 0x01) { + return 0; + } + auto iter = std::lower_bound(sAttrIdMap.begin(), sAttrIdMap.end(), id.entryId(), lessEntryId); + if (iter == sAttrIdMap.end()) { + return SDK_LOLLIPOP_MR1; + } + return iter->second; +} + +static const std::unordered_map<std::u16string, size_t> sAttrMap = { + { u"marqueeRepeatLimit", 2 }, + { u"windowNoDisplay", 3 }, + { u"backgroundDimEnabled", 3 }, + { u"inputType", 3 }, + { u"isDefault", 3 }, + { u"windowDisablePreview", 3 }, + { u"privateImeOptions", 3 }, + { u"editorExtras", 3 }, + { u"settingsActivity", 3 }, + { u"fastScrollEnabled", 3 }, + { u"reqTouchScreen", 3 }, + { u"reqKeyboardType", 3 }, + { u"reqHardKeyboard", 3 }, + { u"reqNavigation", 3 }, + { u"windowSoftInputMode", 3 }, + { u"imeFullscreenBackground", 3 }, + { u"noHistory", 3 }, + { u"headerDividersEnabled", 3 }, + { u"footerDividersEnabled", 3 }, + { u"candidatesTextStyleSpans", 3 }, + { u"smoothScrollbar", 3 }, + { u"reqFiveWayNav", 3 }, + { u"keyBackground", 3 }, + { u"keyTextSize", 3 }, + { u"labelTextSize", 3 }, + { u"keyTextColor", 3 }, + { u"keyPreviewLayout", 3 }, + { u"keyPreviewOffset", 3 }, + { u"keyPreviewHeight", 3 }, + { u"verticalCorrection", 3 }, + { u"popupLayout", 3 }, + { u"state_long_pressable", 3 }, + { u"keyWidth", 3 }, + { u"keyHeight", 3 }, + { u"horizontalGap", 3 }, + { u"verticalGap", 3 }, + { u"rowEdgeFlags", 3 }, + { u"codes", 3 }, + { u"popupKeyboard", 3 }, + { u"popupCharacters", 3 }, + { u"keyEdgeFlags", 3 }, + { u"isModifier", 3 }, + { u"isSticky", 3 }, + { u"isRepeatable", 3 }, + { u"iconPreview", 3 }, + { u"keyOutputText", 3 }, + { u"keyLabel", 3 }, + { u"keyIcon", 3 }, + { u"keyboardMode", 3 }, + { u"isScrollContainer", 3 }, + { u"fillEnabled", 3 }, + { u"updatePeriodMillis", 3 }, + { u"initialLayout", 3 }, + { u"voiceSearchMode", 3 }, + { u"voiceLanguageModel", 3 }, + { u"voicePromptText", 3 }, + { u"voiceLanguage", 3 }, + { u"voiceMaxResults", 3 }, + { u"bottomOffset", 3 }, + { u"topOffset", 3 }, + { u"allowSingleTap", 3 }, + { u"handle", 3 }, + { u"content", 3 }, + { u"animateOnClick", 3 }, + { u"configure", 3 }, + { u"hapticFeedbackEnabled", 3 }, + { u"innerRadius", 3 }, + { u"thickness", 3 }, + { u"sharedUserLabel", 3 }, + { u"dropDownWidth", 3 }, + { u"dropDownAnchor", 3 }, + { u"imeOptions", 3 }, + { u"imeActionLabel", 3 }, + { u"imeActionId", 3 }, + { u"imeExtractEnterAnimation", 3 }, + { u"imeExtractExitAnimation", 3 }, + { u"tension", 4 }, + { u"extraTension", 4 }, + { u"anyDensity", 4 }, + { u"searchSuggestThreshold", 4 }, + { u"includeInGlobalSearch", 4 }, + { u"onClick", 4 }, + { u"targetSdkVersion", 4 }, + { u"maxSdkVersion", 4 }, + { u"testOnly", 4 }, + { u"contentDescription", 4 }, + { u"gestureStrokeWidth", 4 }, + { u"gestureColor", 4 }, + { u"uncertainGestureColor", 4 }, + { u"fadeOffset", 4 }, + { u"fadeDuration", 4 }, + { u"gestureStrokeType", 4 }, + { u"gestureStrokeLengthThreshold", 4 }, + { u"gestureStrokeSquarenessThreshold", 4 }, + { u"gestureStrokeAngleThreshold", 4 }, + { u"eventsInterceptionEnabled", 4 }, + { u"fadeEnabled", 4 }, + { u"backupAgent", 4 }, + { u"allowBackup", 4 }, + { u"glEsVersion", 4 }, + { u"queryAfterZeroResults", 4 }, + { u"dropDownHeight", 4 }, + { u"smallScreens", 4 }, + { u"normalScreens", 4 }, + { u"largeScreens", 4 }, + { u"progressBarStyleInverse", 4 }, + { u"progressBarStyleSmallInverse", 4 }, + { u"progressBarStyleLargeInverse", 4 }, + { u"searchSettingsDescription", 4 }, + { u"textColorPrimaryInverseDisableOnly", 4 }, + { u"autoUrlDetect", 4 }, + { u"resizeable", 4 }, + { u"required", 5 }, + { u"accountType", 5 }, + { u"contentAuthority", 5 }, + { u"userVisible", 5 }, + { u"windowShowWallpaper", 5 }, + { u"wallpaperOpenEnterAnimation", 5 }, + { u"wallpaperOpenExitAnimation", 5 }, + { u"wallpaperCloseEnterAnimation", 5 }, + { u"wallpaperCloseExitAnimation", 5 }, + { u"wallpaperIntraOpenEnterAnimation", 5 }, + { u"wallpaperIntraOpenExitAnimation", 5 }, + { u"wallpaperIntraCloseEnterAnimation", 5 }, + { u"wallpaperIntraCloseExitAnimation", 5 }, + { u"supportsUploading", 5 }, + { u"killAfterRestore", 5 }, + { u"restoreNeedsApplication", 5 }, + { u"smallIcon", 5 }, + { u"accountPreferences", 5 }, + { u"textAppearanceSearchResultSubtitle", 5 }, + { u"textAppearanceSearchResultTitle", 5 }, + { u"summaryColumn", 5 }, + { u"detailColumn", 5 }, + { u"detailSocialSummary", 5 }, + { u"thumbnail", 5 }, + { u"detachWallpaper", 5 }, + { u"finishOnCloseSystemDialogs", 5 }, + { u"scrollbarFadeDuration", 5 }, + { u"scrollbarDefaultDelayBeforeFade", 5 }, + { u"fadeScrollbars", 5 }, + { u"colorBackgroundCacheHint", 5 }, + { u"dropDownHorizontalOffset", 5 }, + { u"dropDownVerticalOffset", 5 }, + { u"quickContactBadgeStyleWindowSmall", 6 }, + { u"quickContactBadgeStyleWindowMedium", 6 }, + { u"quickContactBadgeStyleWindowLarge", 6 }, + { u"quickContactBadgeStyleSmallWindowSmall", 6 }, + { u"quickContactBadgeStyleSmallWindowMedium", 6 }, + { u"quickContactBadgeStyleSmallWindowLarge", 6 }, + { u"author", 7 }, + { u"autoStart", 7 }, + { u"expandableListViewWhiteStyle", 8 }, + { u"installLocation", 8 }, + { u"vmSafeMode", 8 }, + { u"webTextViewStyle", 8 }, + { u"restoreAnyVersion", 8 }, + { u"tabStripLeft", 8 }, + { u"tabStripRight", 8 }, + { u"tabStripEnabled", 8 }, + { u"logo", 9 }, + { u"xlargeScreens", 9 }, + { u"immersive", 9 }, + { u"overScrollMode", 9 }, + { u"overScrollHeader", 9 }, + { u"overScrollFooter", 9 }, + { u"filterTouchesWhenObscured", 9 }, + { u"textSelectHandleLeft", 9 }, + { u"textSelectHandleRight", 9 }, + { u"textSelectHandle", 9 }, + { u"textSelectHandleWindowStyle", 9 }, + { u"popupAnimationStyle", 9 }, + { u"screenSize", 9 }, + { u"screenDensity", 9 }, + { u"allContactsName", 11 }, + { u"windowActionBar", 11 }, + { u"actionBarStyle", 11 }, + { u"navigationMode", 11 }, + { u"displayOptions", 11 }, + { u"subtitle", 11 }, + { u"customNavigationLayout", 11 }, + { u"hardwareAccelerated", 11 }, + { u"measureWithLargestChild", 11 }, + { u"animateFirstView", 11 }, + { u"dropDownSpinnerStyle", 11 }, + { u"actionDropDownStyle", 11 }, + { u"actionButtonStyle", 11 }, + { u"showAsAction", 11 }, + { u"previewImage", 11 }, + { u"actionModeBackground", 11 }, + { u"actionModeCloseDrawable", 11 }, + { u"windowActionModeOverlay", 11 }, + { u"valueFrom", 11 }, + { u"valueTo", 11 }, + { u"valueType", 11 }, + { u"propertyName", 11 }, + { u"ordering", 11 }, + { u"fragment", 11 }, + { u"windowActionBarOverlay", 11 }, + { u"fragmentOpenEnterAnimation", 11 }, + { u"fragmentOpenExitAnimation", 11 }, + { u"fragmentCloseEnterAnimation", 11 }, + { u"fragmentCloseExitAnimation", 11 }, + { u"fragmentFadeEnterAnimation", 11 }, + { u"fragmentFadeExitAnimation", 11 }, + { u"actionBarSize", 11 }, + { u"imeSubtypeLocale", 11 }, + { u"imeSubtypeMode", 11 }, + { u"imeSubtypeExtraValue", 11 }, + { u"splitMotionEvents", 11 }, + { u"listChoiceBackgroundIndicator", 11 }, + { u"spinnerMode", 11 }, + { u"animateLayoutChanges", 11 }, + { u"actionBarTabStyle", 11 }, + { u"actionBarTabBarStyle", 11 }, + { u"actionBarTabTextStyle", 11 }, + { u"actionOverflowButtonStyle", 11 }, + { u"actionModeCloseButtonStyle", 11 }, + { u"titleTextStyle", 11 }, + { u"subtitleTextStyle", 11 }, + { u"iconifiedByDefault", 11 }, + { u"actionLayout", 11 }, + { u"actionViewClass", 11 }, + { u"activatedBackgroundIndicator", 11 }, + { u"state_activated", 11 }, + { u"listPopupWindowStyle", 11 }, + { u"popupMenuStyle", 11 }, + { u"textAppearanceLargePopupMenu", 11 }, + { u"textAppearanceSmallPopupMenu", 11 }, + { u"breadCrumbTitle", 11 }, + { u"breadCrumbShortTitle", 11 }, + { u"listDividerAlertDialog", 11 }, + { u"textColorAlertDialogListItem", 11 }, + { u"loopViews", 11 }, + { u"dialogTheme", 11 }, + { u"alertDialogTheme", 11 }, + { u"dividerVertical", 11 }, + { u"homeAsUpIndicator", 11 }, + { u"enterFadeDuration", 11 }, + { u"exitFadeDuration", 11 }, + { u"selectableItemBackground", 11 }, + { u"autoAdvanceViewId", 11 }, + { u"useIntrinsicSizeAsMinimum", 11 }, + { u"actionModeCutDrawable", 11 }, + { u"actionModeCopyDrawable", 11 }, + { u"actionModePasteDrawable", 11 }, + { u"textEditPasteWindowLayout", 11 }, + { u"textEditNoPasteWindowLayout", 11 }, + { u"textIsSelectable", 11 }, + { u"windowEnableSplitTouch", 11 }, + { u"indeterminateProgressStyle", 11 }, + { u"progressBarPadding", 11 }, + { u"animationResolution", 11 }, + { u"state_accelerated", 11 }, + { u"baseline", 11 }, + { u"homeLayout", 11 }, + { u"opacity", 11 }, + { u"alpha", 11 }, + { u"transformPivotX", 11 }, + { u"transformPivotY", 11 }, + { u"translationX", 11 }, + { u"translationY", 11 }, + { u"scaleX", 11 }, + { u"scaleY", 11 }, + { u"rotation", 11 }, + { u"rotationX", 11 }, + { u"rotationY", 11 }, + { u"showDividers", 11 }, + { u"dividerPadding", 11 }, + { u"borderlessButtonStyle", 11 }, + { u"dividerHorizontal", 11 }, + { u"itemPadding", 11 }, + { u"buttonBarStyle", 11 }, + { u"buttonBarButtonStyle", 11 }, + { u"segmentedButtonStyle", 11 }, + { u"staticWallpaperPreview", 11 }, + { u"allowParallelSyncs", 11 }, + { u"isAlwaysSyncable", 11 }, + { u"verticalScrollbarPosition", 11 }, + { u"fastScrollAlwaysVisible", 11 }, + { u"fastScrollThumbDrawable", 11 }, + { u"fastScrollPreviewBackgroundLeft", 11 }, + { u"fastScrollPreviewBackgroundRight", 11 }, + { u"fastScrollTrackDrawable", 11 }, + { u"fastScrollOverlayPosition", 11 }, + { u"customTokens", 11 }, + { u"nextFocusForward", 11 }, + { u"firstDayOfWeek", 11 }, + { u"showWeekNumber", 11 }, + { u"minDate", 11 }, + { u"maxDate", 11 }, + { u"shownWeekCount", 11 }, + { u"selectedWeekBackgroundColor", 11 }, + { u"focusedMonthDateColor", 11 }, + { u"unfocusedMonthDateColor", 11 }, + { u"weekNumberColor", 11 }, + { u"weekSeparatorLineColor", 11 }, + { u"selectedDateVerticalBar", 11 }, + { u"weekDayTextAppearance", 11 }, + { u"dateTextAppearance", 11 }, + { u"solidColor", 11 }, + { u"spinnersShown", 11 }, + { u"calendarViewShown", 11 }, + { u"state_multiline", 11 }, + { u"detailsElementBackground", 11 }, + { u"textColorHighlightInverse", 11 }, + { u"textColorLinkInverse", 11 }, + { u"editTextColor", 11 }, + { u"editTextBackground", 11 }, + { u"horizontalScrollViewStyle", 11 }, + { u"layerType", 11 }, + { u"alertDialogIcon", 11 }, + { u"windowMinWidthMajor", 11 }, + { u"windowMinWidthMinor", 11 }, + { u"queryHint", 11 }, + { u"fastScrollTextColor", 11 }, + { u"largeHeap", 11 }, + { u"windowCloseOnTouchOutside", 11 }, + { u"datePickerStyle", 11 }, + { u"calendarViewStyle", 11 }, + { u"textEditSidePasteWindowLayout", 11 }, + { u"textEditSideNoPasteWindowLayout", 11 }, + { u"actionMenuTextAppearance", 11 }, + { u"actionMenuTextColor", 11 }, + { u"textCursorDrawable", 12 }, + { u"resizeMode", 12 }, + { u"requiresSmallestWidthDp", 12 }, + { u"compatibleWidthLimitDp", 12 }, + { u"largestWidthLimitDp", 12 }, + { u"state_hovered", 13 }, + { u"state_drag_can_accept", 13 }, + { u"state_drag_hovered", 13 }, + { u"stopWithTask", 13 }, + { u"switchTextOn", 13 }, + { u"switchTextOff", 13 }, + { u"switchPreferenceStyle", 13 }, + { u"switchTextAppearance", 13 }, + { u"track", 13 }, + { u"switchMinWidth", 13 }, + { u"switchPadding", 13 }, + { u"thumbTextPadding", 13 }, + { u"textSuggestionsWindowStyle", 13 }, + { u"textEditSuggestionItemLayout", 13 }, + { u"rowCount", 13 }, + { u"rowOrderPreserved", 13 }, + { u"columnCount", 13 }, + { u"columnOrderPreserved", 13 }, + { u"useDefaultMargins", 13 }, + { u"alignmentMode", 13 }, + { u"layout_row", 13 }, + { u"layout_rowSpan", 13 }, + { u"layout_columnSpan", 13 }, + { u"actionModeSelectAllDrawable", 13 }, + { u"isAuxiliary", 13 }, + { u"accessibilityEventTypes", 13 }, + { u"packageNames", 13 }, + { u"accessibilityFeedbackType", 13 }, + { u"notificationTimeout", 13 }, + { u"accessibilityFlags", 13 }, + { u"canRetrieveWindowContent", 13 }, + { u"listPreferredItemHeightLarge", 13 }, + { u"listPreferredItemHeightSmall", 13 }, + { u"actionBarSplitStyle", 13 }, + { u"actionProviderClass", 13 }, + { u"backgroundStacked", 13 }, + { u"backgroundSplit", 13 }, + { u"textAllCaps", 13 }, + { u"colorPressedHighlight", 13 }, + { u"colorLongPressedHighlight", 13 }, + { u"colorFocusedHighlight", 13 }, + { u"colorActivatedHighlight", 13 }, + { u"colorMultiSelectHighlight", 13 }, + { u"drawableStart", 13 }, + { u"drawableEnd", 13 }, + { u"actionModeStyle", 13 }, + { u"minResizeWidth", 13 }, + { u"minResizeHeight", 13 }, + { u"actionBarWidgetTheme", 13 }, + { u"uiOptions", 13 }, + { u"subtypeLocale", 13 }, + { u"subtypeExtraValue", 13 }, + { u"actionBarDivider", 13 }, + { u"actionBarItemBackground", 13 }, + { u"actionModeSplitBackground", 13 }, + { u"textAppearanceListItem", 13 }, + { u"textAppearanceListItemSmall", 13 }, + { u"targetDescriptions", 13 }, + { u"directionDescriptions", 13 }, + { u"overridesImplicitlyEnabledSubtype", 13 }, + { u"listPreferredItemPaddingLeft", 13 }, + { u"listPreferredItemPaddingRight", 13 }, + { u"requiresFadingEdge", 13 }, + { u"publicKey", 13 }, + { u"parentActivityName", 16 }, + { u"isolatedProcess", 16 }, + { u"importantForAccessibility", 16 }, + { u"keyboardLayout", 16 }, + { u"fontFamily", 16 }, + { u"mediaRouteButtonStyle", 16 }, + { u"mediaRouteTypes", 16 }, + { u"supportsRtl", 17 }, + { u"textDirection", 17 }, + { u"textAlignment", 17 }, + { u"layoutDirection", 17 }, + { u"paddingStart", 17 }, + { u"paddingEnd", 17 }, + { u"layout_marginStart", 17 }, + { u"layout_marginEnd", 17 }, + { u"layout_toStartOf", 17 }, + { u"layout_toEndOf", 17 }, + { u"layout_alignStart", 17 }, + { u"layout_alignEnd", 17 }, + { u"layout_alignParentStart", 17 }, + { u"layout_alignParentEnd", 17 }, + { u"listPreferredItemPaddingStart", 17 }, + { u"listPreferredItemPaddingEnd", 17 }, + { u"singleUser", 17 }, + { u"presentationTheme", 17 }, + { u"subtypeId", 17 }, + { u"initialKeyguardLayout", 17 }, + { u"widgetCategory", 17 }, + { u"permissionGroupFlags", 17 }, + { u"labelFor", 17 }, + { u"permissionFlags", 17 }, + { u"checkedTextViewStyle", 17 }, + { u"showOnLockScreen", 17 }, + { u"format12Hour", 17 }, + { u"format24Hour", 17 }, + { u"timeZone", 17 }, + { u"mipMap", 18 }, + { u"mirrorForRtl", 18 }, + { u"windowOverscan", 18 }, + { u"requiredForAllUsers", 18 }, + { u"indicatorStart", 18 }, + { u"indicatorEnd", 18 }, + { u"childIndicatorStart", 18 }, + { u"childIndicatorEnd", 18 }, + { u"restrictedAccountType", 18 }, + { u"requiredAccountType", 18 }, + { u"canRequestTouchExplorationMode", 18 }, + { u"canRequestEnhancedWebAccessibility", 18 }, + { u"canRequestFilterKeyEvents", 18 }, + { u"layoutMode", 18 }, + { u"keySet", 19 }, + { u"targetId", 19 }, + { u"fromScene", 19 }, + { u"toScene", 19 }, + { u"transition", 19 }, + { u"transitionOrdering", 19 }, + { u"fadingMode", 19 }, + { u"startDelay", 19 }, + { u"ssp", 19 }, + { u"sspPrefix", 19 }, + { u"sspPattern", 19 }, + { u"addPrintersActivity", 19 }, + { u"vendor", 19 }, + { u"category", 19 }, + { u"isAsciiCapable", 19 }, + { u"autoMirrored", 19 }, + { u"supportsSwitchingToNextInputMethod", 19 }, + { u"requireDeviceUnlock", 19 }, + { u"apduServiceBanner", 19 }, + { u"accessibilityLiveRegion", 19 }, + { u"windowTranslucentStatus", 19 }, + { u"windowTranslucentNavigation", 19 }, + { u"advancedPrintOptionsActivity", 19 }, + { u"banner", 20 }, + { u"windowSwipeToDismiss", 20 }, + { u"isGame", 20 }, + { u"allowEmbedded", 20 }, + { u"setupActivity", 20 }, + { u"fastScrollStyle", 21 }, + { u"windowContentTransitions", 21 }, + { u"windowContentTransitionManager", 21 }, + { u"translationZ", 21 }, + { u"tintMode", 21 }, + { u"controlX1", 21 }, + { u"controlY1", 21 }, + { u"controlX2", 21 }, + { u"controlY2", 21 }, + { u"transitionName", 21 }, + { u"transitionGroup", 21 }, + { u"viewportWidth", 21 }, + { u"viewportHeight", 21 }, + { u"fillColor", 21 }, + { u"pathData", 21 }, + { u"strokeColor", 21 }, + { u"strokeWidth", 21 }, + { u"trimPathStart", 21 }, + { u"trimPathEnd", 21 }, + { u"trimPathOffset", 21 }, + { u"strokeLineCap", 21 }, + { u"strokeLineJoin", 21 }, + { u"strokeMiterLimit", 21 }, + { u"colorControlNormal", 21 }, + { u"colorControlActivated", 21 }, + { u"colorButtonNormal", 21 }, + { u"colorControlHighlight", 21 }, + { u"persistableMode", 21 }, + { u"titleTextAppearance", 21 }, + { u"subtitleTextAppearance", 21 }, + { u"slideEdge", 21 }, + { u"actionBarTheme", 21 }, + { u"textAppearanceListItemSecondary", 21 }, + { u"colorPrimary", 21 }, + { u"colorPrimaryDark", 21 }, + { u"colorAccent", 21 }, + { u"nestedScrollingEnabled", 21 }, + { u"windowEnterTransition", 21 }, + { u"windowExitTransition", 21 }, + { u"windowSharedElementEnterTransition", 21 }, + { u"windowSharedElementExitTransition", 21 }, + { u"windowAllowReturnTransitionOverlap", 21 }, + { u"windowAllowEnterTransitionOverlap", 21 }, + { u"sessionService", 21 }, + { u"stackViewStyle", 21 }, + { u"switchStyle", 21 }, + { u"elevation", 21 }, + { u"excludeId", 21 }, + { u"excludeClass", 21 }, + { u"hideOnContentScroll", 21 }, + { u"actionOverflowMenuStyle", 21 }, + { u"documentLaunchMode", 21 }, + { u"maxRecents", 21 }, + { u"autoRemoveFromRecents", 21 }, + { u"stateListAnimator", 21 }, + { u"toId", 21 }, + { u"fromId", 21 }, + { u"reversible", 21 }, + { u"splitTrack", 21 }, + { u"targetName", 21 }, + { u"excludeName", 21 }, + { u"matchOrder", 21 }, + { u"windowDrawsSystemBarBackgrounds", 21 }, + { u"statusBarColor", 21 }, + { u"navigationBarColor", 21 }, + { u"contentInsetStart", 21 }, + { u"contentInsetEnd", 21 }, + { u"contentInsetLeft", 21 }, + { u"contentInsetRight", 21 }, + { u"paddingMode", 21 }, + { u"layout_rowWeight", 21 }, + { u"layout_columnWeight", 21 }, + { u"translateX", 21 }, + { u"translateY", 21 }, + { u"selectableItemBackgroundBorderless", 21 }, + { u"elegantTextHeight", 21 }, + { u"searchKeyphraseId", 21 }, + { u"searchKeyphrase", 21 }, + { u"searchKeyphraseSupportedLocales", 21 }, + { u"windowTransitionBackgroundFadeDuration", 21 }, + { u"overlapAnchor", 21 }, + { u"progressTint", 21 }, + { u"progressTintMode", 21 }, + { u"progressBackgroundTint", 21 }, + { u"progressBackgroundTintMode", 21 }, + { u"secondaryProgressTint", 21 }, + { u"secondaryProgressTintMode", 21 }, + { u"indeterminateTint", 21 }, + { u"indeterminateTintMode", 21 }, + { u"backgroundTint", 21 }, + { u"backgroundTintMode", 21 }, + { u"foregroundTint", 21 }, + { u"foregroundTintMode", 21 }, + { u"buttonTint", 21 }, + { u"buttonTintMode", 21 }, + { u"thumbTint", 21 }, + { u"thumbTintMode", 21 }, + { u"fullBackupOnly", 21 }, + { u"propertyXName", 21 }, + { u"propertyYName", 21 }, + { u"relinquishTaskIdentity", 21 }, + { u"tileModeX", 21 }, + { u"tileModeY", 21 }, + { u"actionModeShareDrawable", 21 }, + { u"actionModeFindDrawable", 21 }, + { u"actionModeWebSearchDrawable", 21 }, + { u"transitionVisibilityMode", 21 }, + { u"minimumHorizontalAngle", 21 }, + { u"minimumVerticalAngle", 21 }, + { u"maximumAngle", 21 }, + { u"searchViewStyle", 21 }, + { u"closeIcon", 21 }, + { u"goIcon", 21 }, + { u"searchIcon", 21 }, + { u"voiceIcon", 21 }, + { u"commitIcon", 21 }, + { u"suggestionRowLayout", 21 }, + { u"queryBackground", 21 }, + { u"submitBackground", 21 }, + { u"buttonBarPositiveButtonStyle", 21 }, + { u"buttonBarNeutralButtonStyle", 21 }, + { u"buttonBarNegativeButtonStyle", 21 }, + { u"popupElevation", 21 }, + { u"actionBarPopupTheme", 21 }, + { u"multiArch", 21 }, + { u"touchscreenBlocksFocus", 21 }, + { u"windowElevation", 21 }, + { u"launchTaskBehindTargetAnimation", 21 }, + { u"launchTaskBehindSourceAnimation", 21 }, + { u"restrictionType", 21 }, + { u"dayOfWeekBackground", 21 }, + { u"dayOfWeekTextAppearance", 21 }, + { u"headerMonthTextAppearance", 21 }, + { u"headerDayOfMonthTextAppearance", 21 }, + { u"headerYearTextAppearance", 21 }, + { u"yearListItemTextAppearance", 21 }, + { u"yearListSelectorColor", 21 }, + { u"calendarTextColor", 21 }, + { u"recognitionService", 21 }, + { u"timePickerStyle", 21 }, + { u"timePickerDialogTheme", 21 }, + { u"headerTimeTextAppearance", 21 }, + { u"headerAmPmTextAppearance", 21 }, + { u"numbersTextColor", 21 }, + { u"numbersBackgroundColor", 21 }, + { u"numbersSelectorColor", 21 }, + { u"amPmTextColor", 21 }, + { u"amPmBackgroundColor", 21 }, + { u"searchKeyphraseRecognitionFlags", 21 }, + { u"checkMarkTint", 21 }, + { u"checkMarkTintMode", 21 }, + { u"popupTheme", 21 }, + { u"toolbarStyle", 21 }, + { u"windowClipToOutline", 21 }, + { u"datePickerDialogTheme", 21 }, + { u"showText", 21 }, + { u"windowReturnTransition", 21 }, + { u"windowReenterTransition", 21 }, + { u"windowSharedElementReturnTransition", 21 }, + { u"windowSharedElementReenterTransition", 21 }, + { u"resumeWhilePausing", 21 }, + { u"datePickerMode", 21 }, + { u"timePickerMode", 21 }, + { u"inset", 21 }, + { u"letterSpacing", 21 }, + { u"fontFeatureSettings", 21 }, + { u"outlineProvider", 21 }, + { u"contentAgeHint", 21 }, + { u"country", 21 }, + { u"windowSharedElementsUseOverlay", 21 }, + { u"reparent", 21 }, + { u"reparentWithOverlay", 21 }, + { u"ambientShadowAlpha", 21 }, + { u"spotShadowAlpha", 21 }, + { u"navigationIcon", 21 }, + { u"navigationContentDescription", 21 }, + { u"fragmentExitTransition", 21 }, + { u"fragmentEnterTransition", 21 }, + { u"fragmentSharedElementEnterTransition", 21 }, + { u"fragmentReturnTransition", 21 }, + { u"fragmentSharedElementReturnTransition", 21 }, + { u"fragmentReenterTransition", 21 }, + { u"fragmentAllowEnterTransitionOverlap", 21 }, + { u"fragmentAllowReturnTransitionOverlap", 21 }, + { u"patternPathData", 21 }, + { u"strokeAlpha", 21 }, + { u"fillAlpha", 21 }, + { u"windowActivityTransitions", 21 }, + { u"colorEdgeEffect", 21 } +}; + +size_t findAttributeSdkLevel(const ResourceName& name) { + if (name.package != u"android" && name.type != ResourceType::kAttr) { + return 0; + } + + auto iter = sAttrMap.find(name.entry); + if (iter != sAttrMap.end()) { + return iter->second; + } + return SDK_LOLLIPOP_MR1; +} + +} // namespace aapt diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h new file mode 100644 index 0000000..803da03 --- /dev/null +++ b/tools/aapt2/SdkConstants.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_SDK_CONSTANTS_H +#define AAPT_SDK_CONSTANTS_H + +#include "Resource.h" + +namespace aapt { + +enum { + SDK_CUPCAKE = 3, + SDK_DONUT = 4, + SDK_ECLAIR = 5, + SDK_ECLAIR_0_1 = 6, + SDK_ECLAIR_MR1 = 7, + SDK_FROYO = 8, + SDK_GINGERBREAD = 9, + SDK_GINGERBREAD_MR1 = 10, + SDK_HONEYCOMB = 11, + SDK_HONEYCOMB_MR1 = 12, + SDK_HONEYCOMB_MR2 = 13, + SDK_ICE_CREAM_SANDWICH = 14, + SDK_ICE_CREAM_SANDWICH_MR1 = 15, + SDK_JELLY_BEAN = 16, + SDK_JELLY_BEAN_MR1 = 17, + SDK_JELLY_BEAN_MR2 = 18, + SDK_KITKAT = 19, + SDK_KITKAT_WATCH = 20, + SDK_LOLLIPOP = 21, + SDK_LOLLIPOP_MR1 = 22, +}; + +size_t findAttributeSdkLevel(ResourceId id); +size_t findAttributeSdkLevel(const ResourceName& name); + +} // namespace aapt + +#endif // AAPT_SDK_CONSTANTS_H diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h new file mode 100644 index 0000000..3606488 --- /dev/null +++ b/tools/aapt2/Source.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_SOURCE_H +#define AAPT_SOURCE_H + +#include <ostream> +#include <string> +#include <tuple> + +namespace aapt { + +struct SourceLineColumn; +struct SourceLine; + +/** + * Represents a file on disk. Used for logging and + * showing errors. + */ +struct Source { + std::string path; + + inline SourceLine line(size_t line) const; +}; + +/** + * Represents a file on disk and a line number in that file. + * Used for logging and showing errors. + */ +struct SourceLine { + std::string path; + size_t line; + + inline SourceLineColumn column(size_t column) const; +}; + +/** + * Represents a file on disk and a line:column number in that file. + * Used for logging and showing errors. + */ +struct SourceLineColumn { + std::string path; + size_t line; + size_t column; +}; + +// +// Implementations +// + +SourceLine Source::line(size_t line) const { + return SourceLine{ path, line }; +} + +SourceLineColumn SourceLine::column(size_t column) const { + return SourceLineColumn{ path, line, column }; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) { + return out << source.path; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const SourceLine& source) { + return out << source.path << ":" << source.line; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const SourceLineColumn& source) { + return out << source.path << ":" << source.line << ":" << source.column; +} + +inline bool operator<(const SourceLine& lhs, const SourceLine& rhs) { + return std::tie(lhs.path, lhs.line) < std::tie(rhs.path, rhs.line); +} + +} // namespace aapt + +#endif // AAPT_SOURCE_H diff --git a/tools/aapt2/SourceXmlPullParser.cpp b/tools/aapt2/SourceXmlPullParser.cpp new file mode 100644 index 0000000..8099044 --- /dev/null +++ b/tools/aapt2/SourceXmlPullParser.cpp @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <iostream> +#include <string> + +#include "Maybe.h" +#include "SourceXmlPullParser.h" +#include "Util.h" + +namespace aapt { + +constexpr char kXmlNamespaceSep = 1; + +SourceXmlPullParser::SourceXmlPullParser(std::istream& in) : mIn(in), mEmpty(), mDepth(0) { + mParser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); + XML_SetUserData(mParser, this); + XML_SetElementHandler(mParser, startElementHandler, endElementHandler); + XML_SetNamespaceDeclHandler(mParser, startNamespaceHandler, endNamespaceHandler); + XML_SetCharacterDataHandler(mParser, characterDataHandler); + XML_SetCommentHandler(mParser, commentDataHandler); + mEventQueue.push(EventData{ Event::kStartDocument, 0, mDepth++ }); +} + +SourceXmlPullParser::~SourceXmlPullParser() { + XML_ParserFree(mParser); +} + +SourceXmlPullParser::Event SourceXmlPullParser::next() { + const Event currentEvent = getEvent(); + if (currentEvent == Event::kBadDocument || currentEvent == Event::kEndDocument) { + return currentEvent; + } + + mEventQueue.pop(); + while (mEventQueue.empty()) { + mIn.read(mBuffer, sizeof(mBuffer) / sizeof(*mBuffer)); + + const bool done = mIn.eof(); + if (mIn.bad() && !done) { + mLastError = strerror(errno); + mEventQueue.push(EventData{ Event::kBadDocument }); + continue; + } + + if (XML_Parse(mParser, mBuffer, mIn.gcount(), done) == XML_STATUS_ERROR) { + mLastError = XML_ErrorString(XML_GetErrorCode(mParser)); + mEventQueue.push(EventData{ Event::kBadDocument }); + continue; + } + + if (done) { + mEventQueue.push(EventData{ Event::kEndDocument, 0, 0 }); + } + } + + Event event = getEvent(); + + // Record namespace prefixes and package names so that we can do our own + // handling of references that use namespace aliases. + if (event == Event::kStartNamespace || event == Event::kEndNamespace) { + Maybe<std::u16string> result = util::extractPackageFromNamespace(getNamespaceUri()); + if (event == Event::kStartNamespace) { + if (result) { + mPackageAliases.emplace_back(getNamespacePrefix(), result.value()); + } + } else { + if (result) { + assert(mPackageAliases.back().second == result.value()); + mPackageAliases.pop_back(); + } + } + } + + return event; +} + +SourceXmlPullParser::Event SourceXmlPullParser::getEvent() const { + return mEventQueue.front().event; +} + +const std::string& SourceXmlPullParser::getLastError() const { + return mLastError; +} + +const std::u16string& SourceXmlPullParser::getComment() const { + return mEventQueue.front().comment; +} + +size_t SourceXmlPullParser::getLineNumber() const { + return mEventQueue.front().lineNumber; +} + +size_t SourceXmlPullParser::getDepth() const { + return mEventQueue.front().depth; +} + +const std::u16string& SourceXmlPullParser::getText() const { + if (getEvent() != Event::kText) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getNamespacePrefix() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getNamespaceUri() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { + return mEmpty; + } + return mEventQueue.front().data2; +} + +bool SourceXmlPullParser::applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const { + const auto endIter = mPackageAliases.rend(); + for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { + if (iter->first == *package) { + if (iter->second.empty()) { + *package = defaultPackage; + } else { + *package = iter->second; + } + return true; + } + } + return false; +} + +const std::u16string& SourceXmlPullParser::getElementNamespace() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getElementName() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { + return mEmpty; + } + return mEventQueue.front().data2; +} + +XmlPullParser::const_iterator SourceXmlPullParser::beginAttributes() const { + return mEventQueue.front().attributes.begin(); +} + +XmlPullParser::const_iterator SourceXmlPullParser::endAttributes() const { + return mEventQueue.front().attributes.end(); +} + +size_t SourceXmlPullParser::getAttributeCount() const { + if (getEvent() != Event::kStartElement) { + return 0; + } + return mEventQueue.front().attributes.size(); +} + +/** + * Extracts the namespace and name of an expanded element or attribute name. + */ +static void splitName(const char* name, std::u16string& outNs, std::u16string& outName) { + const char* p = name; + while (*p != 0 && *p != kXmlNamespaceSep) { + p++; + } + + if (*p == 0) { + outNs = std::u16string(); + outName = util::utf8ToUtf16(name); + } else { + outNs = util::utf8ToUtf16(StringPiece(name, (p - name))); + outName = util::utf8ToUtf16(p + 1); + } +} + +void XMLCALL SourceXmlPullParser::startNamespaceHandler(void* userData, const char* prefix, + const char* uri) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + std::u16string namespaceUri = uri != nullptr ? util::utf8ToUtf16(uri) : std::u16string(); + parser->mNamespaceUris.push(namespaceUri); + parser->mEventQueue.push(EventData{ + Event::kStartNamespace, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth++, + prefix != nullptr ? util::utf8ToUtf16(prefix) : std::u16string(), + namespaceUri + }); +} + +void XMLCALL SourceXmlPullParser::startElementHandler(void* userData, const char* name, + const char** attrs) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + EventData data = { + Event::kStartElement, XML_GetCurrentLineNumber(parser->mParser), parser->mDepth++ + }; + splitName(name, data.data1, data.data2); + + while (*attrs) { + Attribute attribute; + splitName(*attrs++, attribute.namespaceUri, attribute.name); + attribute.value = util::utf8ToUtf16(*attrs++); + + // Insert in sorted order. + auto iter = std::lower_bound(data.attributes.begin(), data.attributes.end(), attribute); + data.attributes.insert(iter, std::move(attribute)); + } + + // Move the structure into the queue (no copy). + parser->mEventQueue.push(std::move(data)); +} + +void XMLCALL SourceXmlPullParser::characterDataHandler(void* userData, const char* s, int len) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kText, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth, + util::utf8ToUtf16(StringPiece(s, len)) + }); +} + +void XMLCALL SourceXmlPullParser::endElementHandler(void* userData, const char* name) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + EventData data = { + Event::kEndElement, XML_GetCurrentLineNumber(parser->mParser), --(parser->mDepth) + }; + splitName(name, data.data1, data.data2); + + // Move the data into the queue (no copy). + parser->mEventQueue.push(std::move(data)); +} + +void XMLCALL SourceXmlPullParser::endNamespaceHandler(void* userData, const char* prefix) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kEndNamespace, + XML_GetCurrentLineNumber(parser->mParser), + --(parser->mDepth), + prefix != nullptr ? util::utf8ToUtf16(prefix) : std::u16string(), + parser->mNamespaceUris.top() + }); + parser->mNamespaceUris.pop(); +} + +void XMLCALL SourceXmlPullParser::commentDataHandler(void* userData, const char* comment) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kComment, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth, + util::utf8ToUtf16(comment) + }); +} + +} // namespace aapt diff --git a/tools/aapt2/SourceXmlPullParser.h b/tools/aapt2/SourceXmlPullParser.h new file mode 100644 index 0000000..15936d6 --- /dev/null +++ b/tools/aapt2/SourceXmlPullParser.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_SOURCE_XML_PULL_PARSER_H +#define AAPT_SOURCE_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <istream> +#include <libexpat/expat.h> +#include <queue> +#include <stack> +#include <string> +#include <vector> + +namespace aapt { + +class SourceXmlPullParser : public XmlPullParser { +public: + SourceXmlPullParser(std::istream& in); + SourceXmlPullParser(const SourceXmlPullParser& rhs) = delete; + ~SourceXmlPullParser(); + + Event getEvent() const override; + const std::string& getLastError() const override ; + Event next() override ; + + const std::u16string& getComment() const override; + size_t getLineNumber() const override; + size_t getDepth() const override; + + const std::u16string& getText() const override; + + const std::u16string& getNamespacePrefix() const override; + const std::u16string& getNamespaceUri() const override; + bool applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const override; + + + const std::u16string& getElementNamespace() const override; + const std::u16string& getElementName() const override; + + const_iterator beginAttributes() const override; + const_iterator endAttributes() const override; + size_t getAttributeCount() const override; + +private: + static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri); + static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs); + static void XMLCALL characterDataHandler(void* userData, const char* s, int len); + static void XMLCALL endElementHandler(void* userData, const char* name); + static void XMLCALL endNamespaceHandler(void* userData, const char* prefix); + static void XMLCALL commentDataHandler(void* userData, const char* comment); + + struct EventData { + Event event; + size_t lineNumber; + size_t depth; + std::u16string data1; + std::u16string data2; + std::u16string comment; + std::vector<Attribute> attributes; + }; + + std::istream& mIn; + XML_Parser mParser; + char mBuffer[16384]; + std::queue<EventData> mEventQueue; + std::string mLastError; + const std::u16string mEmpty; + size_t mDepth; + std::stack<std::u16string> mNamespaceUris; + std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; +}; + +} // namespace aapt + +#endif // AAPT_SOURCE_XML_PULL_PARSER_H diff --git a/tools/aapt2/StringPiece.h b/tools/aapt2/StringPiece.h new file mode 100644 index 0000000..e2a1597 --- /dev/null +++ b/tools/aapt2/StringPiece.h @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_STRING_PIECE_H +#define AAPT_STRING_PIECE_H + +#include <ostream> +#include <string> +#include <utils/String8.h> +#include <utils/Unicode.h> + +namespace aapt { + +/** + * Read only wrapper around basic C strings. + * Prevents excessive copying. + * + * WARNING: When creating from std::basic_string<>, moving the original + * std::basic_string<> will invalidate the data held in a BasicStringPiece<>. + * BasicStringPiece<> should only be used transitively. + */ +template <typename TChar> +class BasicStringPiece { +public: + using const_iterator = const TChar*; + + BasicStringPiece(); + BasicStringPiece(const BasicStringPiece<TChar>& str); + BasicStringPiece(const std::basic_string<TChar>& str); + BasicStringPiece(const TChar* str); + BasicStringPiece(const TChar* str, size_t len); + + BasicStringPiece<TChar>& operator=(const BasicStringPiece<TChar>& rhs); + BasicStringPiece<TChar>& assign(const TChar* str, size_t len); + + BasicStringPiece<TChar> substr(size_t start, size_t len) const; + BasicStringPiece<TChar> substr(BasicStringPiece<TChar>::const_iterator begin, + BasicStringPiece<TChar>::const_iterator end) const; + + const TChar* data() const; + size_t length() const; + size_t size() const; + bool empty() const; + std::basic_string<TChar> toString() const; + + int compare(const BasicStringPiece<TChar>& rhs) const; + bool operator<(const BasicStringPiece<TChar>& rhs) const; + bool operator>(const BasicStringPiece<TChar>& rhs) const; + bool operator==(const BasicStringPiece<TChar>& rhs) const; + bool operator!=(const BasicStringPiece<TChar>& rhs) const; + + const_iterator begin() const; + const_iterator end() const; + +private: + const TChar* mData; + size_t mLength; +}; + +using StringPiece = BasicStringPiece<char>; +using StringPiece16 = BasicStringPiece<char16_t>; + +// +// BasicStringPiece implementation. +// + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece() : mData(nullptr) , mLength(0) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const BasicStringPiece<TChar>& str) : + mData(str.mData), mLength(str.mLength) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const std::basic_string<TChar>& str) : + mData(str.data()), mLength(str.length()) { +} + +template <> +inline BasicStringPiece<char>::BasicStringPiece(const char* str) : + mData(str), mLength(str != nullptr ? strlen(str) : 0) { +} + +template <> +inline BasicStringPiece<char16_t>::BasicStringPiece(const char16_t* str) : + mData(str), mLength(str != nullptr ? strlen16(str) : 0) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const TChar* str, size_t len) : + mData(str), mLength(len) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::operator=( + const BasicStringPiece<TChar>& rhs) { + mData = rhs.mData; + mLength = rhs.mLength; + return *this; +} + +template <typename TChar> +inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::assign(const TChar* str, size_t len) { + mData = str; + mLength = len; + return *this; +} + + +template <typename TChar> +inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(size_t start, size_t len) const { + if (start + len > mLength) { + return BasicStringPiece<TChar>(); + } + return BasicStringPiece<TChar>(mData + start, len); +} + +template <typename TChar> +inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr( + BasicStringPiece<TChar>::const_iterator begin, + BasicStringPiece<TChar>::const_iterator end) const { + return BasicStringPiece<TChar>(begin, end - begin); +} + +template <typename TChar> +inline const TChar* BasicStringPiece<TChar>::data() const { + return mData; +} + +template <typename TChar> +inline size_t BasicStringPiece<TChar>::length() const { + return mLength; +} + +template <typename TChar> +inline size_t BasicStringPiece<TChar>::size() const { + return mLength; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::empty() const { + return mLength == 0; +} + +template <typename TChar> +inline std::basic_string<TChar> BasicStringPiece<TChar>::toString() const { + return std::basic_string<TChar>(mData, mLength); +} + +template <> +inline int BasicStringPiece<char>::compare(const BasicStringPiece<char>& rhs) const { + const char nullStr = '\0'; + const char* b1 = mData != nullptr ? mData : &nullStr; + const char* e1 = b1 + mLength; + const char* b2 = rhs.mData != nullptr ? rhs.mData : &nullStr; + const char* e2 = b2 + rhs.mLength; + + while (b1 < e1 && b2 < e2) { + const int d = static_cast<int>(*b1++) - static_cast<int>(*b2++); + if (d) { + return d; + } + } + return static_cast<int>(mLength - rhs.mLength); +} + +inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char16_t>& str) { + android::String8 utf8(str.data(), str.size()); + return out.write(utf8.string(), utf8.size()); +} + + +template <> +inline int BasicStringPiece<char16_t>::compare(const BasicStringPiece<char16_t>& rhs) const { + const char16_t nullStr = u'\0'; + const char16_t* b1 = mData != nullptr ? mData : &nullStr; + const char16_t* b2 = rhs.mData != nullptr ? rhs.mData : &nullStr; + return strzcmp16(b1, mLength, b2, rhs.mLength); +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator<(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) < 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator>(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) > 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator==(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) == 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator!=(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) != 0; +} + +template <typename TChar> +inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::begin() const { + return mData; +} + +template <typename TChar> +inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::end() const { + return mData + mLength; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char>& str) { + return out.write(str.data(), str.size()); +} + +} // namespace aapt + +#endif // AAPT_STRING_PIECE_H diff --git a/tools/aapt2/StringPiece_test.cpp b/tools/aapt2/StringPiece_test.cpp new file mode 100644 index 0000000..43f7a37 --- /dev/null +++ b/tools/aapt2/StringPiece_test.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <algorithm> +#include <gtest/gtest.h> +#include <string> +#include <vector> + +#include "StringPiece.h" + +namespace aapt { + +TEST(StringPieceTest, CompareNonNullTerminatedPiece) { + StringPiece a("hello world", 5); + StringPiece b("hello moon", 5); + EXPECT_EQ(a, b); + + StringPiece16 a16(u"hello world", 5); + StringPiece16 b16(u"hello moon", 5); + EXPECT_EQ(a16, b16); +} + +TEST(StringPieceTest, PiecesHaveCorrectSortOrder) { + std::u16string testing(u"testing"); + std::u16string banana(u"banana"); + std::u16string car(u"car"); + + EXPECT_TRUE(StringPiece16(testing) > banana); + EXPECT_TRUE(StringPiece16(testing) > car); + EXPECT_TRUE(StringPiece16(banana) < testing); + EXPECT_TRUE(StringPiece16(banana) < car); + EXPECT_TRUE(StringPiece16(car) < testing); + EXPECT_TRUE(StringPiece16(car) > banana); +} + +TEST(StringPieceTest, PiecesHaveCorrectSortOrderUtf8) { + std::string testing("testing"); + std::string banana("banana"); + std::string car("car"); + + EXPECT_TRUE(StringPiece(testing) > banana); + EXPECT_TRUE(StringPiece(testing) > car); + EXPECT_TRUE(StringPiece(banana) < testing); + EXPECT_TRUE(StringPiece(banana) < car); + EXPECT_TRUE(StringPiece(car) < testing); + EXPECT_TRUE(StringPiece(car) > banana); +} + +} // namespace aapt diff --git a/tools/aapt2/StringPool.cpp b/tools/aapt2/StringPool.cpp new file mode 100644 index 0000000..c19aa98 --- /dev/null +++ b/tools/aapt2/StringPool.cpp @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" +#include "StringPiece.h" +#include "StringPool.h" +#include "Util.h" + +#include <algorithm> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <string> + +namespace aapt { + +StringPool::Ref::Ref() : mEntry(nullptr) { +} + +StringPool::Ref::Ref(const StringPool::Ref& rhs) : mEntry(rhs.mEntry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::Ref::Ref(StringPool::Entry* entry) : mEntry(entry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::Ref::~Ref() { + if (mEntry != nullptr) { + mEntry->ref--; + } +} + +StringPool::Ref& StringPool::Ref::operator=(const StringPool::Ref& rhs) { + if (rhs.mEntry != nullptr) { + rhs.mEntry->ref++; + } + + if (mEntry != nullptr) { + mEntry->ref--; + } + mEntry = rhs.mEntry; + return *this; +} + +const std::u16string* StringPool::Ref::operator->() const { + return &mEntry->value; +} + +const std::u16string& StringPool::Ref::operator*() const { + return mEntry->value; +} + +size_t StringPool::Ref::getIndex() const { + return mEntry->index; +} + +const StringPool::Context& StringPool::Ref::getContext() const { + return mEntry->context; +} + +StringPool::StyleRef::StyleRef() : mEntry(nullptr) { +} + +StringPool::StyleRef::StyleRef(const StringPool::StyleRef& rhs) : mEntry(rhs.mEntry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::StyleRef::StyleRef(StringPool::StyleEntry* entry) : mEntry(entry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::StyleRef::~StyleRef() { + if (mEntry != nullptr) { + mEntry->ref--; + } +} + +StringPool::StyleRef& StringPool::StyleRef::operator=(const StringPool::StyleRef& rhs) { + if (rhs.mEntry != nullptr) { + rhs.mEntry->ref++; + } + + if (mEntry != nullptr) { + mEntry->ref--; + } + mEntry = rhs.mEntry; + return *this; +} + +const StringPool::StyleEntry* StringPool::StyleRef::operator->() const { + return mEntry; +} + +const StringPool::StyleEntry& StringPool::StyleRef::operator*() const { + return *mEntry; +} + +size_t StringPool::StyleRef::getIndex() const { + return mEntry->str.getIndex(); +} + +const StringPool::Context& StringPool::StyleRef::getContext() const { + return mEntry->str.getContext(); +} + +StringPool::Ref StringPool::makeRef(const StringPiece16& str) { + return makeRefImpl(str, Context{}, true); +} + +StringPool::Ref StringPool::makeRef(const StringPiece16& str, const Context& context) { + return makeRefImpl(str, context, true); +} + +StringPool::Ref StringPool::makeRefImpl(const StringPiece16& str, const Context& context, + bool unique) { + if (unique) { + auto iter = mIndexedStrings.find(str); + if (iter != std::end(mIndexedStrings)) { + return Ref(iter->second); + } + } + + Entry* entry = new Entry(); + entry->value = str.toString(); + entry->context = context; + entry->index = mStrings.size(); + entry->ref = 0; + mStrings.emplace_back(entry); + mIndexedStrings.insert(std::make_pair(StringPiece16(entry->value), entry)); + return Ref(entry); +} + +StringPool::StyleRef StringPool::makeRef(const StyleString& str) { + return makeRef(str, Context{}); +} + +StringPool::StyleRef StringPool::makeRef(const StyleString& str, const Context& context) { + Entry* entry = new Entry(); + entry->value = str.str; + entry->context = context; + entry->index = mStrings.size(); + entry->ref = 0; + mStrings.emplace_back(entry); + mIndexedStrings.insert(std::make_pair(StringPiece16(entry->value), entry)); + + StyleEntry* styleEntry = new StyleEntry(); + styleEntry->str = Ref(entry); + for (const aapt::Span& span : str.spans) { + styleEntry->spans.emplace_back(Span{makeRef(span.name), + span.firstChar, span.lastChar}); + } + styleEntry->ref = 0; + mStyles.emplace_back(styleEntry); + return StyleRef(styleEntry); +} + +StringPool::StyleRef StringPool::makeRef(const StyleRef& ref) { + Entry* entry = new Entry(); + entry->value = *ref.mEntry->str; + entry->context = ref.mEntry->str.mEntry->context; + entry->index = mStrings.size(); + entry->ref = 0; + mStrings.emplace_back(entry); + mIndexedStrings.insert(std::make_pair(StringPiece16(entry->value), entry)); + + StyleEntry* styleEntry = new StyleEntry(); + styleEntry->str = Ref(entry); + for (const Span& span : ref.mEntry->spans) { + styleEntry->spans.emplace_back(Span{ makeRef(*span.name), span.firstChar, span.lastChar }); + } + styleEntry->ref = 0; + mStyles.emplace_back(styleEntry); + return StyleRef(styleEntry); +} + +void StringPool::merge(StringPool&& pool) { + mIndexedStrings.insert(pool.mIndexedStrings.begin(), pool.mIndexedStrings.end()); + pool.mIndexedStrings.clear(); + std::move(pool.mStrings.begin(), pool.mStrings.end(), std::back_inserter(mStrings)); + pool.mStrings.clear(); + std::move(pool.mStyles.begin(), pool.mStyles.end(), std::back_inserter(mStyles)); + pool.mStyles.clear(); + + // Assign the indices. + const size_t len = mStrings.size(); + for (size_t index = 0; index < len; index++) { + mStrings[index]->index = index; + } +} + +void StringPool::hintWillAdd(size_t stringCount, size_t styleCount) { + mStrings.reserve(mStrings.size() + stringCount); + mStyles.reserve(mStyles.size() + styleCount); +} + +void StringPool::prune() { + const auto iterEnd = std::end(mIndexedStrings); + auto indexIter = std::begin(mIndexedStrings); + while (indexIter != iterEnd) { + if (indexIter->second->ref <= 0) { + mIndexedStrings.erase(indexIter++); + } else { + ++indexIter; + } + } + + auto endIter2 = std::remove_if(std::begin(mStrings), std::end(mStrings), + [](const std::unique_ptr<Entry>& entry) -> bool { + return entry->ref <= 0; + } + ); + + auto endIter3 = std::remove_if(std::begin(mStyles), std::end(mStyles), + [](const std::unique_ptr<StyleEntry>& entry) -> bool { + return entry->ref <= 0; + } + ); + + // Remove the entries at the end or else we'll be accessing + // a deleted string from the StyleEntry. + mStrings.erase(endIter2, std::end(mStrings)); + mStyles.erase(endIter3, std::end(mStyles)); +} + +void StringPool::sort(const std::function<bool(const Entry&, const Entry&)>& cmp) { + std::sort(std::begin(mStrings), std::end(mStrings), + [&cmp](const std::unique_ptr<Entry>& a, const std::unique_ptr<Entry>& b) -> bool { + return cmp(*a, *b); + } + ); + + // Assign the indices. + const size_t len = mStrings.size(); + for (size_t index = 0; index < len; index++) { + mStrings[index]->index = index; + } + + // Reorder the styles. + std::sort(std::begin(mStyles), std::end(mStyles), + [](const std::unique_ptr<StyleEntry>& lhs, + const std::unique_ptr<StyleEntry>& rhs) -> bool { + return lhs->str.getIndex() < rhs->str.getIndex(); + } + ); +} + +template <typename T> +static T* encodeLength(T* data, size_t length) { + static_assert(std::is_integral<T>::value, "wat."); + + constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1); + constexpr size_t kMaxSize = kMask - 1; + if (length > kMaxSize) { + *data++ = kMask | (kMaxSize & (length >> (sizeof(T) * 8))); + } + *data++ = length; + return data; +} + +template <typename T> +static size_t encodedLengthUnits(size_t length) { + static_assert(std::is_integral<T>::value, "wat."); + + constexpr size_t kMask = 1 << ((sizeof(T) * 8) - 1); + constexpr size_t kMaxSize = kMask - 1; + return length > kMaxSize ? 2 : 1; +} + + +bool StringPool::flatten(BigBuffer* out, const StringPool& pool, bool utf8) { + const size_t startIndex = out->size(); + android::ResStringPool_header* header = out->nextBlock<android::ResStringPool_header>(); + header->header.type = android::RES_STRING_POOL_TYPE; + header->header.headerSize = sizeof(*header); + header->stringCount = pool.size(); + if (utf8) { + header->flags |= android::ResStringPool_header::UTF8_FLAG; + } + + uint32_t* indices = pool.size() != 0 ? out->nextBlock<uint32_t>(pool.size()) : nullptr; + + uint32_t* styleIndices = nullptr; + if (!pool.mStyles.empty()) { + header->styleCount = pool.mStyles.back()->str.getIndex() + 1; + styleIndices = out->nextBlock<uint32_t>(header->styleCount); + } + + const size_t beforeStringsIndex = out->size(); + header->stringsStart = beforeStringsIndex - startIndex; + + for (const auto& entry : pool) { + *indices = out->size() - beforeStringsIndex; + indices++; + + if (utf8) { + std::string encoded = util::utf16ToUtf8(entry->value); + + const size_t totalSize = encodedLengthUnits<char>(entry->value.size()) + + encodedLengthUnits<char>(encoded.length()) + + encoded.size() + 1; + + char* data = out->nextBlock<char>(totalSize); + + // First encode the actual UTF16 string length. + data = encodeLength(data, entry->value.size()); + + // Now encode the size of the converted UTF8 string. + data = encodeLength(data, encoded.length()); + strncpy(data, encoded.data(), encoded.size()); + } else { + const size_t totalSize = encodedLengthUnits<char16_t>(entry->value.size()) + + entry->value.size() + 1; + + char16_t* data = out->nextBlock<char16_t>(totalSize); + + // Encode the actual UTF16 string length. + data = encodeLength(data, entry->value.size()); + strncpy16(data, entry->value.data(), entry->value.size()); + } + } + + out->align4(); + + if (!pool.mStyles.empty()) { + const size_t beforeStylesIndex = out->size(); + header->stylesStart = beforeStylesIndex - startIndex; + + size_t currentIndex = 0; + for (const auto& entry : pool.mStyles) { + while (entry->str.getIndex() > currentIndex) { + styleIndices[currentIndex++] = out->size() - beforeStylesIndex; + + uint32_t* spanOffset = out->nextBlock<uint32_t>(); + *spanOffset = android::ResStringPool_span::END; + } + styleIndices[currentIndex++] = out->size() - beforeStylesIndex; + + android::ResStringPool_span* span = + out->nextBlock<android::ResStringPool_span>(entry->spans.size()); + for (const auto& s : entry->spans) { + span->name.index = s.name.getIndex(); + span->firstChar = s.firstChar; + span->lastChar = s.lastChar; + span++; + } + + uint32_t* spanEnd = out->nextBlock<uint32_t>(); + *spanEnd = android::ResStringPool_span::END; + } + + // The error checking code in the platform looks for an entire + // ResStringPool_span structure worth of 0xFFFFFFFF at the end + // of the style block, so fill in the remaining 2 32bit words + // with 0xFFFFFFFF. + const size_t paddingLength = sizeof(android::ResStringPool_span) + - sizeof(android::ResStringPool_span::name); + uint8_t* padding = out->nextBlock<uint8_t>(paddingLength); + memset(padding, 0xff, paddingLength); + out->align4(); + } + header->header.size = out->size() - startIndex; + return true; +} + +bool StringPool::flattenUtf8(BigBuffer* out, const StringPool& pool) { + return flatten(out, pool, true); +} + +bool StringPool::flattenUtf16(BigBuffer* out, const StringPool& pool) { + return flatten(out, pool, false); +} + +} // namespace aapt diff --git a/tools/aapt2/StringPool.h b/tools/aapt2/StringPool.h new file mode 100644 index 0000000..14304a6 --- /dev/null +++ b/tools/aapt2/StringPool.h @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_STRING_POOL_H +#define AAPT_STRING_POOL_H + +#include "BigBuffer.h" +#include "ConfigDescription.h" +#include "StringPiece.h" + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { + +struct Span { + std::u16string name; + uint32_t firstChar; + uint32_t lastChar; +}; + +struct StyleString { + std::u16string str; + std::vector<Span> spans; +}; + +class StringPool { +public: + struct Context { + uint32_t priority; + ConfigDescription config; + }; + + class Entry; + + class Ref { + public: + Ref(); + Ref(const Ref&); + ~Ref(); + + Ref& operator=(const Ref& rhs); + const std::u16string* operator->() const; + const std::u16string& operator*() const; + + size_t getIndex() const; + const Context& getContext() const; + + private: + friend class StringPool; + + Ref(Entry* entry); + + Entry* mEntry; + }; + + class StyleEntry; + + class StyleRef { + public: + StyleRef(); + StyleRef(const StyleRef&); + ~StyleRef(); + + StyleRef& operator=(const StyleRef& rhs); + const StyleEntry* operator->() const; + const StyleEntry& operator*() const; + + size_t getIndex() const; + const Context& getContext() const; + + private: + friend class StringPool; + + StyleRef(StyleEntry* entry); + + StyleEntry* mEntry; + }; + + class Entry { + public: + std::u16string value; + Context context; + size_t index; + + private: + friend class StringPool; + friend class Ref; + + int ref; + }; + + struct Span { + Ref name; + uint32_t firstChar; + uint32_t lastChar; + }; + + class StyleEntry { + public: + Ref str; + std::vector<Span> spans; + + private: + friend class StringPool; + friend class StyleRef; + + int ref; + }; + + using const_iterator = std::vector<std::unique_ptr<Entry>>::const_iterator; + + static bool flattenUtf8(BigBuffer* out, const StringPool& pool); + static bool flattenUtf16(BigBuffer* out, const StringPool& pool); + + StringPool() = default; + StringPool(const StringPool&) = delete; + + /** + * Adds a string to the pool, unless it already exists. Returns + * a reference to the string in the pool. + */ + Ref makeRef(const StringPiece16& str); + + /** + * Adds a string to the pool, unless it already exists, with a context + * object that can be used when sorting the string pool. Returns + * a reference to the string in the pool. + */ + Ref makeRef(const StringPiece16& str, const Context& context); + + /** + * Adds a style to the string pool and returns a reference to it. + */ + StyleRef makeRef(const StyleString& str); + + /** + * Adds a style to the string pool with a context object that + * can be used when sorting the string pool. Returns a reference + * to the style in the string pool. + */ + StyleRef makeRef(const StyleString& str, const Context& context); + + /** + * Adds a style from another string pool. Returns a reference to the + * style in the string pool. + */ + StyleRef makeRef(const StyleRef& ref); + + /** + * Moves pool into this one without coalescing strings. When this + * function returns, pool will be empty. + */ + void merge(StringPool&& pool); + + /** + * Retuns the number of strings in the table. + */ + inline size_t size() const; + + /** + * Reserves space for strings and styles as an optimization. + */ + void hintWillAdd(size_t stringCount, size_t styleCount); + + /** + * Sorts the strings according to some comparison function. + */ + void sort(const std::function<bool(const Entry&, const Entry&)>& cmp); + + /** + * Removes any strings that have no references. + */ + void prune(); + +private: + friend const_iterator begin(const StringPool& pool); + friend const_iterator end(const StringPool& pool); + + static bool flatten(BigBuffer* out, const StringPool& pool, bool utf8); + + Ref makeRefImpl(const StringPiece16& str, const Context& context, bool unique); + + std::vector<std::unique_ptr<Entry>> mStrings; + std::vector<std::unique_ptr<StyleEntry>> mStyles; + std::multimap<StringPiece16, Entry*> mIndexedStrings; +}; + +// +// Inline implementation +// + +inline size_t StringPool::size() const { + return mStrings.size(); +} + +inline StringPool::const_iterator begin(const StringPool& pool) { + return pool.mStrings.begin(); +} + +inline StringPool::const_iterator end(const StringPool& pool) { + return pool.mStrings.end(); +} + +} // namespace aapt + +#endif // AAPT_STRING_POOL_H diff --git a/tools/aapt2/StringPool_test.cpp b/tools/aapt2/StringPool_test.cpp new file mode 100644 index 0000000..9552937 --- /dev/null +++ b/tools/aapt2/StringPool_test.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "StringPool.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <string> + +using namespace android; + +namespace aapt { + +TEST(StringPoolTest, InsertOneString) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + EXPECT_EQ(*ref, u"wut"); +} + +TEST(StringPoolTest, InsertTwoUniqueStrings) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + StringPool::Ref ref2 = pool.makeRef(u"hey"); + + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(*ref2, u"hey"); +} + +TEST(StringPoolTest, DoNotInsertNewDuplicateString) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + StringPool::Ref ref2 = pool.makeRef(u"wut"); + + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(*ref2, u"wut"); + EXPECT_EQ(1u, pool.size()); +} + +TEST(StringPoolTest, MaintainInsertionOrderIndex) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::Ref ref2 = pool.makeRef(u"a"); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + EXPECT_EQ(0u, ref.getIndex()); + EXPECT_EQ(1u, ref2.getIndex()); + EXPECT_EQ(2u, ref3.getIndex()); +} + +TEST(StringPoolTest, PruneStringsWithNoReferences) { + StringPool pool; + + { + StringPool::Ref ref = pool.makeRef(u"wut"); + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(1u, pool.size()); + } + + EXPECT_EQ(1u, pool.size()); + pool.prune(); + EXPECT_EQ(0u, pool.size()); +} + +TEST(StringPoolTest, SortAndMaintainIndexesInReferences) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::StyleRef ref2 = pool.makeRef(StyleString{ {u"a"} }); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + EXPECT_EQ(*ref, u"z"); + EXPECT_EQ(0u, ref.getIndex()); + + EXPECT_EQ(*(ref2->str), u"a"); + EXPECT_EQ(1u, ref2.getIndex()); + + EXPECT_EQ(*ref3, u"m"); + EXPECT_EQ(2u, ref3.getIndex()); + + pool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.value < b.value; + }); + + + EXPECT_EQ(*ref, u"z"); + EXPECT_EQ(2u, ref.getIndex()); + + EXPECT_EQ(*(ref2->str), u"a"); + EXPECT_EQ(0u, ref2.getIndex()); + + EXPECT_EQ(*ref3, u"m"); + EXPECT_EQ(1u, ref3.getIndex()); +} + +TEST(StringPoolTest, SortAndStillDedupe) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::Ref ref2 = pool.makeRef(u"a"); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + pool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.value < b.value; + }); + + StringPool::Ref ref4 = pool.makeRef(u"z"); + StringPool::Ref ref5 = pool.makeRef(u"a"); + StringPool::Ref ref6 = pool.makeRef(u"m"); + + EXPECT_EQ(ref4.getIndex(), ref.getIndex()); + EXPECT_EQ(ref5.getIndex(), ref2.getIndex()); + EXPECT_EQ(ref6.getIndex(), ref3.getIndex()); +} + +TEST(StringPoolTest, AddStyles) { + StringPool pool; + + StyleString str { + { u"android" }, + { + Span{ { u"b" }, 2, 6 } + } + }; + + StringPool::StyleRef ref = pool.makeRef(str); + + EXPECT_EQ(0u, ref.getIndex()); + EXPECT_EQ(std::u16string(u"android"), *(ref->str)); + ASSERT_EQ(1u, ref->spans.size()); + + const StringPool::Span& span = ref->spans.front(); + EXPECT_EQ(*(span.name), u"b"); + EXPECT_EQ(2u, span.firstChar); + EXPECT_EQ(6u, span.lastChar); +} + +TEST(StringPoolTest, DoNotDedupeStyleWithSameStringAsNonStyle) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"android"); + + StyleString str { { u"android" } }; + StringPool::StyleRef styleRef = pool.makeRef(str); + + EXPECT_NE(ref.getIndex(), styleRef.getIndex()); +} + +TEST(StringPoolTest, FlattenEmptyStringPoolUtf8) { + StringPool pool; + BigBuffer buffer(1024); + StringPool::flattenUtf8(&buffer, pool); + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + android::ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), android::NO_ERROR); +} + +constexpr const char16_t* sLongString = u"バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限します。メール、SMSや、同期を使 用するその他のアプリは、起動しても更新されないことがあります。バッテリーセーバーは端末の充電中は自動的にOFFになります。"; + +TEST(StringPoolTest, FlattenUtf8) { + StringPool pool; + + StringPool::Ref ref1 = pool.makeRef(u"hello"); + StringPool::Ref ref2 = pool.makeRef(u"goodbye"); + StringPool::Ref ref3 = pool.makeRef(sLongString); + StringPool::StyleRef ref4 = pool.makeRef(StyleString{ + { u"style" }, + { Span{ { u"b" }, 0, 1 }, Span{ { u"i" }, 2, 3 } } + }); + + EXPECT_EQ(0u, ref1.getIndex()); + EXPECT_EQ(1u, ref2.getIndex()); + EXPECT_EQ(2u, ref3.getIndex()); + EXPECT_EQ(3u, ref4.getIndex()); + + BigBuffer buffer(1024); + StringPool::flattenUtf8(&buffer, pool); + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + { + android::ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), android::NO_ERROR); + + EXPECT_EQ(util::getString(test, 0), u"hello"); + EXPECT_EQ(util::getString(test, 1), u"goodbye"); + EXPECT_EQ(util::getString(test, 2), sLongString); + EXPECT_EQ(util::getString(test, 3), u"style"); + + const ResStringPool_span* span = test.styleAt(3); + ASSERT_NE(nullptr, span); + EXPECT_EQ(util::getString(test, span->name.index), u"b"); + EXPECT_EQ(0u, span->firstChar); + EXPECT_EQ(1u, span->lastChar); + span++; + + ASSERT_NE(ResStringPool_span::END, span->name.index); + EXPECT_EQ(util::getString(test, span->name.index), u"i"); + EXPECT_EQ(2u, span->firstChar); + EXPECT_EQ(3u, span->lastChar); + span++; + + EXPECT_EQ(ResStringPool_span::END, span->name.index); + } +} + +} // namespace aapt diff --git a/tools/aapt2/TableFlattener.cpp b/tools/aapt2/TableFlattener.cpp new file mode 100644 index 0000000..539c48f --- /dev/null +++ b/tools/aapt2/TableFlattener.cpp @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" +#include "ConfigDescription.h" +#include "Logger.h" +#include "ResourceTable.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "StringPool.h" +#include "TableFlattener.h" +#include "Util.h" + +#include <algorithm> +#include <androidfw/ResourceTypes.h> +#include <sstream> + +namespace aapt { + +struct FlatEntry { + const ResourceEntry* entry; + const Value* value; + uint32_t entryKey; + uint32_t sourcePathKey; + uint32_t sourceLine; +}; + +/** + * Visitor that knows how to encode Map values. + */ +class MapFlattener : public ConstValueVisitor { +public: + MapFlattener(BigBuffer* out, const FlatEntry& flatEntry, SymbolEntryVector* symbols) : + mOut(out), mSymbols(symbols) { + mMap = mOut->nextBlock<android::ResTable_map_entry>(); + mMap->key.index = flatEntry.entryKey; + mMap->flags = android::ResTable_entry::FLAG_COMPLEX; + if (flatEntry.entry->publicStatus.isPublic) { + mMap->flags |= android::ResTable_entry::FLAG_PUBLIC; + } + if (flatEntry.value->isWeak()) { + mMap->flags |= android::ResTable_entry::FLAG_WEAK; + } + + ResTable_entry_source* sourceBlock = mOut->nextBlock<ResTable_entry_source>(); + sourceBlock->pathIndex = flatEntry.sourcePathKey; + sourceBlock->line = flatEntry.sourceLine; + + mMap->size = sizeof(*mMap) + sizeof(*sourceBlock); + } + + void flattenParent(const Reference& ref) { + if (!ref.id.isValid()) { + mSymbols->push_back({ + ResourceNameRef(ref.name), + (mOut->size() - mMap->size) + sizeof(*mMap) - sizeof(android::ResTable_entry) + }); + } + mMap->parent.ident = ref.id.id; + } + + void flattenEntry(const Reference& key, const Item& value) { + mMap->count++; + + android::ResTable_map* outMapEntry = mOut->nextBlock<android::ResTable_map>(); + + // Write the key. + if (!Res_INTERNALID(key.id.id) && !key.id.isValid()) { + assert(key.name.isValid()); + mSymbols->push_back(std::make_pair(ResourceNameRef(key.name), + mOut->size() - sizeof(*outMapEntry))); + } + outMapEntry->name.ident = key.id.id; + + // Write the value. + value.flatten(outMapEntry->value); + + if (outMapEntry->value.data == 0x0) { + visitFunc<Reference>(value, [&](const Reference& reference) { + mSymbols->push_back(std::make_pair(ResourceNameRef(reference.name), + mOut->size() - sizeof(outMapEntry->value.data))); + }); + } + outMapEntry->value.size = sizeof(outMapEntry->value); + } + + void flattenValueOnly(const Item& value) { + mMap->count++; + + android::ResTable_map* outMapEntry = mOut->nextBlock<android::ResTable_map>(); + + // Write the value. + value.flatten(outMapEntry->value); + + if (outMapEntry->value.data == 0x0) { + visitFunc<Reference>(value, [&](const Reference& reference) { + mSymbols->push_back(std::make_pair(ResourceNameRef(reference.name), + mOut->size() - sizeof(outMapEntry->value.data))); + }); + } + outMapEntry->value.size = sizeof(outMapEntry->value); + } + + static bool compareStyleEntries(const Style::Entry* lhs, const Style::Entry* rhs) { + return lhs->key.id < rhs->key.id; + } + + void visit(const Style& style, ValueVisitorArgs&) override { + if (style.parent.name.isValid()) { + flattenParent(style.parent); + } + + // First sort the entries by ID. + std::vector<const Style::Entry*> sortedEntries; + for (const auto& styleEntry : style.entries) { + auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), + &styleEntry, compareStyleEntries); + sortedEntries.insert(iter, &styleEntry); + } + + for (const Style::Entry* styleEntry : sortedEntries) { + flattenEntry(styleEntry->key, *styleEntry->value); + } + } + + void visit(const Attribute& attr, ValueVisitorArgs&) override { + android::Res_value tempVal; + tempVal.dataType = android::Res_value::TYPE_INT_DEC; + tempVal.data = attr.typeMask; + flattenEntry(Reference(ResourceId{android::ResTable_map::ATTR_TYPE}), + BinaryPrimitive(tempVal)); + + for (const auto& symbol : attr.symbols) { + tempVal.data = symbol.value; + flattenEntry(symbol.symbol, BinaryPrimitive(tempVal)); + } + } + + void visit(const Styleable& styleable, ValueVisitorArgs&) override { + for (const auto& attr : styleable.entries) { + flattenEntry(attr, BinaryPrimitive(android::Res_value{})); + } + } + + void visit(const Array& array, ValueVisitorArgs&) override { + for (const auto& item : array.items) { + flattenValueOnly(*item); + } + } + + void visit(const Plural& plural, ValueVisitorArgs&) override { + const size_t count = plural.values.size(); + for (size_t i = 0; i < count; i++) { + if (!plural.values[i]) { + continue; + } + + ResourceId q; + switch (i) { + case Plural::Zero: + q.id = android::ResTable_map::ATTR_ZERO; + break; + + case Plural::One: + q.id = android::ResTable_map::ATTR_ONE; + break; + + case Plural::Two: + q.id = android::ResTable_map::ATTR_TWO; + break; + + case Plural::Few: + q.id = android::ResTable_map::ATTR_FEW; + break; + + case Plural::Many: + q.id = android::ResTable_map::ATTR_MANY; + break; + + case Plural::Other: + q.id = android::ResTable_map::ATTR_OTHER; + break; + + default: + assert(false); + break; + } + + flattenEntry(Reference(q), *plural.values[i]); + } + } + +private: + BigBuffer* mOut; + SymbolEntryVector* mSymbols; + android::ResTable_map_entry* mMap; +}; + +/** + * Flattens a value, with special handling for References. + */ +struct ValueFlattener : ConstValueVisitor { + ValueFlattener(BigBuffer* out, SymbolEntryVector* symbols) : + result(false), mOut(out), mOutValue(nullptr), mSymbols(symbols) { + mOutValue = mOut->nextBlock<android::Res_value>(); + } + + virtual void visit(const Reference& ref, ValueVisitorArgs& a) override { + visitItem(ref, a); + if (mOutValue->data == 0x0) { + mSymbols->push_back({ + ResourceNameRef(ref.name), + mOut->size() - sizeof(mOutValue->data)}); + } + } + + virtual void visitItem(const Item& item, ValueVisitorArgs&) override { + result = item.flatten(*mOutValue); + mOutValue->res0 = 0; + mOutValue->size = sizeof(*mOutValue); + } + + bool result; + +private: + BigBuffer* mOut; + android::Res_value* mOutValue; + SymbolEntryVector* mSymbols; +}; + +TableFlattener::TableFlattener(Options options) +: mOptions(options) { +} + +bool TableFlattener::flattenValue(BigBuffer* out, const FlatEntry& flatEntry, + SymbolEntryVector* symbols) { + if (flatEntry.value->isItem()) { + android::ResTable_entry* entry = out->nextBlock<android::ResTable_entry>(); + + if (flatEntry.entry->publicStatus.isPublic) { + entry->flags |= android::ResTable_entry::FLAG_PUBLIC; + } + + if (flatEntry.value->isWeak()) { + entry->flags |= android::ResTable_entry::FLAG_WEAK; + } + + entry->key.index = flatEntry.entryKey; + entry->size = sizeof(*entry); + + if (mOptions.useExtendedChunks) { + // Write the extra source block. This will be ignored by + // the Android runtime. + ResTable_entry_source* sourceBlock = out->nextBlock<ResTable_entry_source>(); + sourceBlock->pathIndex = flatEntry.sourcePathKey; + sourceBlock->line = flatEntry.sourceLine; + entry->size += sizeof(*sourceBlock); + } + + const Item* item = static_cast<const Item*>(flatEntry.value); + ValueFlattener flattener(out, symbols); + item->accept(flattener, {}); + return flattener.result; + } + + MapFlattener flattener(out, flatEntry, symbols); + flatEntry.value->accept(flattener, {}); + return true; +} + +bool TableFlattener::flatten(BigBuffer* out, const ResourceTable& table) { + const size_t beginning = out->size(); + + if (table.getPackage().size() == 0) { + Logger::error() + << "ResourceTable has no package name." + << std::endl; + return false; + } + + if (table.getPackageId() == ResourceTable::kUnsetPackageId) { + Logger::error() + << "ResourceTable has no package ID set." + << std::endl; + return false; + } + + SymbolEntryVector symbolEntries; + + StringPool typePool; + StringPool keyPool; + StringPool sourcePool; + + // Sort the types by their IDs. They will be inserted into the StringPool + // in this order. + std::vector<ResourceTableType*> sortedTypes; + for (const auto& type : table) { + if (type->type == ResourceType::kStyleable && !mOptions.useExtendedChunks) { + continue; + } + + auto iter = std::lower_bound(std::begin(sortedTypes), std::end(sortedTypes), type.get(), + [](const ResourceTableType* lhs, const ResourceTableType* rhs) -> bool { + return lhs->typeId < rhs->typeId; + }); + sortedTypes.insert(iter, type.get()); + } + + BigBuffer typeBlock(1024); + size_t expectedTypeId = 1; + for (const ResourceTableType* type : sortedTypes) { + if (type->typeId == ResourceTableType::kUnsetTypeId + || type->typeId == 0) { + Logger::error() + << "resource type '" + << type->type + << "' from package '" + << table.getPackage() + << "' has no ID." + << std::endl; + return false; + } + + // If there is a gap in the type IDs, fill in the StringPool + // with empty values until we reach the ID we expect. + while (type->typeId > expectedTypeId) { + std::u16string typeName(u"?"); + typeName += expectedTypeId; + typePool.makeRef(typeName); + expectedTypeId++; + } + expectedTypeId++; + typePool.makeRef(toString(type->type)); + + android::ResTable_typeSpec* spec = typeBlock.nextBlock<android::ResTable_typeSpec>(); + spec->header.type = android::RES_TABLE_TYPE_SPEC_TYPE; + spec->header.headerSize = sizeof(*spec); + spec->header.size = spec->header.headerSize + (type->entries.size() * sizeof(uint32_t)); + spec->id = type->typeId; + spec->entryCount = type->entries.size(); + + if (type->entries.empty()) { + continue; + } + + // Reserve space for the masks of each resource in this type. These + // show for which configuration axis the resource changes. + uint32_t* configMasks = typeBlock.nextBlock<uint32_t>(type->entries.size()); + + // Sort the entries by entry ID and write their configuration masks. + std::vector<ResourceEntry*> entries; + const size_t entryCount = type->entries.size(); + for (size_t entryIndex = 0; entryIndex < entryCount; entryIndex++) { + const auto& entry = type->entries[entryIndex]; + + if (entry->entryId == ResourceEntry::kUnsetEntryId) { + Logger::error() + << "resource '" + << ResourceName{ table.getPackage(), type->type, entry->name } + << "' has no ID." + << std::endl; + return false; + } + + auto iter = std::lower_bound(std::begin(entries), std::end(entries), entry.get(), + [](const ResourceEntry* lhs, const ResourceEntry* rhs) -> bool { + return lhs->entryId < rhs->entryId; + }); + entries.insert(iter, entry.get()); + + // Populate the config masks for this entry. + if (entry->publicStatus.isPublic) { + configMasks[entry->entryId] |= android::ResTable_typeSpec::SPEC_PUBLIC; + } + + const size_t configCount = entry->values.size(); + for (size_t i = 0; i < configCount; i++) { + const ConfigDescription& config = entry->values[i].config; + for (size_t j = i + 1; j < configCount; j++) { + configMasks[entry->entryId] |= config.diff(entry->values[j].config); + } + } + } + + const size_t beforePublicHeader = typeBlock.size(); + Public_header* publicHeader = nullptr; + if (mOptions.useExtendedChunks) { + publicHeader = typeBlock.nextBlock<Public_header>(); + publicHeader->header.type = RES_TABLE_PUBLIC_TYPE; + publicHeader->header.headerSize = sizeof(*publicHeader); + publicHeader->typeId = type->typeId; + } + + // The binary resource table lists resource entries for each configuration. + // We store them inverted, where a resource entry lists the values for each + // configuration available. Here we reverse this to match the binary table. + std::map<ConfigDescription, std::vector<FlatEntry>> data; + for (const ResourceEntry* entry : entries) { + size_t keyIndex = keyPool.makeRef(entry->name).getIndex(); + + if (keyIndex > std::numeric_limits<uint32_t>::max()) { + Logger::error() + << "resource key string pool exceeded max size." + << std::endl; + return false; + } + + if (publicHeader && entry->publicStatus.isPublic) { + // Write the public status of this entry. + Public_entry* publicEntry = typeBlock.nextBlock<Public_entry>(); + publicEntry->entryId = static_cast<uint32_t>(entry->entryId); + publicEntry->key.index = static_cast<uint32_t>(keyIndex); + publicEntry->source.index = static_cast<uint32_t>(sourcePool.makeRef( + util::utf8ToUtf16(entry->publicStatus.source.path)).getIndex()); + publicEntry->sourceLine = static_cast<uint32_t>(entry->publicStatus.source.line); + publicHeader->count += 1; + } + + for (const auto& configValue : entry->values) { + data[configValue.config].push_back(FlatEntry{ + entry, + configValue.value.get(), + static_cast<uint32_t>(keyIndex), + static_cast<uint32_t>(sourcePool.makeRef(util::utf8ToUtf16( + configValue.source.path)).getIndex()), + static_cast<uint32_t>(configValue.source.line) + }); + } + } + + if (publicHeader) { + typeBlock.align4(); + publicHeader->header.size = + static_cast<uint32_t>(typeBlock.size() - beforePublicHeader); + } + + // Begin flattening a configuration for the current type. + for (const auto& entry : data) { + const size_t typeHeaderStart = typeBlock.size(); + android::ResTable_type* typeHeader = typeBlock.nextBlock<android::ResTable_type>(); + typeHeader->header.type = android::RES_TABLE_TYPE_TYPE; + typeHeader->header.headerSize = sizeof(*typeHeader); + typeHeader->id = type->typeId; + typeHeader->entryCount = type->entries.size(); + typeHeader->entriesStart = typeHeader->header.headerSize + + (sizeof(uint32_t) * type->entries.size()); + typeHeader->config = entry.first; + + uint32_t* indices = typeBlock.nextBlock<uint32_t>(type->entries.size()); + memset(indices, 0xff, type->entries.size() * sizeof(uint32_t)); + + const size_t entryStart = typeBlock.size(); + for (const FlatEntry& flatEntry : entry.second) { + assert(flatEntry.entry->entryId < type->entries.size()); + indices[flatEntry.entry->entryId] = typeBlock.size() - entryStart; + if (!flattenValue(&typeBlock, flatEntry, &symbolEntries)) { + Logger::error() + << "failed to flatten resource '" + << ResourceNameRef { + table.getPackage(), type->type, flatEntry.entry->name } + << "' for configuration '" + << entry.first + << "'." + << std::endl; + return false; + } + } + + typeBlock.align4(); + typeHeader->header.size = typeBlock.size() - typeHeaderStart; + } + } + + const size_t beforeTable = out->size(); + android::ResTable_header* header = out->nextBlock<android::ResTable_header>(); + header->header.type = android::RES_TABLE_TYPE; + header->header.headerSize = sizeof(*header); + header->packageCount = 1; + + SymbolTable_entry* symbolEntryData = nullptr; + if (!symbolEntries.empty() && mOptions.useExtendedChunks) { + const size_t beforeSymbolTable = out->size(); + StringPool symbolPool; + SymbolTable_header* symbolHeader = out->nextBlock<SymbolTable_header>(); + symbolHeader->header.type = RES_TABLE_SYMBOL_TABLE_TYPE; + symbolHeader->header.headerSize = sizeof(*symbolHeader); + symbolHeader->count = symbolEntries.size(); + + symbolEntryData = out->nextBlock<SymbolTable_entry>(symbolHeader->count); + + size_t i = 0; + for (const auto& entry : symbolEntries) { + symbolEntryData[i].offset = entry.second; + StringPool::Ref ref = symbolPool.makeRef( + entry.first.package.toString() + u":" + + toString(entry.first.type).toString() + u"/" + + entry.first.entry.toString()); + symbolEntryData[i].stringIndex = ref.getIndex(); + i++; + } + + StringPool::flattenUtf8(out, symbolPool); + out->align4(); + symbolHeader->header.size = out->size() - beforeSymbolTable; + } + + if (sourcePool.size() > 0 && mOptions.useExtendedChunks) { + const size_t beforeSourcePool = out->size(); + android::ResChunk_header* sourceHeader = out->nextBlock<android::ResChunk_header>(); + sourceHeader->type = RES_TABLE_SOURCE_POOL_TYPE; + sourceHeader->headerSize = sizeof(*sourceHeader); + StringPool::flattenUtf8(out, sourcePool); + out->align4(); + sourceHeader->size = out->size() - beforeSourcePool; + } + + StringPool::flattenUtf8(out, table.getValueStringPool()); + + const size_t beforePackageIndex = out->size(); + android::ResTable_package* package = out->nextBlock<android::ResTable_package>(); + package->header.type = android::RES_TABLE_PACKAGE_TYPE; + package->header.headerSize = sizeof(*package); + + if (table.getPackageId() > std::numeric_limits<uint8_t>::max()) { + Logger::error() + << "package ID 0x'" + << std::hex << table.getPackageId() << std::dec + << "' is invalid." + << std::endl; + return false; + } + package->id = table.getPackageId(); + + if (table.getPackage().size() >= sizeof(package->name) / sizeof(package->name[0])) { + Logger::error() + << "package name '" + << table.getPackage() + << "' is too long." + << std::endl; + return false; + } + memcpy(package->name, reinterpret_cast<const uint16_t*>(table.getPackage().data()), + table.getPackage().length() * sizeof(char16_t)); + package->name[table.getPackage().length()] = 0; + + package->typeStrings = package->header.headerSize; + StringPool::flattenUtf16(out, typePool); + package->keyStrings = out->size() - beforePackageIndex; + StringPool::flattenUtf16(out, keyPool); + + if (symbolEntryData != nullptr) { + for (size_t i = 0; i < symbolEntries.size(); i++) { + symbolEntryData[i].offset += out->size() - beginning; + } + } + + out->appendBuffer(std::move(typeBlock)); + + package->header.size = out->size() - beforePackageIndex; + header->header.size = out->size() - beforeTable; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/TableFlattener.h b/tools/aapt2/TableFlattener.h new file mode 100644 index 0000000..ccbb737 --- /dev/null +++ b/tools/aapt2/TableFlattener.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_TABLE_FLATTENER_H +#define AAPT_TABLE_FLATTENER_H + +#include "BigBuffer.h" +#include "ResourceTable.h" + +namespace aapt { + +using SymbolEntryVector = std::vector<std::pair<ResourceNameRef, uint32_t>>; + +struct FlatEntry; + +/** + * Flattens a ResourceTable into a binary format suitable + * for loading into a ResTable on the host or device. + */ +struct TableFlattener { + /** + * A set of options for this TableFlattener. + */ + struct Options { + /** + * Specifies whether to output extended chunks, like + * source information and mising symbol entries. Default + * is true. + * + * Set this to false when emitting the final table to be used + * on device. + */ + bool useExtendedChunks = true; + }; + + TableFlattener(Options options); + + bool flatten(BigBuffer* out, const ResourceTable& table); + +private: + bool flattenValue(BigBuffer* out, const FlatEntry& flatEntry, SymbolEntryVector* symbols); + + Options mOptions; +}; + +} // namespace aapt + +#endif // AAPT_TABLE_FLATTENER_H diff --git a/tools/aapt2/Util.cpp b/tools/aapt2/Util.cpp new file mode 100644 index 0000000..03ecd1a --- /dev/null +++ b/tools/aapt2/Util.cpp @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" +#include "Maybe.h" +#include "StringPiece.h" +#include "Util.h" + +#include <algorithm> +#include <ostream> +#include <string> +#include <utils/Unicode.h> +#include <vector> + +namespace aapt { +namespace util { + +constexpr const char16_t* kSchemaAuto = u"http://schemas.android.com/apk/res-auto"; +constexpr const char16_t* kSchemaPrefix = u"http://schemas.android.com/apk/res/"; + +static std::vector<std::string> splitAndTransform(const StringPiece& str, char sep, + const std::function<char(char)>& f) { + std::vector<std::string> parts; + const StringPiece::const_iterator end = std::end(str); + StringPiece::const_iterator start = std::begin(str); + StringPiece::const_iterator current; + do { + current = std::find(start, end, sep); + parts.emplace_back(str.substr(start, current).toString()); + if (f) { + std::string& part = parts.back(); + std::transform(part.begin(), part.end(), part.begin(), f); + } + start = current + 1; + } while (current != end); + return parts; +} + +std::vector<std::string> split(const StringPiece& str, char sep) { + return splitAndTransform(str, sep, nullptr); +} + +std::vector<std::string> splitAndLowercase(const StringPiece& str, char sep) { + return splitAndTransform(str, sep, ::tolower); +} + +StringPiece16 trimWhitespace(const StringPiece16& str) { + if (str.size() == 0 || str.data() == nullptr) { + return str; + } + + const char16_t* start = str.data(); + const char16_t* end = str.data() + str.length(); + + while (start != end && util::isspace16(*start)) { + start++; + } + + while (end != start && util::isspace16(*(end - 1))) { + end--; + } + + return StringPiece16(start, end - start); +} + +StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16& str, + const StringPiece16& allowedChars) { + const auto endIter = str.end(); + for (auto iter = str.begin(); iter != endIter; ++iter) { + char16_t c = *iter; + if ((c >= u'a' && c <= u'z') || + (c >= u'A' && c <= u'Z') || + (c >= u'0' && c <= u'9')) { + continue; + } + + bool match = false; + for (char16_t i : allowedChars) { + if (c == i) { + match = true; + break; + } + } + + if (!match) { + return iter; + } + } + return endIter; +} + +bool isJavaClassName(const StringPiece16& str) { + size_t pieces = 0; + for (const StringPiece16& piece : tokenize(str, u'.')) { + pieces++; + if (piece.empty()) { + return false; + } + + // Can't have starting or trailing $ character. + if (piece.data()[0] == u'$' || piece.data()[piece.size() - 1] == u'$') { + return false; + } + + if (findNonAlphaNumericAndNotInSet(piece, u"$_") != piece.end()) { + return false; + } + } + return pieces >= 2; +} + +Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, + const StringPiece16& className) { + if (className.empty()) { + return {}; + } + + if (util::isJavaClassName(className)) { + return className.toString(); + } + + if (package.empty()) { + return {}; + } + + std::u16string result(package.data(), package.size()); + if (className.data()[0] != u'.') { + result += u'.'; + } + result.append(className.data(), className.size()); + if (!isJavaClassName(result)) { + return {}; + } + return result; +} + +static Maybe<char16_t> parseUnicodeCodepoint(const char16_t** start, const char16_t* end) { + char16_t code = 0; + for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) { + char16_t c = **start; + int a; + if (c >= '0' && c <= '9') { + a = c - '0'; + } else if (c >= 'a' && c <= 'f') { + a = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + a = c - 'A' + 10; + } else { + return make_nothing<char16_t>(); + } + code = (code << 4) | a; + } + return make_value(code); +} + +StringBuilder& StringBuilder::append(const StringPiece16& str) { + if (!mError.empty()) { + return *this; + } + + const char16_t* const end = str.end(); + const char16_t* start = str.begin(); + const char16_t* current = start; + while (current != end) { + if (*current == u'"') { + if (!mQuote && mTrailingSpace) { + // We found an opening quote, and we have + // trailing space, so we should append that + // space now. + if (mTrailingSpace) { + // We had trailing whitespace, so + // replace with a single space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + } + mQuote = !mQuote; + mStr.append(start, current - start); + start = current + 1; + } else if (*current == u'\'' && !mQuote) { + // This should be escaped. + mError = "unescaped apostrophe"; + return *this; + } else if (*current == u'\\') { + // This is an escape sequence, convert to the real value. + if (!mQuote && mTrailingSpace) { + // We had trailing whitespace, so + // replace with a single space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + mStr.append(start, current - start); + start = current + 1; + + current++; + if (current != end) { + switch (*current) { + case u't': + mStr += u'\t'; + break; + case u'n': + mStr += u'\n'; + break; + case u'#': + mStr += u'#'; + break; + case u'@': + mStr += u'@'; + break; + case u'?': + mStr += u'?'; + break; + case u'"': + mStr += u'"'; + break; + case u'\'': + mStr += u'\''; + break; + case u'\\': + mStr += u'\\'; + break; + case u'u': { + current++; + Maybe<char16_t> c = parseUnicodeCodepoint(¤t, end); + if (!c) { + mError = "invalid unicode escape sequence"; + return *this; + } + mStr += c.value(); + current -= 1; + break; + } + + default: + // Ignore. + break; + } + start = current + 1; + } + } else if (!mQuote) { + // This is not quoted text, so look for whitespace. + if (isspace16(*current)) { + // We found whitespace, see if we have seen some + // before. + if (!mTrailingSpace) { + // We didn't see a previous adjacent space, + // so mark that we did. + mTrailingSpace = true; + mStr.append(start, current - start); + } + + // Keep skipping whitespace. + start = current + 1; + } else if (mTrailingSpace) { + // We saw trailing space before, so replace all + // that trailing space with one space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + } + current++; + } + mStr.append(start, end - start); + return *this; +} + +std::u16string utf8ToUtf16(const StringPiece& utf8) { + ssize_t utf16Length = utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(utf8.data()), + utf8.length()); + if (utf16Length <= 0) { + return {}; + } + + std::u16string utf16; + utf16.resize(utf16Length); + utf8_to_utf16(reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length(), &*utf16.begin()); + return utf16; +} + +std::string utf16ToUtf8(const StringPiece16& utf16) { + ssize_t utf8Length = utf16_to_utf8_length(utf16.data(), utf16.length()); + if (utf8Length <= 0) { + return {}; + } + + std::string utf8; + utf8.resize(utf8Length); + utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin()); + return utf8; +} + +bool writeAll(std::ostream& out, const BigBuffer& buffer) { + for (const auto& b : buffer) { + if (!out.write(reinterpret_cast<const char*>(b.buffer.get()), b.size)) { + return false; + } + } + return true; +} + +std::unique_ptr<uint8_t[]> copy(const BigBuffer& buffer) { + std::unique_ptr<uint8_t[]> data = std::unique_ptr<uint8_t[]>(new uint8_t[buffer.size()]); + uint8_t* p = data.get(); + for (const auto& block : buffer) { + memcpy(p, block.buffer.get(), block.size); + p += block.size; + } + return data; +} + +Maybe<std::u16string> extractPackageFromNamespace(const std::u16string& namespaceUri) { + if (stringStartsWith<char16_t>(namespaceUri, kSchemaPrefix)) { + StringPiece16 schemaPrefix = kSchemaPrefix; + StringPiece16 package = namespaceUri; + return package.substr(schemaPrefix.size(), package.size() - schemaPrefix.size()) + .toString(); + } else if (namespaceUri == kSchemaAuto) { + return std::u16string(); + } + return {}; +} + +} // namespace util +} // namespace aapt diff --git a/tools/aapt2/Util.h b/tools/aapt2/Util.h new file mode 100644 index 0000000..9cdb152 --- /dev/null +++ b/tools/aapt2/Util.h @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_UTIL_H +#define AAPT_UTIL_H + +#include "BigBuffer.h" +#include "Maybe.h" +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> +#include <functional> +#include <memory> +#include <ostream> +#include <string> +#include <vector> + +namespace aapt { +namespace util { + +std::vector<std::string> split(const StringPiece& str, char sep); +std::vector<std::string> splitAndLowercase(const StringPiece& str, char sep); + +/** + * Returns true if the string starts with prefix. + */ +template <typename T> +bool stringStartsWith(const BasicStringPiece<T>& str, const BasicStringPiece<T>& prefix) { + if (str.size() < prefix.size()) { + return false; + } + return str.substr(0, prefix.size()) == prefix; +} + +/** + * Returns true if the string ends with suffix. + */ +template <typename T> +bool stringEndsWith(const BasicStringPiece<T>& str, const BasicStringPiece<T>& suffix) { + if (str.size() < suffix.size()) { + return false; + } + return str.substr(str.size() - suffix.size(), suffix.size()) == suffix; +} + +/** + * Creates a new StringPiece16 that points to a substring + * of the original string without leading or trailing whitespace. + */ +StringPiece16 trimWhitespace(const StringPiece16& str); + +/** + * UTF-16 isspace(). It basically checks for lower range characters that are + * whitespace. + */ +inline bool isspace16(char16_t c) { + return c < 0x0080 && isspace(c); +} + +/** + * Returns an iterator to the first character that is not alpha-numeric and that + * is not in the allowedChars set. + */ +StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16& str, + const StringPiece16& allowedChars); + +/** + * Tests that the string is a valid Java class name. + */ +bool isJavaClassName(const StringPiece16& str); + +/** + * Converts the class name to a fully qualified class name from the given `package`. Ex: + * + * asdf --> package.asdf + * .asdf --> package.asdf + * .a.b --> package.a.b + * asdf.adsf --> asdf.adsf + */ +Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, + const StringPiece16& className); + + +/** + * Makes a std::unique_ptr<> with the template parameter inferred by the compiler. + * This will be present in C++14 and can be removed then. + */ +template <typename T, class... Args> +std::unique_ptr<T> make_unique(Args&&... args) { + return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); +} + +/** + * Writes a set of items to the std::ostream, joining the times with the provided + * separator. + */ +template <typename Iterator> +::std::function<::std::ostream&(::std::ostream&)> joiner(Iterator begin, Iterator end, + const char* sep) { + return [begin, end, sep](::std::ostream& out) -> ::std::ostream& { + for (auto iter = begin; iter != end; ++iter) { + if (iter != begin) { + out << sep; + } + out << *iter; + } + return out; + }; +} + +inline ::std::function<::std::ostream&(::std::ostream&)> formatSize(size_t size) { + return [size](::std::ostream& out) -> ::std::ostream& { + constexpr size_t K = 1024u; + constexpr size_t M = K * K; + constexpr size_t G = M * K; + if (size < K) { + out << size << "B"; + } else if (size < M) { + out << (double(size) / K) << " KiB"; + } else if (size < G) { + out << (double(size) / M) << " MiB"; + } else { + out << (double(size) / G) << " GiB"; + } + return out; + }; +} + +/** + * Helper method to extract a string from a StringPool. + */ +inline StringPiece16 getString(const android::ResStringPool& pool, size_t idx) { + size_t len; + const char16_t* str = pool.stringAt(idx, &len); + if (str != nullptr) { + return StringPiece16(str, len); + } + return StringPiece16(); +} + +class StringBuilder { +public: + StringBuilder& append(const StringPiece16& str); + const std::u16string& str() const; + const std::string& error() const; + operator bool() const; + +private: + std::u16string mStr; + bool mQuote = false; + bool mTrailingSpace = false; + std::string mError; +}; + +inline const std::u16string& StringBuilder::str() const { + return mStr; +} + +inline const std::string& StringBuilder::error() const { + return mError; +} + +inline StringBuilder::operator bool() const { + return mError.empty(); +} + +/** + * Converts a UTF8 string to a UTF16 string. + */ +std::u16string utf8ToUtf16(const StringPiece& utf8); +std::string utf16ToUtf8(const StringPiece16& utf8); + +/** + * Writes the entire BigBuffer to the output stream. + */ +bool writeAll(std::ostream& out, const BigBuffer& buffer); + +/* + * Copies the entire BigBuffer into a single buffer. + */ +std::unique_ptr<uint8_t[]> copy(const BigBuffer& buffer); + +/** + * A Tokenizer implemented as an iterable collection. It does not allocate + * any memory on the heap nor use standard containers. + */ +template <typename Char> +class Tokenizer { +public: + class iterator { + public: + iterator(const iterator&) = default; + iterator& operator=(const iterator&) = default; + + iterator& operator++(); + BasicStringPiece<Char> operator*(); + bool operator==(const iterator& rhs) const; + bool operator!=(const iterator& rhs) const; + + private: + friend class Tokenizer<Char>; + + iterator(BasicStringPiece<Char> s, Char sep, BasicStringPiece<Char> tok); + + BasicStringPiece<Char> str; + Char separator; + BasicStringPiece<Char> token; + }; + + Tokenizer(BasicStringPiece<Char> str, Char sep); + iterator begin(); + iterator end(); + +private: + const iterator mBegin; + const iterator mEnd; +}; + +template <typename Char> +inline Tokenizer<Char> tokenize(BasicStringPiece<Char> str, Char sep) { + return Tokenizer<Char>(str, sep); +} + +template <typename Char> +typename Tokenizer<Char>::iterator& Tokenizer<Char>::iterator::operator++() { + const Char* start = token.end(); + const Char* end = str.end(); + if (start == end) { + token.assign(token.end(), 0); + return *this; + } + + start += 1; + const Char* current = start; + while (current != end) { + if (*current == separator) { + token.assign(start, current - start); + return *this; + } + ++current; + } + token.assign(start, end - start); + return *this; +} + +template <typename Char> +inline BasicStringPiece<Char> Tokenizer<Char>::iterator::operator*() { + return token; +} + +template <typename Char> +inline bool Tokenizer<Char>::iterator::operator==(const iterator& rhs) const { + // We check equality here a bit differently. + // We need to know that the addresses are the same. + return token.begin() == rhs.token.begin() && token.end() == rhs.token.end(); +} + +template <typename Char> +inline bool Tokenizer<Char>::iterator::operator!=(const iterator& rhs) const { + return !(*this == rhs); +} + +template <typename Char> +inline Tokenizer<Char>::iterator::iterator(BasicStringPiece<Char> s, Char sep, + BasicStringPiece<Char> tok) : + str(s), separator(sep), token(tok) { +} + +template <typename Char> +inline typename Tokenizer<Char>::iterator Tokenizer<Char>::begin() { + return mBegin; +} + +template <typename Char> +inline typename Tokenizer<Char>::iterator Tokenizer<Char>::end() { + return mEnd; +} + +template <typename Char> +inline Tokenizer<Char>::Tokenizer(BasicStringPiece<Char> str, Char sep) : + mBegin(++iterator(str, sep, BasicStringPiece<Char>(str.begin() - 1, 0))), + mEnd(str, sep, BasicStringPiece<Char>(str.end(), 0)) { +} + +/** + * Returns a package name if the namespace URI is of the form: + * http://schemas.android.com/apk/res/<package> + * + * Special case: if namespaceUri is http://schemas.android.com/apk/res-auto, + * returns an empty package name. + */ +Maybe<std::u16string> extractPackageFromNamespace(const std::u16string& namespaceUri); + +} // namespace util + +/** + * Stream operator for functions. Calls the function with the stream as an argument. + * In the aapt namespace for lookup. + */ +inline ::std::ostream& operator<<(::std::ostream& out, + ::std::function<::std::ostream&(::std::ostream&)> f) { + return f(out); +} + +} // namespace aapt + +#endif // AAPT_UTIL_H diff --git a/tools/aapt2/Util_test.cpp b/tools/aapt2/Util_test.cpp new file mode 100644 index 0000000..0b08d24 --- /dev/null +++ b/tools/aapt2/Util_test.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 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. + */ + +#include <gtest/gtest.h> +#include <string> + +#include "StringPiece.h" +#include "Util.h" + +namespace aapt { + +TEST(UtilTest, TrimOnlyWhitespace) { + const std::u16string full = u"\n "; + + StringPiece16 trimmed = util::trimWhitespace(full); + EXPECT_TRUE(trimmed.empty()); + EXPECT_EQ(0u, trimmed.size()); +} + +TEST(UtilTest, StringEndsWith) { + EXPECT_TRUE(util::stringEndsWith<char>("hello.xml", ".xml")); +} + +TEST(UtilTest, StringStartsWith) { + EXPECT_TRUE(util::stringStartsWith<char>("hello.xml", "he")); +} + +TEST(UtilTest, StringBuilderWhitespaceRemoval) { + EXPECT_EQ(StringPiece16(u"hey guys this is so cool"), + util::StringBuilder().append(u" hey guys ") + .append(u" this is so cool ") + .str()); + + EXPECT_EQ(StringPiece16(u" wow, so many \t spaces. what?"), + util::StringBuilder().append(u" \" wow, so many \t ") + .append(u"spaces. \"what? ") + .str()); + + EXPECT_EQ(StringPiece16(u"where is the pie?"), + util::StringBuilder().append(u" where \t ") + .append(u" \nis the "" pie?") + .str()); +} + +TEST(UtilTest, StringBuilderEscaping) { + EXPECT_EQ(StringPiece16(u"hey guys\n this \t is so\\ cool"), + util::StringBuilder().append(u" hey guys\\n ") + .append(u" this \\t is so\\\\ cool ") + .str()); + + EXPECT_EQ(StringPiece16(u"@?#\\\'"), + util::StringBuilder().append(u"\\@\\?\\#\\\\\\'") + .str()); +} + +TEST(UtilTest, StringBuilderMisplacedQuote) { + util::StringBuilder builder{}; + EXPECT_FALSE(builder.append(u"they're coming!")); +} + +TEST(UtilTest, StringBuilderUnicodeCodes) { + EXPECT_EQ(StringPiece16(u"\u00AF\u0AF0 woah"), + util::StringBuilder().append(u"\\u00AF\\u0AF0 woah") + .str()); + + EXPECT_FALSE(util::StringBuilder().append(u"\\u00 yo")); +} + +TEST(UtilTest, TokenizeInput) { + auto tokenizer = util::tokenize(StringPiece16(u"this| is|the|end"), u'|'); + auto iter = tokenizer.begin(); + ASSERT_EQ(*iter, StringPiece16(u"this")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u" is")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"the")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"end")); + ++iter; + ASSERT_EQ(tokenizer.end(), iter); +} + +TEST(UtilTest, IsJavaClassName) { + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class$Inner")); + EXPECT_TRUE(util::isJavaClassName(u"android_test.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"_android_.test._Class_")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.$Inner")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.Inner$")); + EXPECT_FALSE(util::isJavaClassName(u".test.Class")); + EXPECT_FALSE(util::isJavaClassName(u"android")); +} + +TEST(UtilTest, FullyQualifiedClassName) { + Maybe<std::u16string> res = util::getFullyQualifiedClassName(u"android", u"asdf"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.asdf"); + + res = util::getFullyQualifiedClassName(u"android", u".asdf"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.asdf"); + + res = util::getFullyQualifiedClassName(u"android", u".a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.a.b"); + + res = util::getFullyQualifiedClassName(u"android", u"a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u"a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u""); + ASSERT_FALSE(res); + + res = util::getFullyQualifiedClassName(u"android", u"./Apple"); + ASSERT_FALSE(res); +} + + +} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.cpp b/tools/aapt2/XliffXmlPullParser.cpp new file mode 100644 index 0000000..31115f2 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "XliffXmlPullParser.h" + +#include <string> + +namespace aapt { + +XliffXmlPullParser::XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser) : + mParser(parser) { +} + +XmlPullParser::Event XliffXmlPullParser::next() { + while (XmlPullParser::isGoodEvent(mParser->next())) { + Event event = mParser->getEvent(); + if (event != Event::kStartElement && event != Event::kEndElement) { + break; + } + + if (mParser->getElementNamespace() != + u"urn:oasis:names:tc:xliff:document:1.2") { + break; + } + + const std::u16string& name = mParser->getElementName(); + if (name != u"bpt" + && name != u"ept" + && name != u"it" + && name != u"ph" + && name != u"g" + && name != u"bx" + && name != u"ex" + && name != u"x") { + break; + } + + // We hit a tag that was ignored, so get the next event. + } + return mParser->getEvent(); +} + +XmlPullParser::Event XliffXmlPullParser::getEvent() const { + return mParser->getEvent(); +} + +const std::string& XliffXmlPullParser::getLastError() const { + return mParser->getLastError(); +} + +const std::u16string& XliffXmlPullParser::getComment() const { + return mParser->getComment(); +} + +size_t XliffXmlPullParser::getLineNumber() const { + return mParser->getLineNumber(); +} + +size_t XliffXmlPullParser::getDepth() const { + return mParser->getDepth(); +} + +const std::u16string& XliffXmlPullParser::getText() const { + return mParser->getText(); +} + +const std::u16string& XliffXmlPullParser::getNamespacePrefix() const { + return mParser->getNamespacePrefix(); +} + +const std::u16string& XliffXmlPullParser::getNamespaceUri() const { + return mParser->getNamespaceUri(); +} + +bool XliffXmlPullParser::applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const { + return mParser->applyPackageAlias(package, defaultPackage); +} + +const std::u16string& XliffXmlPullParser::getElementNamespace() const { + return mParser->getElementNamespace(); +} + +const std::u16string& XliffXmlPullParser::getElementName() const { + return mParser->getElementName(); +} + +size_t XliffXmlPullParser::getAttributeCount() const { + return mParser->getAttributeCount(); +} + +XmlPullParser::const_iterator XliffXmlPullParser::beginAttributes() const { + return mParser->beginAttributes(); +} + +XmlPullParser::const_iterator XliffXmlPullParser::endAttributes() const { + return mParser->endAttributes(); +} + +} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.h b/tools/aapt2/XliffXmlPullParser.h new file mode 100644 index 0000000..7791227 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_XLIFF_XML_PULL_PARSER_H +#define AAPT_XLIFF_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <memory> +#include <string> + +namespace aapt { + +/** + * Strips xliff elements and provides the caller with a view of the + * underlying XML without xliff. + */ +class XliffXmlPullParser : public XmlPullParser { +public: + XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser); + XliffXmlPullParser(const XliffXmlPullParser& rhs) = delete; + + Event getEvent() const override; + const std::string& getLastError() const override; + Event next() override; + + const std::u16string& getComment() const override; + size_t getLineNumber() const override; + size_t getDepth() const override; + + const std::u16string& getText() const override; + + const std::u16string& getNamespacePrefix() const override; + const std::u16string& getNamespaceUri() const override; + bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) + const override; + + const std::u16string& getElementNamespace() const override; + const std::u16string& getElementName() const override; + + const_iterator beginAttributes() const override; + const_iterator endAttributes() const override; + size_t getAttributeCount() const override; + +private: + std::shared_ptr<XmlPullParser> mParser; +}; + +} // namespace aapt + +#endif // AAPT_XLIFF_XML_PULL_PARSER_H diff --git a/tools/aapt2/XliffXmlPullParser_test.cpp b/tools/aapt2/XliffXmlPullParser_test.cpp new file mode 100644 index 0000000..f903072 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser_test.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "SourceXmlPullParser.h" +#include "XliffXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(XliffXmlPullParserTest, IgnoreXliffTags) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">" << std::endl + << "<string name=\"foo\">" + << "Hey <xliff:g><xliff:it>there</xliff:it></xliff:g> world</string>" << std::endl + << "</resources>" << std::endl; + std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); + XliffXmlPullParser parser(sourceParser); + EXPECT_EQ(XmlPullParser::Event::kStartDocument, parser.getEvent()); + + EXPECT_EQ(XmlPullParser::Event::kStartNamespace, parser.next()); + EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); + EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"resources"); + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. + + EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"string"); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u"Hey "); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u"there"); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u" world"); + + EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"string"); + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. + + EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"resources"); + + EXPECT_EQ(XmlPullParser::Event::kEndNamespace, parser.next()); + EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); + EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, parser.next()); +} + +} // namespace aapt diff --git a/tools/aapt2/XmlDom.cpp b/tools/aapt2/XmlDom.cpp new file mode 100644 index 0000000..763029f --- /dev/null +++ b/tools/aapt2/XmlDom.cpp @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "Logger.h" +#include "Util.h" +#include "XmlDom.h" +#include "XmlPullParser.h" + +#include <cassert> +#include <memory> +#include <stack> +#include <string> +#include <tuple> + +namespace aapt { +namespace xml { + +constexpr char kXmlNamespaceSep = 1; + +struct Stack { + std::unique_ptr<xml::Node> root; + std::stack<xml::Node*> nodeStack; + std::u16string pendingComment; +}; + +/** + * Extracts the namespace and name of an expanded element or attribute name. + */ +static void splitName(const char* name, std::u16string* outNs, std::u16string* outName) { + const char* p = name; + while (*p != 0 && *p != kXmlNamespaceSep) { + p++; + } + + if (*p == 0) { + outNs->clear(); + *outName = util::utf8ToUtf16(name); + } else { + *outNs = util::utf8ToUtf16(StringPiece(name, (p - name))); + *outName = util::utf8ToUtf16(p + 1); + } +} + +static void addToStack(Stack* stack, XML_Parser parser, std::unique_ptr<Node> node) { + node->lineNumber = XML_GetCurrentLineNumber(parser); + node->columnNumber = XML_GetCurrentColumnNumber(parser); + + Node* thisNode = node.get(); + if (!stack->nodeStack.empty()) { + stack->nodeStack.top()->addChild(std::move(node)); + } else { + stack->root = std::move(node); + } + + if (thisNode->type != NodeType::kText) { + stack->nodeStack.push(thisNode); + } +} + +static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + std::unique_ptr<Namespace> ns = util::make_unique<Namespace>(); + if (prefix) { + ns->namespacePrefix = util::utf8ToUtf16(prefix); + } + + if (uri) { + ns->namespaceUri = util::utf8ToUtf16(uri); + } + + addToStack(stack, parser, std::move(ns)); +} + +static void XMLCALL endNamespaceHandler(void* userData, const char* prefix) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + assert(!stack->nodeStack.empty()); + stack->nodeStack.pop(); +} + +static bool lessAttribute(const Attribute& lhs, const Attribute& rhs) { + return std::tie(lhs.namespaceUri, lhs.name, lhs.value) < + std::tie(rhs.namespaceUri, rhs.name, rhs.value); +} + +static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + std::unique_ptr<Element> el = util::make_unique<Element>(); + splitName(name, &el->namespaceUri, &el->name); + + while (*attrs) { + Attribute attribute; + splitName(*attrs++, &attribute.namespaceUri, &attribute.name); + attribute.value = util::utf8ToUtf16(*attrs++); + + // Insert in sorted order. + auto iter = std::lower_bound(el->attributes.begin(), el->attributes.end(), attribute, + lessAttribute); + el->attributes.insert(iter, std::move(attribute)); + } + + el->comment = std::move(stack->pendingComment); + addToStack(stack, parser, std::move(el)); +} + +static void XMLCALL endElementHandler(void* userData, const char* name) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + assert(!stack->nodeStack.empty()); + stack->nodeStack.top()->comment = std::move(stack->pendingComment); + stack->nodeStack.pop(); +} + +static void XMLCALL characterDataHandler(void* userData, const char* s, int len) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + if (!s || len <= 0) { + return; + } + + // See if we can just append the text to a previous text node. + if (!stack->nodeStack.empty()) { + Node* currentParent = stack->nodeStack.top(); + if (!currentParent->children.empty()) { + Node* lastChild = currentParent->children.back().get(); + if (lastChild->type == NodeType::kText) { + Text* text = static_cast<Text*>(lastChild); + text->text += util::utf8ToUtf16(StringPiece(s, len)); + return; + } + } + } + + std::unique_ptr<Text> text = util::make_unique<Text>(); + text->text = util::utf8ToUtf16(StringPiece(s, len)); + addToStack(stack, parser, std::move(text)); +} + +static void XMLCALL commentDataHandler(void* userData, const char* comment) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + if (!stack->pendingComment.empty()) { + stack->pendingComment += '\n'; + } + stack->pendingComment += util::utf8ToUtf16(comment); +} + +std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger) { + Stack stack; + + XML_Parser parser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); + XML_SetUserData(parser, &stack); + XML_UseParserAsHandlerArg(parser); + XML_SetElementHandler(parser, startElementHandler, endElementHandler); + XML_SetNamespaceDeclHandler(parser, startNamespaceHandler, endNamespaceHandler); + XML_SetCharacterDataHandler(parser, characterDataHandler); + XML_SetCommentHandler(parser, commentDataHandler); + + char buffer[1024]; + while (!in->eof()) { + in->read(buffer, sizeof(buffer) / sizeof(buffer[0])); + if (in->bad() && !in->eof()) { + stack.root = {}; + logger->error() << strerror(errno) << std::endl; + break; + } + + if (XML_Parse(parser, buffer, in->gcount(), in->eof()) == XML_STATUS_ERROR) { + stack.root = {}; + logger->error(XML_GetCurrentLineNumber(parser)) + << XML_ErrorString(XML_GetErrorCode(parser)) << std::endl; + break; + } + } + + XML_ParserFree(parser); + return std::move(stack.root); +} + +static void copyAttributes(Element* el, android::ResXMLParser* parser) { + const size_t attrCount = parser->getAttributeCount(); + if (attrCount > 0) { + el->attributes.reserve(attrCount); + for (size_t i = 0; i < attrCount; i++) { + Attribute attr; + size_t len; + const char16_t* str16 = parser->getAttributeNamespace(i, &len); + if (str16) { + attr.namespaceUri.assign(str16, len); + } + + str16 = parser->getAttributeName(i, &len); + if (str16) { + attr.name.assign(str16, len); + } + + str16 = parser->getAttributeStringValue(i, &len); + if (str16) { + attr.value.assign(str16, len); + } + el->attributes.push_back(std::move(attr)); + } + } +} + +std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger) { + std::unique_ptr<Node> root; + std::stack<Node*> nodeStack; + + android::ResXMLTree tree; + if (tree.setTo(data, dataLen) != android::NO_ERROR) { + return {}; + } + + android::ResXMLParser::event_code_t code; + while ((code = tree.next()) != android::ResXMLParser::BAD_DOCUMENT && + code != android::ResXMLParser::END_DOCUMENT) { + std::unique_ptr<Node> newNode; + switch (code) { + case android::ResXMLParser::START_NAMESPACE: { + std::unique_ptr<Namespace> node = util::make_unique<Namespace>(); + size_t len; + const char16_t* str16 = tree.getNamespacePrefix(&len); + if (str16) { + node->namespacePrefix.assign(str16, len); + } + + str16 = tree.getNamespaceUri(&len); + if (str16) { + node->namespaceUri.assign(str16, len); + } + newNode = std::move(node); + break; + } + + case android::ResXMLParser::START_TAG: { + std::unique_ptr<Element> node = util::make_unique<Element>(); + size_t len; + const char16_t* str16 = tree.getElementNamespace(&len); + if (str16) { + node->namespaceUri.assign(str16, len); + } + + str16 = tree.getElementName(&len); + if (str16) { + node->name.assign(str16, len); + } + + copyAttributes(node.get(), &tree); + + newNode = std::move(node); + break; + } + + case android::ResXMLParser::TEXT: { + std::unique_ptr<Text> node = util::make_unique<Text>(); + size_t len; + const char16_t* str16 = tree.getText(&len); + if (str16) { + node->text.assign(str16, len); + } + newNode = std::move(node); + break; + } + + case android::ResXMLParser::END_NAMESPACE: + case android::ResXMLParser::END_TAG: + assert(!nodeStack.empty()); + nodeStack.pop(); + break; + + default: + assert(false); + break; + } + + if (newNode) { + newNode->lineNumber = tree.getLineNumber(); + + Node* thisNode = newNode.get(); + if (!root) { + assert(nodeStack.empty()); + root = std::move(newNode); + } else { + assert(!nodeStack.empty()); + nodeStack.top()->addChild(std::move(newNode)); + } + + if (thisNode->type != NodeType::kText) { + nodeStack.push(thisNode); + } + } + } + return std::move(root); +} + +Node::Node(NodeType type) : type(type), parent(nullptr), lineNumber(0), columnNumber(0) { +} + +void Node::addChild(std::unique_ptr<Node> child) { + child->parent = this; + children.push_back(std::move(child)); +} + +Namespace::Namespace() : BaseNode(NodeType::kNamespace) { +} + +std::unique_ptr<Node> Namespace::clone() const { + Namespace* ns = new Namespace(); + ns->lineNumber = lineNumber; + ns->columnNumber = columnNumber; + ns->comment = comment; + ns->namespacePrefix = namespacePrefix; + ns->namespaceUri = namespaceUri; + for (auto& child : children) { + ns->addChild(child->clone()); + } + return std::unique_ptr<Node>(ns); +} + +Element::Element() : BaseNode(NodeType::kElement) { +} + +std::unique_ptr<Node> Element::clone() const { + Element* el = new Element(); + el->lineNumber = lineNumber; + el->columnNumber = columnNumber; + el->comment = comment; + el->namespaceUri = namespaceUri; + el->name = name; + el->attributes = attributes; + for (auto& child : children) { + el->addChild(child->clone()); + } + return std::unique_ptr<Node>(el); +} + +Attribute* Element::findAttribute(const StringPiece16& ns, const StringPiece16& name) { + for (auto& attr : attributes) { + if (ns == attr.namespaceUri && name == attr.name) { + return &attr; + } + } + return nullptr; +} + +Element* Element::findChild(const StringPiece16& ns, const StringPiece16& name) { + return findChildWithAttribute(ns, name, nullptr); +} + +Element* Element::findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, + const Attribute* reqAttr) { + for (auto& childNode : children) { + Node* child = childNode.get(); + while (child->type == NodeType::kNamespace) { + if (child->children.empty()) { + break; + } + child = child->children[0].get(); + } + + if (child->type == NodeType::kElement) { + Element* el = static_cast<Element*>(child); + if (ns == el->namespaceUri && name == el->name) { + if (!reqAttr) { + return el; + } + + Attribute* attrName = el->findAttribute(reqAttr->namespaceUri, reqAttr->name); + if (attrName && attrName->value == reqAttr->value) { + return el; + } + } + } + } + return nullptr; +} + +std::vector<Element*> Element::getChildElements() { + std::vector<Element*> elements; + for (auto& childNode : children) { + Node* child = childNode.get(); + while (child->type == NodeType::kNamespace) { + if (child->children.empty()) { + break; + } + child = child->children[0].get(); + } + + if (child->type == NodeType::kElement) { + elements.push_back(static_cast<Element*>(child)); + } + } + return elements; +} + +Text::Text() : BaseNode(NodeType::kText) { +} + +std::unique_ptr<Node> Text::clone() const { + Text* el = new Text(); + el->lineNumber = lineNumber; + el->columnNumber = columnNumber; + el->comment = comment; + el->text = text; + return std::unique_ptr<Node>(el); +} + +} // namespace xml +} // namespace aapt diff --git a/tools/aapt2/XmlDom.h b/tools/aapt2/XmlDom.h new file mode 100644 index 0000000..6931884 --- /dev/null +++ b/tools/aapt2/XmlDom.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_XML_DOM_H +#define AAPT_XML_DOM_H + +#include "Logger.h" +#include "StringPiece.h" + +#include <istream> +#include <libexpat/expat.h> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { +namespace xml { + +struct Visitor; + +/** + * The type of node. Can be used to downcast to the concrete XML node + * class. + */ +enum class NodeType { + kNamespace, + kElement, + kText, +}; + +/** + * Base class for all XML nodes. + */ +struct Node { + NodeType type; + Node* parent; + size_t lineNumber; + size_t columnNumber; + std::u16string comment; + std::vector<std::unique_ptr<Node>> children; + + Node(NodeType type); + void addChild(std::unique_ptr<Node> child); + virtual std::unique_ptr<Node> clone() const = 0; + virtual void accept(Visitor* visitor) = 0; + virtual ~Node() {} +}; + +/** + * Base class that implements the visitor methods for a + * subclass of Node. + */ +template <typename Derived> +struct BaseNode : public Node { + BaseNode(NodeType t); + virtual void accept(Visitor* visitor) override; +}; + +/** + * A Namespace XML node. Can only have one child. + */ +struct Namespace : public BaseNode<Namespace> { + std::u16string namespacePrefix; + std::u16string namespaceUri; + + Namespace(); + virtual std::unique_ptr<Node> clone() const override; +}; + +/** + * An XML attribute. + */ +struct Attribute { + std::u16string namespaceUri; + std::u16string name; + std::u16string value; +}; + +/** + * An Element XML node. + */ +struct Element : public BaseNode<Element> { + std::u16string namespaceUri; + std::u16string name; + std::vector<Attribute> attributes; + + Element(); + virtual std::unique_ptr<Node> clone() const override; + Attribute* findAttribute(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChild(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, + const xml::Attribute* reqAttr); + std::vector<xml::Element*> getChildElements(); +}; + +/** + * A Text (CDATA) XML node. Can not have any children. + */ +struct Text : public BaseNode<Text> { + std::u16string text; + + Text(); + virtual std::unique_ptr<Node> clone() const override; +}; + +/** + * Inflates an XML DOM from a text stream, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger); + +/** + * Inflates an XML DOM from a binary ResXMLTree, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger); + +/** + * A visitor interface for the different XML Node subtypes. + */ +struct Visitor { + virtual void visit(Namespace* node) = 0; + virtual void visit(Element* node) = 0; + virtual void visit(Text* text) = 0; +}; + +// Implementations + +template <typename Derived> +BaseNode<Derived>::BaseNode(NodeType type) : Node(type) { +} + +template <typename Derived> +void BaseNode<Derived>::accept(Visitor* visitor) { + visitor->visit(static_cast<Derived*>(this)); +} + +} // namespace xml +} // namespace aapt + +#endif // AAPT_XML_DOM_H diff --git a/tools/aapt2/XmlDom_test.cpp b/tools/aapt2/XmlDom_test.cpp new file mode 100644 index 0000000..0217144 --- /dev/null +++ b/tools/aapt2/XmlDom_test.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "XmlDom.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + +TEST(XmlDomTest, Inflate) { + std::stringstream in(kXmlPreamble); + in << R"EOF( + <Layout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </Layout> + )EOF"; + + SourceLogger logger(Source{ "/test/path" }); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + ASSERT_NE(root, nullptr); + + EXPECT_EQ(root->type, xml::NodeType::kNamespace); + xml::Namespace* ns = static_cast<xml::Namespace*>(root.get()); + EXPECT_EQ(ns->namespaceUri, u"http://schemas.android.com/apk/res/android"); + EXPECT_EQ(ns->namespacePrefix, u"android"); +} + +} // namespace aapt diff --git a/tools/aapt2/XmlFlattener.cpp b/tools/aapt2/XmlFlattener.cpp new file mode 100644 index 0000000..56b5613 --- /dev/null +++ b/tools/aapt2/XmlFlattener.cpp @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "BigBuffer.h" +#include "Logger.h" +#include "Maybe.h" +#include "Resolver.h" +#include "Resource.h" +#include "ResourceParser.h" +#include "ResourceValues.h" +#include "SdkConstants.h" +#include "Source.h" +#include "StringPool.h" +#include "Util.h" +#include "XmlFlattener.h" + +#include <androidfw/ResourceTypes.h> +#include <limits> +#include <map> +#include <string> +#include <vector> + +namespace aapt { +namespace xml { + +constexpr uint32_t kLowPriority = 0xffffffffu; + +// A vector that maps String refs to their final destination in the out buffer. +using FlatStringRefList = std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>; + +struct XmlFlattener : public Visitor { + XmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, + const std::u16string& defaultPackage) : + mOut(outBuffer), mPool(pool), mStringRefs(stringRefs), + mDefaultPackage(defaultPackage) { + } + + // No copying. + XmlFlattener(const XmlFlattener&) = delete; + XmlFlattener& operator=(const XmlFlattener&) = delete; + + void writeNamespace(Namespace* node, uint16_t type) { + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_namespaceExt* flatNs = + mOut->nextBlock<android::ResXMLTree_namespaceExt>(); + mOut->align4(); + + flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; + addString(node->namespacePrefix, kLowPriority, &flatNs->prefix); + addString(node->namespaceUri, kLowPriority, &flatNs->uri); + } + + virtual void visit(Namespace* node) override { + // Extract the package/prefix from this namespace node. + Maybe<std::u16string> package = util::extractPackageFromNamespace(node->namespaceUri); + if (package) { + mPackageAliases.emplace_back( + node->namespacePrefix, + package.value().empty() ? mDefaultPackage : package.value()); + } + + writeNamespace(node, android::RES_XML_START_NAMESPACE_TYPE); + for (const auto& child : node->children) { + child->accept(this); + } + writeNamespace(node, android::RES_XML_END_NAMESPACE_TYPE); + + if (package) { + mPackageAliases.pop_back(); + } + } + + virtual void visit(Text* node) override { + if (util::trimWhitespace(node->text).empty()) { + return; + } + + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_cdataExt* flatText = mOut->nextBlock<android::ResXMLTree_cdataExt>(); + mOut->align4(); + + const uint16_t type = android::RES_XML_CDATA_TYPE; + flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; + addString(node->text, kLowPriority, &flatText->data); + } + + virtual void visit(Element* node) override { + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_attrExt* flatElem = mOut->nextBlock<android::ResXMLTree_attrExt>(); + + const uint16_t type = android::RES_XML_START_ELEMENT_TYPE; + flatNode->header = { type, sizeof(*flatNode), 0 }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; + + addString(node->namespaceUri, kLowPriority, &flatElem->ns); + addString(node->name, kLowPriority, &flatElem->name); + flatElem->attributeStart = sizeof(*flatElem); + flatElem->attributeSize = sizeof(android::ResXMLTree_attribute); + flatElem->attributeCount = node->attributes.size(); + + if (!writeAttributes(mOut, node, flatElem)) { + mError = true; + } + + mOut->align4(); + flatNode->header.size = (uint32_t)(mOut->size() - startIndex); + + for (const auto& child : node->children) { + child->accept(this); + } + + const size_t startEndIndex = mOut->size(); + android::ResXMLTree_node* flatEndNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_endElementExt* flatEndElem = + mOut->nextBlock<android::ResXMLTree_endElementExt>(); + mOut->align4(); + + const uint16_t endType = android::RES_XML_END_ELEMENT_TYPE; + flatEndNode->header = { endType, sizeof(*flatEndNode), + (uint32_t)(mOut->size() - startEndIndex) }; + flatEndNode->lineNumber = node->lineNumber; + flatEndNode->comment.index = -1; + + addString(node->namespaceUri, kLowPriority, &flatEndElem->ns); + addString(node->name, kLowPriority, &flatEndElem->name); + } + + bool success() const { + return !mError; + } + +protected: + void addString(const StringPiece16& str, uint32_t priority, android::ResStringPool_ref* dest) { + if (!str.empty()) { + mStringRefs->emplace_back(mPool->makeRef(str, StringPool::Context{ priority }), dest); + } else { + // The device doesn't think a string of size 0 is the same as null. + dest->index = -1; + } + } + + void addString(const StringPool::Ref& ref, android::ResStringPool_ref* dest) { + mStringRefs->emplace_back(ref, dest); + } + + Maybe<std::u16string> getPackageAlias(const std::u16string& prefix) { + const auto endIter = mPackageAliases.rend(); + for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { + if (iter->first == prefix) { + return iter->second; + } + } + return {}; + } + + const std::u16string& getDefaultPackage() const { + return mDefaultPackage; + } + + /** + * Subclasses override this to deal with attributes. Attributes can be flattened as + * raw values or as resources. + */ + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) = 0; + +private: + BigBuffer* mOut; + StringPool* mPool; + FlatStringRefList* mStringRefs; + std::u16string mDefaultPackage; + bool mError = false; + std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; +}; + +/** + * Flattens XML, encoding the attributes as raw strings. This is used in the compile phase. + */ +struct CompileXmlFlattener : public XmlFlattener { + CompileXmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, + const std::u16string& defaultPackage) : + XmlFlattener(outBuffer, pool, stringRefs, defaultPackage) { + } + + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) override { + flatElem->attributeCount = node->attributes.size(); + if (node->attributes.empty()) { + return true; + } + + android::ResXMLTree_attribute* flatAttrs = out->nextBlock<android::ResXMLTree_attribute>( + node->attributes.size()); + for (const Attribute& attr : node->attributes) { + addString(attr.namespaceUri, kLowPriority, &flatAttrs->ns); + addString(attr.name, kLowPriority, &flatAttrs->name); + addString(attr.value, kLowPriority, &flatAttrs->rawValue); + flatAttrs++; + } + return true; + } +}; + +struct AttributeToFlatten { + uint32_t resourceId = 0; + const Attribute* xmlAttr = nullptr; + const ::aapt::Attribute* resourceAttr = nullptr; +}; + +static bool lessAttributeId(const AttributeToFlatten& a, uint32_t id) { + return a.resourceId < id; +} + +/** + * Flattens XML, encoding the attributes as resources. + */ +struct LinkedXmlFlattener : public XmlFlattener { + LinkedXmlFlattener(BigBuffer* outBuffer, StringPool* pool, + std::map<std::u16string, StringPool>* packagePools, + FlatStringRefList* stringRefs, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + SourceLogger* logger, + const FlattenOptions& options) : + XmlFlattener(outBuffer, pool, stringRefs, defaultPackage), mResolver(resolver), + mLogger(logger), mPackagePools(packagePools), mOptions(options) { + } + + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) override { + bool error = false; + std::vector<AttributeToFlatten> sortedAttributes; + uint32_t nextAttributeId = 0x80000000u; + + // Sort and filter attributes by their resource ID. + for (const Attribute& attr : node->attributes) { + AttributeToFlatten attrToFlatten; + attrToFlatten.xmlAttr = &attr; + + Maybe<std::u16string> package = util::extractPackageFromNamespace(attr.namespaceUri); + if (package) { + // Find the Attribute object via our Resolver. + ResourceName attrName = { package.value(), ResourceType::kAttr, attr.name }; + if (attrName.package.empty()) { + attrName.package = getDefaultPackage(); + } + + Maybe<IResolver::Entry> result = mResolver->findAttribute(attrName); + if (!result || !result.value().id.isValid() || !result.value().attr) { + error = true; + mLogger->error(node->lineNumber) + << "unresolved attribute '" << attrName << "'." + << std::endl; + } else { + attrToFlatten.resourceId = result.value().id.id; + attrToFlatten.resourceAttr = result.value().attr; + + size_t sdk = findAttributeSdkLevel(attrToFlatten.resourceId); + if (mOptions.maxSdkAttribute && sdk > mOptions.maxSdkAttribute.value()) { + // We need to filter this attribute out. + mSmallestFilteredSdk = std::min(mSmallestFilteredSdk, sdk); + continue; + } + } + } + + if (attrToFlatten.resourceId == 0) { + // Attributes that have no resource ID (because they don't belong to a + // package) should appear after those that do have resource IDs. Assign + // them some integer value that will appear after. + attrToFlatten.resourceId = nextAttributeId++; + } + + // Insert the attribute into the sorted vector. + auto iter = std::lower_bound(sortedAttributes.begin(), sortedAttributes.end(), + attrToFlatten.resourceId, lessAttributeId); + sortedAttributes.insert(iter, std::move(attrToFlatten)); + } + + flatElem->attributeCount = sortedAttributes.size(); + if (sortedAttributes.empty()) { + return true; + } + + android::ResXMLTree_attribute* flatAttr = out->nextBlock<android::ResXMLTree_attribute>( + sortedAttributes.size()); + + // Now that we have sorted the attributes into their final encoded order, it's time + // to actually write them out. + uint16_t attributeIndex = 1; + for (const AttributeToFlatten& attrToFlatten : sortedAttributes) { + Maybe<std::u16string> package = util::extractPackageFromNamespace( + attrToFlatten.xmlAttr->namespaceUri); + + // Assign the indices for specific attributes. + if (package && package.value() == u"android" && attrToFlatten.xmlAttr->name == u"id") { + flatElem->idIndex = attributeIndex; + } else if (attrToFlatten.xmlAttr->namespaceUri.empty()) { + if (attrToFlatten.xmlAttr->name == u"class") { + flatElem->classIndex = attributeIndex; + } else if (attrToFlatten.xmlAttr->name == u"style") { + flatElem->styleIndex = attributeIndex; + } + } + attributeIndex++; + + // Add the namespaceUri and name to the list of StringRefs to encode. + addString(attrToFlatten.xmlAttr->namespaceUri, kLowPriority, &flatAttr->ns); + flatAttr->rawValue.index = -1; + + if (!attrToFlatten.resourceAttr) { + addString(attrToFlatten.xmlAttr->name, kLowPriority, &flatAttr->name); + } else { + // We've already extracted the package successfully before. + assert(package); + + // Attribute names are stored without packages, but we use + // their StringPool index to lookup their resource IDs. + // This will cause collisions, so we can't dedupe + // attribute names from different packages. We use separate + // pools that we later combine. + // + // Lookup the StringPool for this package and make the reference there. + StringPool::Ref nameRef = (*mPackagePools)[package.value()].makeRef( + attrToFlatten.xmlAttr->name, + StringPool::Context{ attrToFlatten.resourceId }); + + // Add it to the list of strings to flatten. + addString(nameRef, &flatAttr->name); + + if (mOptions.keepRawValues) { + // Keep raw values (this is for static libraries). + // TODO(with a smarter inflater for binary XML, we can do without this). + addString(attrToFlatten.xmlAttr->value, kLowPriority, &flatAttr->rawValue); + } + } + + error |= !flattenItem(node, attrToFlatten.xmlAttr->value, attrToFlatten.resourceAttr, + flatAttr); + flatAttr->typedValue.size = sizeof(flatAttr->typedValue); + flatAttr++; + } + return !error; + } + + Maybe<size_t> getSmallestFilteredSdk() const { + if (mSmallestFilteredSdk == std::numeric_limits<size_t>::max()) { + return {}; + } + return mSmallestFilteredSdk; + } + +private: + bool flattenItem(const Node* el, const std::u16string& value, const ::aapt::Attribute* attr, + android::ResXMLTree_attribute* flatAttr) { + std::unique_ptr<Item> item; + if (!attr) { + bool create = false; + item = ResourceParser::tryParseReference(value, &create); + if (!item) { + flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; + addString(value, kLowPriority, &flatAttr->rawValue); + addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( + &flatAttr->typedValue.data)); + return true; + } + } else { + item = ResourceParser::parseItemForAttribute(value, *attr); + if (!item) { + if (!(attr->typeMask & android::ResTable_map::TYPE_STRING)) { + mLogger->error(el->lineNumber) + << "'" + << value + << "' is not compatible with attribute '" + << *attr + << "'." + << std::endl; + return false; + } + + flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; + addString(value, kLowPriority, &flatAttr->rawValue); + addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( + &flatAttr->typedValue.data)); + return true; + } + } + + assert(item); + + bool error = false; + + // If this is a reference, resolve the name into an ID. + visitFunc<Reference>(*item, [&](Reference& reference) { + // First see if we can convert the package name from a prefix to a real + // package name. + ResourceName realName = reference.name; + if (!realName.package.empty()) { + Maybe<std::u16string> package = getPackageAlias(realName.package); + if (package) { + realName.package = package.value(); + } + } else { + realName.package = getDefaultPackage(); + } + + Maybe<ResourceId> result = mResolver->findId(realName); + if (!result || !result.value().isValid()) { + std::ostream& out = mLogger->error(el->lineNumber) + << "unresolved reference '" + << reference.name + << "'"; + if (realName != reference.name) { + out << " (aka '" << realName << "')"; + } + out << "'." << std::endl; + error = true; + } else { + reference.id = result.value(); + } + }); + + if (error) { + return false; + } + + item->flatten(flatAttr->typedValue); + return true; + } + + std::shared_ptr<IResolver> mResolver; + SourceLogger* mLogger; + std::map<std::u16string, StringPool>* mPackagePools; + FlattenOptions mOptions; + size_t mSmallestFilteredSdk = std::numeric_limits<size_t>::max(); +}; + +/** + * The binary XML file expects the StringPool to appear first, but we haven't collected the + * strings yet. We write to a temporary BigBuffer while parsing the input, adding strings + * we encounter to the StringPool. At the end, we write the StringPool to the given BigBuffer and + * then move the data from the temporary BigBuffer into the given one. This incurs no + * copies as the given BigBuffer simply takes ownership of the data. + */ +static void flattenXml(StringPool* pool, FlatStringRefList* stringRefs, BigBuffer* outBuffer, + BigBuffer&& xmlTreeBuffer) { + // Sort the string pool so that attribute resource IDs show up first. + pool->sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.context.priority < b.context.priority; + }); + + // Now we flatten the string pool references into the correct places. + for (const auto& refEntry : *stringRefs) { + refEntry.second->index = refEntry.first.getIndex(); + } + + // Write the XML header. + const size_t beforeXmlTreeIndex = outBuffer->size(); + android::ResXMLTree_header* header = outBuffer->nextBlock<android::ResXMLTree_header>(); + header->header.type = android::RES_XML_TYPE; + header->header.headerSize = sizeof(*header); + + // Flatten the StringPool. + StringPool::flattenUtf16(outBuffer, *pool); + + // Write the array of resource IDs, indexed by StringPool order. + const size_t beforeResIdMapIndex = outBuffer->size(); + android::ResChunk_header* resIdMapChunk = outBuffer->nextBlock<android::ResChunk_header>(); + resIdMapChunk->type = android::RES_XML_RESOURCE_MAP_TYPE; + resIdMapChunk->headerSize = sizeof(*resIdMapChunk); + for (const auto& str : *pool) { + ResourceId id { str->context.priority }; + if (id.id == kLowPriority || !id.isValid()) { + // When we see the first non-resource ID, + // we're done. + break; + } + + *outBuffer->nextBlock<uint32_t>() = id.id; + } + resIdMapChunk->size = outBuffer->size() - beforeResIdMapIndex; + + // Move the temporary BigBuffer into outBuffer. + outBuffer->appendBuffer(std::move(xmlTreeBuffer)); + header->header.size = outBuffer->size() - beforeXmlTreeIndex; +} + +bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer) { + StringPool pool; + + // This will hold the StringRefs and the location in which to write the index. + // Once we sort the StringPool, we can assign the updated indices + // to the correct data locations. + FlatStringRefList stringRefs; + + // Since we don't know the size of the final StringPool, we write to this + // temporary BigBuffer, which we will append to outBuffer later. + BigBuffer out(1024); + + CompileXmlFlattener flattener(&out, &pool, &stringRefs, defaultPackage); + root->accept(&flattener); + + if (!flattener.success()) { + return false; + } + + flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); + return true; +}; + +Maybe<size_t> flattenAndLink(const Source& source, Node* root, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + const FlattenOptions& options, BigBuffer* outBuffer) { + SourceLogger logger(source); + StringPool pool; + + // Attribute names are stored without packages, but we use + // their StringPool index to lookup their resource IDs. + // This will cause collisions, so we can't dedupe + // attribute names from different packages. We use separate + // pools that we later combine. + std::map<std::u16string, StringPool> packagePools; + + FlatStringRefList stringRefs; + + // Since we don't know the size of the final StringPool, we write to this + // temporary BigBuffer, which we will append to outBuffer later. + BigBuffer out(1024); + + LinkedXmlFlattener flattener(&out, &pool, &packagePools, &stringRefs, defaultPackage, resolver, + &logger, options); + root->accept(&flattener); + + if (!flattener.success()) { + return {}; + } + + // Merge the package pools into the main pool. + for (auto& packagePoolEntry : packagePools) { + pool.merge(std::move(packagePoolEntry.second)); + } + + flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); + + if (flattener.getSmallestFilteredSdk()) { + return flattener.getSmallestFilteredSdk(); + } + return 0; +} + +} // namespace xml +} // namespace aapt diff --git a/tools/aapt2/XmlFlattener.h b/tools/aapt2/XmlFlattener.h new file mode 100644 index 0000000..4ece0a3 --- /dev/null +++ b/tools/aapt2/XmlFlattener.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_XML_FLATTENER_H +#define AAPT_XML_FLATTENER_H + +#include "BigBuffer.h" +#include "Maybe.h" +#include "Resolver.h" +#include "Source.h" +#include "XmlDom.h" + +#include <string> + +namespace aapt { +namespace xml { + +/** + * Flattens an XML file into a binary representation parseable by + * the Android resource system. + */ +bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer); + +/** + * Options for flattenAndLink. + */ +struct FlattenOptions { + /** + * Keep attribute raw string values along with typed values. + */ + bool keepRawValues = false; + + /** + * If set, any attribute introduced in a later SDK will not be encoded. + */ + Maybe<size_t> maxSdkAttribute; +}; + +/** + * Like flatten(Node*,BigBuffer*), but references to resources are checked + * and string values are transformed to typed data where possible. + * + * `defaultPackage` is used when a reference has no package or the namespace URI + * "http://schemas.android.com/apk/res-auto" is used. + * + * `resolver` is used to resolve references to resources. + */ +Maybe<size_t> flattenAndLink(const Source& source, Node* root, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + const FlattenOptions& options, BigBuffer* outBuffer); + +} // namespace xml +} // namespace aapt + +#endif // AAPT_XML_FLATTENER_H diff --git a/tools/aapt2/XmlFlattener_test.cpp b/tools/aapt2/XmlFlattener_test.cpp new file mode 100644 index 0000000..8915d24 --- /dev/null +++ b/tools/aapt2/XmlFlattener_test.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "MockResolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" +#include "XmlFlattener.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +using namespace android; + +namespace aapt { +namespace xml { + +constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + +class XmlFlattenerTest : public ::testing::Test { +public: + virtual void SetUp() override { + mResolver = std::make_shared<MockResolver>( + std::make_shared<ResourceTable>(), + std::map<ResourceName, ResourceId>({ + { ResourceName{ u"android", ResourceType::kAttr, u"attr" }, + ResourceId{ 0x01010000u } }, + { ResourceName{ u"android", ResourceType::kId, u"id" }, + ResourceId{ 0x01020000u } }, + { ResourceName{ u"com.lib", ResourceType::kAttr, u"attr" }, + ResourceId{ 0x01010001u } }, + { ResourceName{ u"com.lib", ResourceType::kId, u"id" }, + ResourceId{ 0x01020001u } }})); + } + + ::testing::AssertionResult testFlatten(const std::string& in, ResXMLTree* outTree) { + std::stringstream input(kXmlPreamble); + input << in << std::endl; + + SourceLogger logger(Source{ "test.xml" }); + std::unique_ptr<Node> root = inflate(&input, &logger); + if (!root) { + return ::testing::AssertionFailure(); + } + + BigBuffer outBuffer(1024); + if (!flattenAndLink(Source{ "test.xml" }, root.get(), std::u16string(u"android"), + mResolver, {}, &outBuffer)) { + return ::testing::AssertionFailure(); + } + + std::unique_ptr<uint8_t[]> data = util::copy(outBuffer); + if (outTree->setTo(data.get(), outBuffer.size(), true) != NO_ERROR) { + return ::testing::AssertionFailure(); + } + return ::testing::AssertionSuccess(); + } + + std::shared_ptr<IResolver> mResolver; +}; + +TEST_F(XmlFlattenerTest, ParseSimpleView) { + std::string input = R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:attr="@id/id" + class="str" + style="@id/id"> + </View> + )EOF"; + ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + } + + const StringPiece16 androidNs = u"http://schemas.android.com/apk/res/android"; + const StringPiece16 attrName = u"attr"; + ssize_t idx = tree.indexOfAttribute(androidNs.data(), androidNs.size(), attrName.data(), + attrName.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0x01010000u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); + + const StringPiece16 class16 = u"class"; + idx = tree.indexOfAttribute(nullptr, 0, class16.data(), class16.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_STRING); + EXPECT_EQ(tree.getAttributeData(idx), tree.getAttributeValueStringID(idx)); + + const StringPiece16 style16 = u"style"; + idx = tree.indexOfAttribute(nullptr, 0, style16.data(), style16.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); + EXPECT_EQ((uint32_t) tree.getAttributeData(idx), 0x01020000u); + EXPECT_EQ(tree.getAttributeValueStringID(idx), -1); + + while (tree.next() != ResXMLTree::END_DOCUMENT) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + } +} + +TEST_F(XmlFlattenerTest, ParseViewWithPackageAlias) { + std::string input = "<View xmlns:ns1=\"http://schemas.android.com/apk/res/android\"\n" + " xmlns:ns2=\"http://schemas.android.com/apk/res/android\"\n" + " ns1:attr=\"@ns2:id/id\">\n" + "</View>"; + ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != ResXMLTree::END_DOCUMENT) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + } +} + +::testing::AssertionResult attributeNameAndValueEquals(ResXMLTree* tree, size_t index, + ResourceId nameId, ResourceId valueId) { + if (index >= tree->getAttributeCount()) { + return ::testing::AssertionFailure() << "index " << index << " is out of bounds (" + << tree->getAttributeCount() << ")"; + } + + if (tree->getAttributeNameResID(index) != nameId.id) { + return ::testing::AssertionFailure() + << "attribute at index " << index << " has ID " + << ResourceId{ (uint32_t) tree->getAttributeNameResID(index) } + << ". Expected ID " << nameId; + } + + if (tree->getAttributeDataType(index) != Res_value::TYPE_REFERENCE) { + return ::testing::AssertionFailure() << "attribute at index " << index << " has value of " + << "type " << std::hex + << tree->getAttributeDataType(index) << std::dec + << ". Expected reference (" << std::hex + << Res_value::TYPE_REFERENCE << std::dec << ")"; + } + + if ((uint32_t) tree->getAttributeData(index) != valueId.id) { + return ::testing::AssertionFailure() + << "attribute at index " << index << " has value " << "with ID " + << ResourceId{ (uint32_t) tree->getAttributeData(index) } + << ". Expected ID " << valueId; + } + return ::testing::AssertionSuccess(); +} + +TEST_F(XmlFlattenerTest, ParseViewWithShadowedPackageAlias) { + std::string input = "<View xmlns:app=\"http://schemas.android.com/apk/res/android\"\n" + " app:attr=\"@app:id/id\">\n" + " <View xmlns:app=\"http://schemas.android.com/apk/res/com.lib\"\n" + " app:attr=\"@app:id/id\"/>\n" + "</View>"; + ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + } + + ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010000u }, + ResourceId{ 0x01020000u })); + + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + } + + ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010001u }, + ResourceId{ 0x01020001u })); +} + +TEST_F(XmlFlattenerTest, ParseViewWithLocalPackageAndAliasOfTheSameName) { + std::string input = "<View xmlns:android=\"http://schemas.android.com/apk/res/com.lib\"\n" + " android:attr=\"@id/id\"/>"; + ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + } + + // We expect the 'android:attr' to be converted to 'com.lib:attr' due to the namespace + // assignment. + // However, we didn't give '@id/id' a package, so it should use the default package + // 'android', and not be converted from 'android' to 'com.lib'. + ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010001u }, + ResourceId{ 0x01020000u })); +} + +/* + * The device ResXMLParser in libandroidfw differentiates between empty namespace and null + * namespace. + */ +TEST_F(XmlFlattenerTest, NoNamespaceIsNotTheSameAsEmptyNamespace) { + std::string input = "<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " package=\"android\"/>"; + + ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + } + + const StringPiece16 kPackage = u"package"; + EXPECT_GE(tree.indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()), 0); +} + +} // namespace xml +} // namespace aapt diff --git a/tools/aapt2/XmlPullParser.h b/tools/aapt2/XmlPullParser.h new file mode 100644 index 0000000..accfd30 --- /dev/null +++ b/tools/aapt2/XmlPullParser.h @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef AAPT_XML_PULL_PARSER_H +#define AAPT_XML_PULL_PARSER_H + +#include <algorithm> +#include <ostream> +#include <string> +#include <vector> + +#include "StringPiece.h" + +namespace aapt { + +class XmlPullParser { +public: + enum class Event { + kBadDocument, + kStartDocument, + kEndDocument, + + kStartNamespace, + kEndNamespace, + kStartElement, + kEndElement, + kText, + kComment, + }; + + static void skipCurrentElement(XmlPullParser* parser); + static bool isGoodEvent(Event event); + + virtual ~XmlPullParser() {} + + /** + * Returns the current event that is being processed. + */ + virtual Event getEvent() const = 0; + + virtual const std::string& getLastError() const = 0; + + /** + * Note, unlike XmlPullParser, the first call to next() will return + * StartElement of the first element. + */ + virtual Event next() = 0; + + // + // These are available for all nodes. + // + + virtual const std::u16string& getComment() const = 0; + virtual size_t getLineNumber() const = 0; + virtual size_t getDepth() const = 0; + + /** + * Returns the character data for a Text event. + */ + virtual const std::u16string& getText() const = 0; + + // + // Namespace prefix and URI are available for StartNamespace and EndNamespace. + // + + virtual const std::u16string& getNamespacePrefix() const = 0; + virtual const std::u16string& getNamespaceUri() const = 0; + + /* + * Uses the current stack of namespaces to resolve the package. Eg: + * xmlns:app = "http://schemas.android.com/apk/res/com.android.app" + * ... + * android:text="@app:string/message" + * + * In this case, 'app' will be converted to 'com.android.app'. + * + * If xmlns:app="http://schemas.android.com/apk/res-auto", then + * 'package' will be set to 'defaultPackage'. + */ + virtual bool applyPackageAlias(std::u16string* package, + const std::u16string& defaultPackage) const = 0; + + // + // These are available for StartElement and EndElement. + // + + virtual const std::u16string& getElementNamespace() const = 0; + virtual const std::u16string& getElementName() const = 0; + + // + // Remaining methods are for retrieving information about attributes + // associated with a StartElement. + // + // Attributes must be in sorted order (according to the less than operator + // of struct Attribute). + // + + struct Attribute { + std::u16string namespaceUri; + std::u16string name; + std::u16string value; + + int compare(const Attribute& rhs) const; + bool operator<(const Attribute& rhs) const; + bool operator==(const Attribute& rhs) const; + bool operator!=(const Attribute& rhs) const; + }; + + using const_iterator = std::vector<Attribute>::const_iterator; + + virtual const_iterator beginAttributes() const = 0; + virtual const_iterator endAttributes() const = 0; + virtual size_t getAttributeCount() const = 0; + const_iterator findAttribute(StringPiece16 namespaceUri, StringPiece16 name) const; +}; + +// +// Implementation +// + +inline ::std::ostream& operator<<(::std::ostream& out, XmlPullParser::Event event) { + switch (event) { + case XmlPullParser::Event::kBadDocument: return out << "BadDocument"; + case XmlPullParser::Event::kStartDocument: return out << "StartDocument"; + case XmlPullParser::Event::kEndDocument: return out << "EndDocument"; + case XmlPullParser::Event::kStartNamespace: return out << "StartNamespace"; + case XmlPullParser::Event::kEndNamespace: return out << "EndNamespace"; + case XmlPullParser::Event::kStartElement: return out << "StartElement"; + case XmlPullParser::Event::kEndElement: return out << "EndElement"; + case XmlPullParser::Event::kText: return out << "Text"; + case XmlPullParser::Event::kComment: return out << "Comment"; + } + return out; +} + +inline void XmlPullParser::skipCurrentElement(XmlPullParser* parser) { + int depth = 1; + while (depth > 0) { + switch (parser->next()) { + case Event::kEndDocument: + case Event::kBadDocument: + return; + case Event::kStartElement: + depth++; + break; + case Event::kEndElement: + depth--; + break; + default: + break; + } + } +} + +inline bool XmlPullParser::isGoodEvent(XmlPullParser::Event event) { + return event != Event::kBadDocument && event != Event::kEndDocument; +} + +inline int XmlPullParser::Attribute::compare(const Attribute& rhs) const { + int cmp = namespaceUri.compare(rhs.namespaceUri); + if (cmp != 0) return cmp; + return name.compare(rhs.name); +} + +inline bool XmlPullParser::Attribute::operator<(const Attribute& rhs) const { + return compare(rhs) < 0; +} + +inline bool XmlPullParser::Attribute::operator==(const Attribute& rhs) const { + return compare(rhs) == 0; +} + +inline bool XmlPullParser::Attribute::operator!=(const Attribute& rhs) const { + return compare(rhs) != 0; +} + +inline XmlPullParser::const_iterator XmlPullParser::findAttribute(StringPiece16 namespaceUri, + StringPiece16 name) const { + const auto endIter = endAttributes(); + const auto iter = std::lower_bound(beginAttributes(), endIter, + std::pair<StringPiece16, StringPiece16>(namespaceUri, name), + [](const Attribute& attr, const std::pair<StringPiece16, StringPiece16>& rhs) -> bool { + int cmp = attr.namespaceUri.compare(0, attr.namespaceUri.size(), + rhs.first.data(), rhs.first.size()); + if (cmp < 0) return true; + if (cmp > 0) return false; + cmp = attr.name.compare(0, attr.name.size(), rhs.second.data(), rhs.second.size()); + if (cmp < 0) return true; + return false; + } + ); + + if (iter != endIter && namespaceUri == iter->namespaceUri && name == iter->name) { + return iter; + } + return endIter; +} + +} // namespace aapt + +#endif // AAPT_XML_PULL_PARSER_H diff --git a/tools/aapt2/ZipEntry.cpp b/tools/aapt2/ZipEntry.cpp new file mode 100644 index 0000000..891b4e1 --- /dev/null +++ b/tools/aapt2/ZipEntry.cpp @@ -0,0 +1,745 @@ +/* + * Copyright (C) 2006 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. + */ + +// +// Access to entries in a Zip archive. +// + +#define LOG_TAG "zip" + +#include "ZipEntry.h" +#include <utils/Log.h> + +#include <stdio.h> +#include <string.h> +#include <assert.h> + +namespace aapt { + +using namespace android; + +/* + * Initialize a new ZipEntry structure from a FILE* positioned at a + * CentralDirectoryEntry. + * + * On exit, the file pointer will be at the start of the next CDE or + * at the EOCD. + */ +status_t ZipEntry::initFromCDE(FILE* fp) +{ + status_t result; + long posn; + bool hasDD; + + //ALOGV("initFromCDE ---\n"); + + /* read the CDE */ + result = mCDE.read(fp); + if (result != NO_ERROR) { + ALOGD("mCDE.read failed\n"); + return result; + } + + //mCDE.dump(); + + /* using the info in the CDE, go load up the LFH */ + posn = ftell(fp); + if (fseek(fp, mCDE.mLocalHeaderRelOffset, SEEK_SET) != 0) { + ALOGD("local header seek failed (%ld)\n", + mCDE.mLocalHeaderRelOffset); + return UNKNOWN_ERROR; + } + + result = mLFH.read(fp); + if (result != NO_ERROR) { + ALOGD("mLFH.read failed\n"); + return result; + } + + if (fseek(fp, posn, SEEK_SET) != 0) + return UNKNOWN_ERROR; + + //mLFH.dump(); + + /* + * We *might* need to read the Data Descriptor at this point and + * integrate it into the LFH. If this bit is set, the CRC-32, + * compressed size, and uncompressed size will be zero. In practice + * these seem to be rare. + */ + hasDD = (mLFH.mGPBitFlag & kUsesDataDescr) != 0; + if (hasDD) { + // do something clever + //ALOGD("+++ has data descriptor\n"); + } + + /* + * Sanity-check the LFH. Note that this will fail if the "kUsesDataDescr" + * flag is set, because the LFH is incomplete. (Not a problem, since we + * prefer the CDE values.) + */ + if (!hasDD && !compareHeaders()) { + ALOGW("warning: header mismatch\n"); + // keep going? + } + + /* + * If the mVersionToExtract is greater than 20, we may have an + * issue unpacking the record -- could be encrypted, compressed + * with something we don't support, or use Zip64 extensions. We + * can defer worrying about that to when we're extracting data. + */ + + return NO_ERROR; +} + +/* + * Initialize a new entry. Pass in the file name and an optional comment. + * + * Initializes the CDE and the LFH. + */ +void ZipEntry::initNew(const char* fileName, const char* comment) +{ + assert(fileName != NULL && *fileName != '\0'); // name required + + /* most fields are properly initialized by constructor */ + mCDE.mVersionMadeBy = kDefaultMadeBy; + mCDE.mVersionToExtract = kDefaultVersion; + mCDE.mCompressionMethod = kCompressStored; + mCDE.mFileNameLength = strlen(fileName); + if (comment != NULL) + mCDE.mFileCommentLength = strlen(comment); + mCDE.mExternalAttrs = 0x81b60020; // matches what WinZip does + + if (mCDE.mFileNameLength > 0) { + mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1]; + strcpy((char*) mCDE.mFileName, fileName); + } + if (mCDE.mFileCommentLength > 0) { + /* TODO: stop assuming null-terminated ASCII here? */ + mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1]; + strcpy((char*) mCDE.mFileComment, comment); + } + + copyCDEtoLFH(); +} + +/* + * Initialize a new entry, starting with the ZipEntry from a different + * archive. + * + * Initializes the CDE and the LFH. + */ +status_t ZipEntry::initFromExternal(const ZipFile* /* pZipFile */, + const ZipEntry* pEntry, const char* storageName) +{ + mCDE = pEntry->mCDE; + if (storageName && *storageName != 0) { + mCDE.mFileNameLength = strlen(storageName); + mCDE.mFileName = new unsigned char[mCDE.mFileNameLength + 1]; + strcpy((char*) mCDE.mFileName, storageName); + } + + // Check whether we got all the memory needed. + if ((mCDE.mFileNameLength > 0 && mCDE.mFileName == NULL) || + (mCDE.mFileCommentLength > 0 && mCDE.mFileComment == NULL) || + (mCDE.mExtraFieldLength > 0 && mCDE.mExtraField == NULL)) { + return NO_MEMORY; + } + + /* construct the LFH from the CDE */ + copyCDEtoLFH(); + + /* + * The LFH "extra" field is independent of the CDE "extra", so we + * handle it here. + */ + assert(mLFH.mExtraField == NULL); + mLFH.mExtraFieldLength = pEntry->mLFH.mExtraFieldLength; + if (mLFH.mExtraFieldLength > 0) { + mLFH.mExtraField = new unsigned char[mLFH.mExtraFieldLength+1]; + if (mLFH.mExtraField == NULL) + return NO_MEMORY; + memcpy(mLFH.mExtraField, pEntry->mLFH.mExtraField, + mLFH.mExtraFieldLength+1); + } + + return NO_ERROR; +} + +/* + * Insert pad bytes in the LFH by tweaking the "extra" field. This will + * potentially confuse something that put "extra" data in here earlier, + * but I can't find an actual problem. + */ +status_t ZipEntry::addPadding(int padding) +{ + if (padding <= 0) + return INVALID_OPERATION; + + //ALOGI("HEY: adding %d pad bytes to existing %d in %s\n", + // padding, mLFH.mExtraFieldLength, mCDE.mFileName); + + if (mLFH.mExtraFieldLength > 0) { + /* extend existing field */ + unsigned char* newExtra; + + newExtra = new unsigned char[mLFH.mExtraFieldLength + padding]; + if (newExtra == NULL) + return NO_MEMORY; + memset(newExtra + mLFH.mExtraFieldLength, 0, padding); + memcpy(newExtra, mLFH.mExtraField, mLFH.mExtraFieldLength); + + delete[] mLFH.mExtraField; + mLFH.mExtraField = newExtra; + mLFH.mExtraFieldLength += padding; + } else { + /* create new field */ + mLFH.mExtraField = new unsigned char[padding]; + memset(mLFH.mExtraField, 0, padding); + mLFH.mExtraFieldLength = padding; + } + + return NO_ERROR; +} + +/* + * Set the fields in the LFH equal to the corresponding fields in the CDE. + * + * This does not touch the LFH "extra" field. + */ +void ZipEntry::copyCDEtoLFH(void) +{ + mLFH.mVersionToExtract = mCDE.mVersionToExtract; + mLFH.mGPBitFlag = mCDE.mGPBitFlag; + mLFH.mCompressionMethod = mCDE.mCompressionMethod; + mLFH.mLastModFileTime = mCDE.mLastModFileTime; + mLFH.mLastModFileDate = mCDE.mLastModFileDate; + mLFH.mCRC32 = mCDE.mCRC32; + mLFH.mCompressedSize = mCDE.mCompressedSize; + mLFH.mUncompressedSize = mCDE.mUncompressedSize; + mLFH.mFileNameLength = mCDE.mFileNameLength; + // the "extra field" is independent + + delete[] mLFH.mFileName; + if (mLFH.mFileNameLength > 0) { + mLFH.mFileName = new unsigned char[mLFH.mFileNameLength+1]; + strcpy((char*) mLFH.mFileName, (const char*) mCDE.mFileName); + } else { + mLFH.mFileName = NULL; + } +} + +/* + * Set some information about a file after we add it. + */ +void ZipEntry::setDataInfo(long uncompLen, long compLen, unsigned long crc32, + int compressionMethod) +{ + mCDE.mCompressionMethod = compressionMethod; + mCDE.mCRC32 = crc32; + mCDE.mCompressedSize = compLen; + mCDE.mUncompressedSize = uncompLen; + mCDE.mCompressionMethod = compressionMethod; + if (compressionMethod == kCompressDeflated) { + mCDE.mGPBitFlag |= 0x0002; // indicates maximum compression used + } + copyCDEtoLFH(); +} + +/* + * See if the data in mCDE and mLFH match up. This is mostly useful for + * debugging these classes, but it can be used to identify damaged + * archives. + * + * Returns "false" if they differ. + */ +bool ZipEntry::compareHeaders(void) const +{ + if (mCDE.mVersionToExtract != mLFH.mVersionToExtract) { + ALOGV("cmp: VersionToExtract\n"); + return false; + } + if (mCDE.mGPBitFlag != mLFH.mGPBitFlag) { + ALOGV("cmp: GPBitFlag\n"); + return false; + } + if (mCDE.mCompressionMethod != mLFH.mCompressionMethod) { + ALOGV("cmp: CompressionMethod\n"); + return false; + } + if (mCDE.mLastModFileTime != mLFH.mLastModFileTime) { + ALOGV("cmp: LastModFileTime\n"); + return false; + } + if (mCDE.mLastModFileDate != mLFH.mLastModFileDate) { + ALOGV("cmp: LastModFileDate\n"); + return false; + } + if (mCDE.mCRC32 != mLFH.mCRC32) { + ALOGV("cmp: CRC32\n"); + return false; + } + if (mCDE.mCompressedSize != mLFH.mCompressedSize) { + ALOGV("cmp: CompressedSize\n"); + return false; + } + if (mCDE.mUncompressedSize != mLFH.mUncompressedSize) { + ALOGV("cmp: UncompressedSize\n"); + return false; + } + if (mCDE.mFileNameLength != mLFH.mFileNameLength) { + ALOGV("cmp: FileNameLength\n"); + return false; + } +#if 0 // this seems to be used for padding, not real data + if (mCDE.mExtraFieldLength != mLFH.mExtraFieldLength) { + ALOGV("cmp: ExtraFieldLength\n"); + return false; + } +#endif + if (mCDE.mFileName != NULL) { + if (strcmp((char*) mCDE.mFileName, (char*) mLFH.mFileName) != 0) { + ALOGV("cmp: FileName\n"); + return false; + } + } + + return true; +} + + +/* + * Convert the DOS date/time stamp into a UNIX time stamp. + */ +time_t ZipEntry::getModWhen(void) const +{ + struct tm parts; + + parts.tm_sec = (mCDE.mLastModFileTime & 0x001f) << 1; + parts.tm_min = (mCDE.mLastModFileTime & 0x07e0) >> 5; + parts.tm_hour = (mCDE.mLastModFileTime & 0xf800) >> 11; + parts.tm_mday = (mCDE.mLastModFileDate & 0x001f); + parts.tm_mon = ((mCDE.mLastModFileDate & 0x01e0) >> 5) -1; + parts.tm_year = ((mCDE.mLastModFileDate & 0xfe00) >> 9) + 80; + parts.tm_wday = parts.tm_yday = 0; + parts.tm_isdst = -1; // DST info "not available" + + return mktime(&parts); +} + +/* + * Set the CDE/LFH timestamp from UNIX time. + */ +void ZipEntry::setModWhen(time_t when) +{ +#if !defined(_WIN32) + struct tm tmResult; +#endif + time_t even; + unsigned short zdate, ztime; + + struct tm* ptm; + + /* round up to an even number of seconds */ + even = (time_t)(((unsigned long)(when) + 1) & (~1)); + + /* expand */ +#if !defined(_WIN32) + ptm = localtime_r(&even, &tmResult); +#else + ptm = localtime(&even); +#endif + + int year; + year = ptm->tm_year; + if (year < 80) + year = 80; + + zdate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; + ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1; + + mCDE.mLastModFileTime = mLFH.mLastModFileTime = ztime; + mCDE.mLastModFileDate = mLFH.mLastModFileDate = zdate; +} + + +/* + * =========================================================================== + * ZipEntry::LocalFileHeader + * =========================================================================== + */ + +/* + * Read a local file header. + * + * On entry, "fp" points to the signature at the start of the header. + * On exit, "fp" points to the start of data. + */ +status_t ZipEntry::LocalFileHeader::read(FILE* fp) +{ + status_t result = NO_ERROR; + unsigned char buf[kLFHLen]; + + assert(mFileName == NULL); + assert(mExtraField == NULL); + + if (fread(buf, 1, kLFHLen, fp) != kLFHLen) { + result = UNKNOWN_ERROR; + goto bail; + } + + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { + ALOGD("whoops: didn't find expected signature\n"); + result = UNKNOWN_ERROR; + goto bail; + } + + mVersionToExtract = ZipEntry::getShortLE(&buf[0x04]); + mGPBitFlag = ZipEntry::getShortLE(&buf[0x06]); + mCompressionMethod = ZipEntry::getShortLE(&buf[0x08]); + mLastModFileTime = ZipEntry::getShortLE(&buf[0x0a]); + mLastModFileDate = ZipEntry::getShortLE(&buf[0x0c]); + mCRC32 = ZipEntry::getLongLE(&buf[0x0e]); + mCompressedSize = ZipEntry::getLongLE(&buf[0x12]); + mUncompressedSize = ZipEntry::getLongLE(&buf[0x16]); + mFileNameLength = ZipEntry::getShortLE(&buf[0x1a]); + mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1c]); + + // TODO: validate sizes + + /* grab filename */ + if (mFileNameLength != 0) { + mFileName = new unsigned char[mFileNameLength+1]; + if (mFileName == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mFileName[mFileNameLength] = '\0'; + } + + /* grab extra field */ + if (mExtraFieldLength != 0) { + mExtraField = new unsigned char[mExtraFieldLength+1]; + if (mExtraField == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mExtraField[mExtraFieldLength] = '\0'; + } + +bail: + return result; +} + +/* + * Write a local file header. + */ +status_t ZipEntry::LocalFileHeader::write(FILE* fp) +{ + unsigned char buf[kLFHLen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mVersionToExtract); + ZipEntry::putShortLE(&buf[0x06], mGPBitFlag); + ZipEntry::putShortLE(&buf[0x08], mCompressionMethod); + ZipEntry::putShortLE(&buf[0x0a], mLastModFileTime); + ZipEntry::putShortLE(&buf[0x0c], mLastModFileDate); + ZipEntry::putLongLE(&buf[0x0e], mCRC32); + ZipEntry::putLongLE(&buf[0x12], mCompressedSize); + ZipEntry::putLongLE(&buf[0x16], mUncompressedSize); + ZipEntry::putShortLE(&buf[0x1a], mFileNameLength); + ZipEntry::putShortLE(&buf[0x1c], mExtraFieldLength); + + if (fwrite(buf, 1, kLFHLen, fp) != kLFHLen) + return UNKNOWN_ERROR; + + /* write filename */ + if (mFileNameLength != 0) { + if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) + return UNKNOWN_ERROR; + } + + /* write "extra field" */ + if (mExtraFieldLength != 0) { + if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + + +/* + * Dump the contents of a LocalFileHeader object. + */ +void ZipEntry::LocalFileHeader::dump(void) const +{ + ALOGD(" LocalFileHeader contents:\n"); + ALOGD(" versToExt=%u gpBits=0x%04x compression=%u\n", + mVersionToExtract, mGPBitFlag, mCompressionMethod); + ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + mLastModFileTime, mLastModFileDate, mCRC32); + ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", + mCompressedSize, mUncompressedSize); + ALOGD(" filenameLen=%u extraLen=%u\n", + mFileNameLength, mExtraFieldLength); + if (mFileName != NULL) + ALOGD(" filename: '%s'\n", mFileName); +} + + +/* + * =========================================================================== + * ZipEntry::CentralDirEntry + * =========================================================================== + */ + +/* + * Read the central dir entry that appears next in the file. + * + * On entry, "fp" should be positioned on the signature bytes for the + * entry. On exit, "fp" will point at the signature word for the next + * entry or for the EOCD. + */ +status_t ZipEntry::CentralDirEntry::read(FILE* fp) +{ + status_t result = NO_ERROR; + unsigned char buf[kCDELen]; + + /* no re-use */ + assert(mFileName == NULL); + assert(mExtraField == NULL); + assert(mFileComment == NULL); + + if (fread(buf, 1, kCDELen, fp) != kCDELen) { + result = UNKNOWN_ERROR; + goto bail; + } + + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { + ALOGD("Whoops: didn't find expected signature\n"); + result = UNKNOWN_ERROR; + goto bail; + } + + mVersionMadeBy = ZipEntry::getShortLE(&buf[0x04]); + mVersionToExtract = ZipEntry::getShortLE(&buf[0x06]); + mGPBitFlag = ZipEntry::getShortLE(&buf[0x08]); + mCompressionMethod = ZipEntry::getShortLE(&buf[0x0a]); + mLastModFileTime = ZipEntry::getShortLE(&buf[0x0c]); + mLastModFileDate = ZipEntry::getShortLE(&buf[0x0e]); + mCRC32 = ZipEntry::getLongLE(&buf[0x10]); + mCompressedSize = ZipEntry::getLongLE(&buf[0x14]); + mUncompressedSize = ZipEntry::getLongLE(&buf[0x18]); + mFileNameLength = ZipEntry::getShortLE(&buf[0x1c]); + mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1e]); + mFileCommentLength = ZipEntry::getShortLE(&buf[0x20]); + mDiskNumberStart = ZipEntry::getShortLE(&buf[0x22]); + mInternalAttrs = ZipEntry::getShortLE(&buf[0x24]); + mExternalAttrs = ZipEntry::getLongLE(&buf[0x26]); + mLocalHeaderRelOffset = ZipEntry::getLongLE(&buf[0x2a]); + + // TODO: validate sizes and offsets + + /* grab filename */ + if (mFileNameLength != 0) { + mFileName = new unsigned char[mFileNameLength+1]; + if (mFileName == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mFileName[mFileNameLength] = '\0'; + } + + /* read "extra field" */ + if (mExtraFieldLength != 0) { + mExtraField = new unsigned char[mExtraFieldLength+1]; + if (mExtraField == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mExtraField[mExtraFieldLength] = '\0'; + } + + + /* grab comment, if any */ + if (mFileCommentLength != 0) { + mFileComment = new unsigned char[mFileCommentLength+1]; + if (mFileComment == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) + { + result = UNKNOWN_ERROR; + goto bail; + } + mFileComment[mFileCommentLength] = '\0'; + } + +bail: + return result; +} + +/* + * Write a central dir entry. + */ +status_t ZipEntry::CentralDirEntry::write(FILE* fp) +{ + unsigned char buf[kCDELen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mVersionMadeBy); + ZipEntry::putShortLE(&buf[0x06], mVersionToExtract); + ZipEntry::putShortLE(&buf[0x08], mGPBitFlag); + ZipEntry::putShortLE(&buf[0x0a], mCompressionMethod); + ZipEntry::putShortLE(&buf[0x0c], mLastModFileTime); + ZipEntry::putShortLE(&buf[0x0e], mLastModFileDate); + ZipEntry::putLongLE(&buf[0x10], mCRC32); + ZipEntry::putLongLE(&buf[0x14], mCompressedSize); + ZipEntry::putLongLE(&buf[0x18], mUncompressedSize); + ZipEntry::putShortLE(&buf[0x1c], mFileNameLength); + ZipEntry::putShortLE(&buf[0x1e], mExtraFieldLength); + ZipEntry::putShortLE(&buf[0x20], mFileCommentLength); + ZipEntry::putShortLE(&buf[0x22], mDiskNumberStart); + ZipEntry::putShortLE(&buf[0x24], mInternalAttrs); + ZipEntry::putLongLE(&buf[0x26], mExternalAttrs); + ZipEntry::putLongLE(&buf[0x2a], mLocalHeaderRelOffset); + + if (fwrite(buf, 1, kCDELen, fp) != kCDELen) + return UNKNOWN_ERROR; + + /* write filename */ + if (mFileNameLength != 0) { + if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) + return UNKNOWN_ERROR; + } + + /* write "extra field" */ + if (mExtraFieldLength != 0) { + if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) + return UNKNOWN_ERROR; + } + + /* write comment */ + if (mFileCommentLength != 0) { + if (fwrite(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +/* + * Dump the contents of a CentralDirEntry object. + */ +void ZipEntry::CentralDirEntry::dump(void) const +{ + ALOGD(" CentralDirEntry contents:\n"); + ALOGD(" versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n", + mVersionMadeBy, mVersionToExtract, mGPBitFlag, mCompressionMethod); + ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + mLastModFileTime, mLastModFileDate, mCRC32); + ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", + mCompressedSize, mUncompressedSize); + ALOGD(" filenameLen=%u extraLen=%u commentLen=%u\n", + mFileNameLength, mExtraFieldLength, mFileCommentLength); + ALOGD(" diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n", + mDiskNumberStart, mInternalAttrs, mExternalAttrs, + mLocalHeaderRelOffset); + + if (mFileName != NULL) + ALOGD(" filename: '%s'\n", mFileName); + if (mFileComment != NULL) + ALOGD(" comment: '%s'\n", mFileComment); +} + +/* + * Copy-assignment operator for CentralDirEntry. + */ +ZipEntry::CentralDirEntry& ZipEntry::CentralDirEntry::operator=(const ZipEntry::CentralDirEntry& src) { + if (this == &src) { + return *this; + } + + // Free up old data. + delete[] mFileName; + delete[] mExtraField; + delete[] mFileComment; + + // Copy scalars. + mVersionMadeBy = src.mVersionMadeBy; + mVersionToExtract = src.mVersionToExtract; + mGPBitFlag = src.mGPBitFlag; + mCompressionMethod = src.mCompressionMethod; + mLastModFileTime = src.mLastModFileTime; + mLastModFileDate = src.mLastModFileDate; + mCRC32 = src.mCRC32; + mCompressedSize = src.mCompressedSize; + mUncompressedSize = src.mUncompressedSize; + mFileNameLength = src.mFileNameLength; + mExtraFieldLength = src.mExtraFieldLength; + mFileCommentLength = src.mFileCommentLength; + mDiskNumberStart = src.mDiskNumberStart; + mInternalAttrs = src.mInternalAttrs; + mExternalAttrs = src.mExternalAttrs; + mLocalHeaderRelOffset = src.mLocalHeaderRelOffset; + + // Copy strings, if necessary. + if (mFileNameLength > 0) { + mFileName = new unsigned char[mFileNameLength + 1]; + if (mFileName != NULL) + strcpy((char*)mFileName, (char*)src.mFileName); + } else { + mFileName = NULL; + } + if (mFileCommentLength > 0) { + mFileComment = new unsigned char[mFileCommentLength + 1]; + if (mFileComment != NULL) + strcpy((char*)mFileComment, (char*)src.mFileComment); + } else { + mFileComment = NULL; + } + if (mExtraFieldLength > 0) { + /* we null-terminate this, though it may not be a string */ + mExtraField = new unsigned char[mExtraFieldLength + 1]; + if (mExtraField != NULL) + memcpy(mExtraField, src.mExtraField, mExtraFieldLength + 1); + } else { + mExtraField = NULL; + } + + return *this; +} + +} // namespace aapt diff --git a/tools/aapt2/ZipEntry.h b/tools/aapt2/ZipEntry.h new file mode 100644 index 0000000..2745a43 --- /dev/null +++ b/tools/aapt2/ZipEntry.h @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2006 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. + */ + +// +// Zip archive entries. +// +// The ZipEntry class is tightly meshed with the ZipFile class. +// +#ifndef __LIBS_ZIPENTRY_H +#define __LIBS_ZIPENTRY_H + +#include <utils/Errors.h> + +#include <stdlib.h> +#include <stdio.h> + +namespace aapt { + +using android::status_t; + +class ZipFile; + +/* + * ZipEntry objects represent a single entry in a Zip archive. + * + * You can use one of these to get or set information about an entry, but + * there are no functions here for accessing the data itself. (We could + * tuck a pointer to the ZipFile in here for convenience, but that raises + * the likelihood of using ZipEntry objects after discarding the ZipFile.) + * + * File information is stored in two places: next to the file data (the Local + * File Header, and possibly a Data Descriptor), and at the end of the file + * (the Central Directory Entry). The two must be kept in sync. + */ +class ZipEntry { +public: + friend class ZipFile; + + ZipEntry(void) + : mDeleted(false), mMarked(false) + {} + ~ZipEntry(void) {} + + /* + * Returns "true" if the data is compressed. + */ + bool isCompressed(void) const { + return mCDE.mCompressionMethod != kCompressStored; + } + int getCompressionMethod(void) const { return mCDE.mCompressionMethod; } + + /* + * Return the uncompressed length. + */ + off_t getUncompressedLen(void) const { return mCDE.mUncompressedSize; } + + /* + * Return the compressed length. For uncompressed data, this returns + * the same thing as getUncompresesdLen(). + */ + off_t getCompressedLen(void) const { return mCDE.mCompressedSize; } + + /* + * Return the offset of the local file header. + */ + off_t getLFHOffset(void) const { return mCDE.mLocalHeaderRelOffset; } + + /* + * Return the absolute file offset of the start of the compressed or + * uncompressed data. + */ + off_t getFileOffset(void) const { + return mCDE.mLocalHeaderRelOffset + + LocalFileHeader::kLFHLen + + mLFH.mFileNameLength + + mLFH.mExtraFieldLength; + } + + /* + * Return the data CRC. + */ + unsigned long getCRC32(void) const { return mCDE.mCRC32; } + + /* + * Return file modification time in UNIX seconds-since-epoch. + */ + time_t getModWhen(void) const; + + /* + * Return the archived file name. + */ + const char* getFileName(void) const { return (const char*) mCDE.mFileName; } + + /* + * Application-defined "mark". Can be useful when synchronizing the + * contents of an archive with contents on disk. + */ + bool getMarked(void) const { return mMarked; } + void setMarked(bool val) { mMarked = val; } + + /* + * Some basic functions for raw data manipulation. "LE" means + * Little Endian. + */ + static inline unsigned short getShortLE(const unsigned char* buf) { + return buf[0] | (buf[1] << 8); + } + static inline unsigned long getLongLE(const unsigned char* buf) { + return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); + } + static inline void putShortLE(unsigned char* buf, short val) { + buf[0] = (unsigned char) val; + buf[1] = (unsigned char) (val >> 8); + } + static inline void putLongLE(unsigned char* buf, long val) { + buf[0] = (unsigned char) val; + buf[1] = (unsigned char) (val >> 8); + buf[2] = (unsigned char) (val >> 16); + buf[3] = (unsigned char) (val >> 24); + } + + /* defined for Zip archives */ + enum { + kCompressStored = 0, // no compression + // shrunk = 1, + // reduced 1 = 2, + // reduced 2 = 3, + // reduced 3 = 4, + // reduced 4 = 5, + // imploded = 6, + // tokenized = 7, + kCompressDeflated = 8, // standard deflate + // Deflate64 = 9, + // lib imploded = 10, + // reserved = 11, + // bzip2 = 12, + }; + + /* + * Deletion flag. If set, the entry will be removed on the next + * call to "flush". + */ + bool getDeleted(void) const { return mDeleted; } + +protected: + /* + * Initialize the structure from the file, which is pointing at + * our Central Directory entry. + */ + status_t initFromCDE(FILE* fp); + + /* + * Initialize the structure for a new file. We need the filename + * and comment so that we can properly size the LFH area. The + * filename is mandatory, the comment is optional. + */ + void initNew(const char* fileName, const char* comment); + + /* + * Initialize the structure with the contents of a ZipEntry from + * another file. If fileName is non-NULL, override the name with fileName. + */ + status_t initFromExternal(const ZipFile* pZipFile, const ZipEntry* pEntry, + const char* fileName); + + /* + * Add some pad bytes to the LFH. We do this by adding or resizing + * the "extra" field. + */ + status_t addPadding(int padding); + + /* + * Set information about the data for this entry. + */ + void setDataInfo(long uncompLen, long compLen, unsigned long crc32, + int compressionMethod); + + /* + * Set the modification date. + */ + void setModWhen(time_t when); + + /* + * Set the offset of the local file header, relative to the start of + * the current file. + */ + void setLFHOffset(off_t offset) { + mCDE.mLocalHeaderRelOffset = (long) offset; + } + + /* mark for deletion; used by ZipFile::remove() */ + void setDeleted(void) { mDeleted = true; } + +private: + /* these are private and not defined */ + ZipEntry(const ZipEntry& src); + ZipEntry& operator=(const ZipEntry& src); + + /* returns "true" if the CDE and the LFH agree */ + bool compareHeaders(void) const; + void copyCDEtoLFH(void); + + bool mDeleted; // set if entry is pending deletion + bool mMarked; // app-defined marker + + /* + * Every entry in the Zip archive starts off with one of these. + */ + class LocalFileHeader { + public: + LocalFileHeader(void) : + mVersionToExtract(0), + mGPBitFlag(0), + mCompressionMethod(0), + mLastModFileTime(0), + mLastModFileDate(0), + mCRC32(0), + mCompressedSize(0), + mUncompressedSize(0), + mFileNameLength(0), + mExtraFieldLength(0), + mFileName(NULL), + mExtraField(NULL) + {} + virtual ~LocalFileHeader(void) { + delete[] mFileName; + delete[] mExtraField; + } + + status_t read(FILE* fp); + status_t write(FILE* fp); + + // unsigned long mSignature; + unsigned short mVersionToExtract; + unsigned short mGPBitFlag; + unsigned short mCompressionMethod; + unsigned short mLastModFileTime; + unsigned short mLastModFileDate; + unsigned long mCRC32; + unsigned long mCompressedSize; + unsigned long mUncompressedSize; + unsigned short mFileNameLength; + unsigned short mExtraFieldLength; + unsigned char* mFileName; + unsigned char* mExtraField; + + enum { + kSignature = 0x04034b50, + kLFHLen = 30, // LocalFileHdr len, excl. var fields + }; + + void dump(void) const; + }; + + /* + * Every entry in the Zip archive has one of these in the "central + * directory" at the end of the file. + */ + class CentralDirEntry { + public: + CentralDirEntry(void) : + mVersionMadeBy(0), + mVersionToExtract(0), + mGPBitFlag(0), + mCompressionMethod(0), + mLastModFileTime(0), + mLastModFileDate(0), + mCRC32(0), + mCompressedSize(0), + mUncompressedSize(0), + mFileNameLength(0), + mExtraFieldLength(0), + mFileCommentLength(0), + mDiskNumberStart(0), + mInternalAttrs(0), + mExternalAttrs(0), + mLocalHeaderRelOffset(0), + mFileName(NULL), + mExtraField(NULL), + mFileComment(NULL) + {} + virtual ~CentralDirEntry(void) { + delete[] mFileName; + delete[] mExtraField; + delete[] mFileComment; + } + + status_t read(FILE* fp); + status_t write(FILE* fp); + + CentralDirEntry& operator=(const CentralDirEntry& src); + + // unsigned long mSignature; + unsigned short mVersionMadeBy; + unsigned short mVersionToExtract; + unsigned short mGPBitFlag; + unsigned short mCompressionMethod; + unsigned short mLastModFileTime; + unsigned short mLastModFileDate; + unsigned long mCRC32; + unsigned long mCompressedSize; + unsigned long mUncompressedSize; + unsigned short mFileNameLength; + unsigned short mExtraFieldLength; + unsigned short mFileCommentLength; + unsigned short mDiskNumberStart; + unsigned short mInternalAttrs; + unsigned long mExternalAttrs; + unsigned long mLocalHeaderRelOffset; + unsigned char* mFileName; + unsigned char* mExtraField; + unsigned char* mFileComment; + + void dump(void) const; + + enum { + kSignature = 0x02014b50, + kCDELen = 46, // CentralDirEnt len, excl. var fields + }; + }; + + enum { + //kDataDescriptorSignature = 0x08074b50, // currently unused + kDataDescriptorLen = 16, // four 32-bit fields + + kDefaultVersion = 20, // need deflate, nothing much else + kDefaultMadeBy = 0x0317, // 03=UNIX, 17=spec v2.3 + kUsesDataDescr = 0x0008, // GPBitFlag bit 3 + }; + + LocalFileHeader mLFH; + CentralDirEntry mCDE; +}; + +}; // namespace aapt + +#endif // __LIBS_ZIPENTRY_H diff --git a/tools/aapt2/ZipFile.cpp b/tools/aapt2/ZipFile.cpp new file mode 100644 index 0000000..268c15e --- /dev/null +++ b/tools/aapt2/ZipFile.cpp @@ -0,0 +1,1306 @@ +/* + * Copyright (C) 2006 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. + */ + +// +// Access to Zip archives. +// + +#define LOG_TAG "zip" + +#include <androidfw/ZipUtils.h> +#include <utils/Log.h> + +#include "ZipFile.h" +#include "Util.h" + +#include <zlib.h> +#define DEF_MEM_LEVEL 8 // normally in zutil.h? + +#include <memory.h> +#include <sys/stat.h> +#include <errno.h> +#include <assert.h> + +namespace aapt { + +using namespace android; + +/* + * Some environments require the "b", some choke on it. + */ +#define FILE_OPEN_RO "rb" +#define FILE_OPEN_RW "r+b" +#define FILE_OPEN_RW_CREATE "w+b" + +/* should live somewhere else? */ +static status_t errnoToStatus(int err) +{ + if (err == ENOENT) + return NAME_NOT_FOUND; + else if (err == EACCES) + return PERMISSION_DENIED; + else + return UNKNOWN_ERROR; +} + +/* + * Open a file and parse its guts. + */ +status_t ZipFile::open(const char* zipFileName, int flags) +{ + bool newArchive = false; + + assert(mZipFp == NULL); // no reopen + + if ((flags & kOpenTruncate)) + flags |= kOpenCreate; // trunc implies create + + if ((flags & kOpenReadOnly) && (flags & kOpenReadWrite)) + return INVALID_OPERATION; // not both + if (!((flags & kOpenReadOnly) || (flags & kOpenReadWrite))) + return INVALID_OPERATION; // not neither + if ((flags & kOpenCreate) && !(flags & kOpenReadWrite)) + return INVALID_OPERATION; // create requires write + + if (flags & kOpenTruncate) { + newArchive = true; + } else { + newArchive = (access(zipFileName, F_OK) != 0); + if (!(flags & kOpenCreate) && newArchive) { + /* not creating, must already exist */ + ALOGD("File %s does not exist", zipFileName); + return NAME_NOT_FOUND; + } + } + + /* open the file */ + const char* openflags; + if (flags & kOpenReadWrite) { + if (newArchive) + openflags = FILE_OPEN_RW_CREATE; + else + openflags = FILE_OPEN_RW; + } else { + openflags = FILE_OPEN_RO; + } + mZipFp = fopen(zipFileName, openflags); + if (mZipFp == NULL) { + int err = errno; + ALOGD("fopen failed: %d\n", err); + return errnoToStatus(err); + } + + status_t result; + if (!newArchive) { + /* + * Load the central directory. If that fails, then this probably + * isn't a Zip archive. + */ + result = readCentralDir(); + } else { + /* + * Newly-created. The EndOfCentralDir constructor actually + * sets everything to be the way we want it (all zeroes). We + * set mNeedCDRewrite so that we create *something* if the + * caller doesn't add any files. (We could also just unlink + * the file if it's brand new and nothing was added, but that's + * probably doing more than we really should -- the user might + * have a need for empty zip files.) + */ + mNeedCDRewrite = true; + result = NO_ERROR; + } + + if (flags & kOpenReadOnly) + mReadOnly = true; + else + assert(!mReadOnly); + + return result; +} + +/* + * Return the Nth entry in the archive. + */ +ZipEntry* ZipFile::getEntryByIndex(int idx) const +{ + if (idx < 0 || idx >= (int) mEntries.size()) + return NULL; + + return mEntries[idx]; +} + +/* + * Find an entry by name. + */ +ZipEntry* ZipFile::getEntryByName(const char* fileName) const +{ + /* + * Do a stupid linear string-compare search. + * + * There are various ways to speed this up, especially since it's rare + * to intermingle changes to the archive with "get by name" calls. We + * don't want to sort the mEntries vector itself, however, because + * it's used to recreate the Central Directory. + * + * (Hash table works, parallel list of pointers in sorted order is good.) + */ + int idx; + + for (idx = mEntries.size()-1; idx >= 0; idx--) { + ZipEntry* pEntry = mEntries[idx]; + if (!pEntry->getDeleted() && + strcmp(fileName, pEntry->getFileName()) == 0) + { + return pEntry; + } + } + + return NULL; +} + +/* + * Empty the mEntries vector. + */ +void ZipFile::discardEntries(void) +{ + int count = mEntries.size(); + + while (--count >= 0) + delete mEntries[count]; + + mEntries.clear(); +} + + +/* + * Find the central directory and read the contents. + * + * The fun thing about ZIP archives is that they may or may not be + * readable from start to end. In some cases, notably for archives + * that were written to stdout, the only length information is in the + * central directory at the end of the file. + * + * Of course, the central directory can be followed by a variable-length + * comment field, so we have to scan through it backwards. The comment + * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff + * itself, plus apparently sometimes people throw random junk on the end + * just for the fun of it. + * + * This is all a little wobbly. If the wrong value ends up in the EOCD + * area, we're hosed. This appears to be the way that everbody handles + * it though, so we're in pretty good company if this fails. + */ +status_t ZipFile::readCentralDir(void) +{ + status_t result = NO_ERROR; + unsigned char* buf = NULL; + off_t fileLength, seekStart; + long readAmount; + int i; + + fseek(mZipFp, 0, SEEK_END); + fileLength = ftell(mZipFp); + rewind(mZipFp); + + /* too small to be a ZIP archive? */ + if (fileLength < EndOfCentralDir::kEOCDLen) { + ALOGD("Length is %ld -- too small\n", (long)fileLength); + result = INVALID_OPERATION; + goto bail; + } + + buf = new unsigned char[EndOfCentralDir::kMaxEOCDSearch]; + if (buf == NULL) { + ALOGD("Failure allocating %d bytes for EOCD search", + EndOfCentralDir::kMaxEOCDSearch); + result = NO_MEMORY; + goto bail; + } + + if (fileLength > EndOfCentralDir::kMaxEOCDSearch) { + seekStart = fileLength - EndOfCentralDir::kMaxEOCDSearch; + readAmount = EndOfCentralDir::kMaxEOCDSearch; + } else { + seekStart = 0; + readAmount = (long) fileLength; + } + if (fseek(mZipFp, seekStart, SEEK_SET) != 0) { + ALOGD("Failure seeking to end of zip at %ld", (long) seekStart); + result = UNKNOWN_ERROR; + goto bail; + } + + /* read the last part of the file into the buffer */ + if (fread(buf, 1, readAmount, mZipFp) != (size_t) readAmount) { + ALOGD("short file? wanted %ld\n", readAmount); + result = UNKNOWN_ERROR; + goto bail; + } + + /* find the end-of-central-dir magic */ + for (i = readAmount - 4; i >= 0; i--) { + if (buf[i] == 0x50 && + ZipEntry::getLongLE(&buf[i]) == EndOfCentralDir::kSignature) + { + ALOGV("+++ Found EOCD at buf+%d\n", i); + break; + } + } + if (i < 0) { + ALOGD("EOCD not found, not Zip\n"); + result = INVALID_OPERATION; + goto bail; + } + + /* extract eocd values */ + result = mEOCD.readBuf(buf + i, readAmount - i); + if (result != NO_ERROR) { + ALOGD("Failure reading %ld bytes of EOCD values", readAmount - i); + goto bail; + } + //mEOCD.dump(); + + if (mEOCD.mDiskNumber != 0 || mEOCD.mDiskWithCentralDir != 0 || + mEOCD.mNumEntries != mEOCD.mTotalNumEntries) + { + ALOGD("Archive spanning not supported\n"); + result = INVALID_OPERATION; + goto bail; + } + + /* + * So far so good. "mCentralDirSize" is the size in bytes of the + * central directory, so we can just seek back that far to find it. + * We can also seek forward mCentralDirOffset bytes from the + * start of the file. + * + * We're not guaranteed to have the rest of the central dir in the + * buffer, nor are we guaranteed that the central dir will have any + * sort of convenient size. We need to skip to the start of it and + * read the header, then the other goodies. + * + * The only thing we really need right now is the file comment, which + * we're hoping to preserve. + */ + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + ALOGD("Failure seeking to central dir offset %ld\n", + mEOCD.mCentralDirOffset); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * Loop through and read the central dir entries. + */ + ALOGV("Scanning %d entries...\n", mEOCD.mTotalNumEntries); + int entry; + for (entry = 0; entry < mEOCD.mTotalNumEntries; entry++) { + ZipEntry* pEntry = new ZipEntry; + + result = pEntry->initFromCDE(mZipFp); + if (result != NO_ERROR) { + ALOGD("initFromCDE failed\n"); + delete pEntry; + goto bail; + } + + mEntries.push_back(pEntry); + } + + + /* + * If all went well, we should now be back at the EOCD. + */ + { + unsigned char checkBuf[4]; + if (fread(checkBuf, 1, 4, mZipFp) != 4) { + ALOGD("EOCD check read failed\n"); + result = INVALID_OPERATION; + goto bail; + } + if (ZipEntry::getLongLE(checkBuf) != EndOfCentralDir::kSignature) { + ALOGD("EOCD read check failed\n"); + result = UNKNOWN_ERROR; + goto bail; + } + ALOGV("+++ EOCD read check passed\n"); + } + +bail: + delete[] buf; + return result; +} + +status_t ZipFile::add(const BigBuffer& buffer, const char* storageName, int compressionMethod, + ZipEntry** ppEntry) { + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + return add(data.get(), buffer.size(), storageName, compressionMethod, ppEntry); +} + + +/* + * Add a new file to the archive. + * + * This requires creating and populating a ZipEntry structure, and copying + * the data into the file at the appropriate position. The "appropriate + * position" is the current location of the central directory, which we + * casually overwrite (we can put it back later). + * + * If we were concerned about safety, we would want to make all changes + * in a temp file and then overwrite the original after everything was + * safely written. Not really a concern for us. + */ +status_t ZipFile::addCommon(const char* fileName, const void* data, size_t size, + const char* storageName, int sourceType, int compressionMethod, + ZipEntry** ppEntry) +{ + ZipEntry* pEntry = NULL; + status_t result = NO_ERROR; + long lfhPosn, startPosn, endPosn, uncompressedLen; + FILE* inputFp = NULL; + unsigned long crc; + time_t modWhen; + + if (mReadOnly) + return INVALID_OPERATION; + + assert(compressionMethod == ZipEntry::kCompressDeflated || + compressionMethod == ZipEntry::kCompressStored); + + /* make sure we're in a reasonable state */ + assert(mZipFp != NULL); + assert(mEntries.size() == mEOCD.mTotalNumEntries); + + /* make sure it doesn't already exist */ + if (getEntryByName(storageName) != NULL) + return ALREADY_EXISTS; + + if (!data) { + inputFp = fopen(fileName, FILE_OPEN_RO); + if (inputFp == NULL) + return errnoToStatus(errno); + } + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + + pEntry = new ZipEntry; + pEntry->initNew(storageName, NULL); + + /* + * From here on out, failures are more interesting. + */ + mNeedCDRewrite = true; + + /* + * Write the LFH, even though it's still mostly blank. We need it + * as a place-holder. In theory the LFH isn't necessary, but in + * practice some utilities demand it. + */ + lfhPosn = ftell(mZipFp); + pEntry->mLFH.write(mZipFp); + startPosn = ftell(mZipFp); + + /* + * Copy the data in, possibly compressing it as we go. + */ + if (sourceType == ZipEntry::kCompressStored) { + if (compressionMethod == ZipEntry::kCompressDeflated) { + bool failed = false; + result = compressFpToFp(mZipFp, inputFp, data, size, &crc); + if (result != NO_ERROR) { + ALOGD("compression failed, storing\n"); + failed = true; + } else { + /* + * Make sure it has compressed "enough". This probably ought + * to be set through an API call, but I don't expect our + * criteria to change over time. + */ + long src = inputFp ? ftell(inputFp) : size; + long dst = ftell(mZipFp) - startPosn; + if (dst + (dst / 10) > src) { + ALOGD("insufficient compression (src=%ld dst=%ld), storing\n", + src, dst); + failed = true; + } + } + + if (failed) { + compressionMethod = ZipEntry::kCompressStored; + if (inputFp) rewind(inputFp); + fseek(mZipFp, startPosn, SEEK_SET); + /* fall through to kCompressStored case */ + } + } + /* handle "no compression" request, or failed compression from above */ + if (compressionMethod == ZipEntry::kCompressStored) { + if (inputFp) { + result = copyFpToFp(mZipFp, inputFp, &crc); + } else { + result = copyDataToFp(mZipFp, data, size, &crc); + } + if (result != NO_ERROR) { + // don't need to truncate; happens in CDE rewrite + ALOGD("failed copying data in\n"); + goto bail; + } + } + + // currently seeked to end of file + uncompressedLen = inputFp ? ftell(inputFp) : size; + } else if (sourceType == ZipEntry::kCompressDeflated) { + /* we should support uncompressed-from-compressed, but it's not + * important right now */ + assert(compressionMethod == ZipEntry::kCompressDeflated); + + bool scanResult; + int method; + long compressedLen; + + scanResult = ZipUtils::examineGzip(inputFp, &method, &uncompressedLen, + &compressedLen, &crc); + if (!scanResult || method != ZipEntry::kCompressDeflated) { + ALOGD("this isn't a deflated gzip file?"); + result = UNKNOWN_ERROR; + goto bail; + } + + result = copyPartialFpToFp(mZipFp, inputFp, compressedLen, NULL); + if (result != NO_ERROR) { + ALOGD("failed copying gzip data in\n"); + goto bail; + } + } else { + assert(false); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * We could write the "Data Descriptor", but there doesn't seem to + * be any point since we're going to go back and write the LFH. + * + * Update file offsets. + */ + endPosn = ftell(mZipFp); // seeked to end of compressed data + + /* + * Success! Fill out new values. + */ + pEntry->setDataInfo(uncompressedLen, endPosn - startPosn, crc, + compressionMethod); + modWhen = getModTime(inputFp ? fileno(inputFp) : fileno(mZipFp)); + pEntry->setModWhen(modWhen); + pEntry->setLFHOffset(lfhPosn); + mEOCD.mNumEntries++; + mEOCD.mTotalNumEntries++; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + mEOCD.mCentralDirOffset = endPosn; + + /* + * Go back and write the LFH. + */ + if (fseek(mZipFp, lfhPosn, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + pEntry->mLFH.write(mZipFp); + + /* + * Add pEntry to the list. + */ + mEntries.push_back(pEntry); + if (ppEntry != NULL) + *ppEntry = pEntry; + pEntry = NULL; + +bail: + if (inputFp != NULL) + fclose(inputFp); + delete pEntry; + return result; +} + +/* + * Add an entry by copying it from another zip file. If "padding" is + * nonzero, the specified number of bytes will be added to the "extra" + * field in the header. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ +status_t ZipFile::add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, + const char* storageName, int padding, ZipEntry** ppEntry) +{ + ZipEntry* pEntry = NULL; + status_t result; + long lfhPosn, endPosn; + + if (mReadOnly) + return INVALID_OPERATION; + + /* make sure we're in a reasonable state */ + assert(mZipFp != NULL); + assert(mEntries.size() == mEOCD.mTotalNumEntries); + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + + pEntry = new ZipEntry; + if (pEntry == NULL) { + result = NO_MEMORY; + goto bail; + } + + result = pEntry->initFromExternal(pSourceZip, pSourceEntry, storageName); + if (result != NO_ERROR) { + goto bail; + } + if (padding != 0) { + result = pEntry->addPadding(padding); + if (result != NO_ERROR) + goto bail; + } + + /* + * From here on out, failures are more interesting. + */ + mNeedCDRewrite = true; + + /* + * Write the LFH. Since we're not recompressing the data, we already + * have all of the fields filled out. + */ + lfhPosn = ftell(mZipFp); + pEntry->mLFH.write(mZipFp); + + /* + * Copy the data over. + * + * If the "has data descriptor" flag is set, we want to copy the DD + * fields as well. This is a fixed-size area immediately following + * the data. + */ + if (fseek(pSourceZip->mZipFp, pSourceEntry->getFileOffset(), SEEK_SET) != 0) + { + result = UNKNOWN_ERROR; + goto bail; + } + + off_t copyLen; + copyLen = pSourceEntry->getCompressedLen(); + if ((pSourceEntry->mLFH.mGPBitFlag & ZipEntry::kUsesDataDescr) != 0) + copyLen += ZipEntry::kDataDescriptorLen; + + if (copyPartialFpToFp(mZipFp, pSourceZip->mZipFp, copyLen, NULL) + != NO_ERROR) + { + ALOGW("copy of '%s' failed\n", pEntry->mCDE.mFileName); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * Update file offsets. + */ + endPosn = ftell(mZipFp); + + /* + * Success! Fill out new values. + */ + pEntry->setLFHOffset(lfhPosn); // sets mCDE.mLocalHeaderRelOffset + mEOCD.mNumEntries++; + mEOCD.mTotalNumEntries++; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + mEOCD.mCentralDirOffset = endPosn; + + /* + * Add pEntry to the list. + */ + mEntries.push_back(pEntry); + if (ppEntry != NULL) + *ppEntry = pEntry; + pEntry = NULL; + + result = NO_ERROR; + +bail: + delete pEntry; + return result; +} + +/* + * Copy all of the bytes in "src" to "dst". + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the data. + */ +status_t ZipFile::copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32) +{ + unsigned char tmpBuf[32768]; + size_t count; + + *pCRC32 = crc32(0L, Z_NULL, 0); + + while (1) { + count = fread(tmpBuf, 1, sizeof(tmpBuf), srcFp); + if (ferror(srcFp) || ferror(dstFp)) + return errnoToStatus(errno); + if (count == 0) + break; + + *pCRC32 = crc32(*pCRC32, tmpBuf, count); + + if (fwrite(tmpBuf, 1, count, dstFp) != count) { + ALOGD("fwrite %d bytes failed\n", (int) count); + return UNKNOWN_ERROR; + } + } + + return NO_ERROR; +} + +/* + * Copy all of the bytes in "src" to "dst". + * + * On exit, "dstFp" will be seeked immediately past the data. + */ +status_t ZipFile::copyDataToFp(FILE* dstFp, + const void* data, size_t size, unsigned long* pCRC32) +{ + *pCRC32 = crc32(0L, Z_NULL, 0); + if (size > 0) { + *pCRC32 = crc32(*pCRC32, (const unsigned char*)data, size); + if (fwrite(data, 1, size, dstFp) != size) { + ALOGD("fwrite %d bytes failed\n", (int) size); + return UNKNOWN_ERROR; + } + } + + return NO_ERROR; +} + +/* + * Copy some of the bytes in "src" to "dst". + * + * If "pCRC32" is NULL, the CRC will not be computed. + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the data just written. + */ +status_t ZipFile::copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, + unsigned long* pCRC32) +{ + unsigned char tmpBuf[32768]; + size_t count; + + if (pCRC32 != NULL) + *pCRC32 = crc32(0L, Z_NULL, 0); + + while (length) { + long readSize; + + readSize = sizeof(tmpBuf); + if (readSize > length) + readSize = length; + + count = fread(tmpBuf, 1, readSize, srcFp); + if ((long) count != readSize) { // error or unexpected EOF + ALOGD("fread %d bytes failed\n", (int) readSize); + return UNKNOWN_ERROR; + } + + if (pCRC32 != NULL) + *pCRC32 = crc32(*pCRC32, tmpBuf, count); + + if (fwrite(tmpBuf, 1, count, dstFp) != count) { + ALOGD("fwrite %d bytes failed\n", (int) count); + return UNKNOWN_ERROR; + } + + length -= readSize; + } + + return NO_ERROR; +} + +/* + * Compress all of the data in "srcFp" and write it to "dstFp". + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the compressed data. + */ +status_t ZipFile::compressFpToFp(FILE* dstFp, FILE* srcFp, + const void* data, size_t size, unsigned long* pCRC32) +{ + status_t result = NO_ERROR; + const size_t kBufSize = 32768; + unsigned char* inBuf = NULL; + unsigned char* outBuf = NULL; + z_stream zstream; + bool atEof = false; // no feof() aviailable yet + unsigned long crc; + int zerr; + + /* + * Create an input buffer and an output buffer. + */ + inBuf = new unsigned char[kBufSize]; + outBuf = new unsigned char[kBufSize]; + if (inBuf == NULL || outBuf == NULL) { + result = NO_MEMORY; + goto bail; + } + + /* + * Initialize the zlib stream. + */ + memset(&zstream, 0, sizeof(zstream)); + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; + zstream.next_in = NULL; + zstream.avail_in = 0; + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + zstream.data_type = Z_UNKNOWN; + + zerr = deflateInit2(&zstream, Z_BEST_COMPRESSION, + Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY); + if (zerr != Z_OK) { + result = UNKNOWN_ERROR; + if (zerr == Z_VERSION_ERROR) { + ALOGE("Installed zlib is not compatible with linked version (%s)\n", + ZLIB_VERSION); + } else { + ALOGD("Call to deflateInit2 failed (zerr=%d)\n", zerr); + } + goto bail; + } + + crc = crc32(0L, Z_NULL, 0); + + /* + * Loop while we have data. + */ + do { + size_t getSize; + int flush; + + /* only read if the input buffer is empty */ + if (zstream.avail_in == 0 && !atEof) { + ALOGV("+++ reading %d bytes\n", (int)kBufSize); + if (data) { + getSize = size > kBufSize ? kBufSize : size; + memcpy(inBuf, data, getSize); + data = ((const char*)data) + getSize; + size -= getSize; + } else { + getSize = fread(inBuf, 1, kBufSize, srcFp); + if (ferror(srcFp)) { + ALOGD("deflate read failed (errno=%d)\n", errno); + goto z_bail; + } + } + if (getSize < kBufSize) { + ALOGV("+++ got %d bytes, EOF reached\n", + (int)getSize); + atEof = true; + } + + crc = crc32(crc, inBuf, getSize); + + zstream.next_in = inBuf; + zstream.avail_in = getSize; + } + + if (atEof) + flush = Z_FINISH; /* tell zlib that we're done */ + else + flush = Z_NO_FLUSH; /* more to come! */ + + zerr = deflate(&zstream, flush); + if (zerr != Z_OK && zerr != Z_STREAM_END) { + ALOGD("zlib deflate call failed (zerr=%d)\n", zerr); + result = UNKNOWN_ERROR; + goto z_bail; + } + + /* write when we're full or when we're done */ + if (zstream.avail_out == 0 || + (zerr == Z_STREAM_END && zstream.avail_out != (uInt) kBufSize)) + { + ALOGV("+++ writing %d bytes\n", (int) (zstream.next_out - outBuf)); + if (fwrite(outBuf, 1, zstream.next_out - outBuf, dstFp) != + (size_t)(zstream.next_out - outBuf)) + { + ALOGD("write %d failed in deflate\n", + (int) (zstream.next_out - outBuf)); + goto z_bail; + } + + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + } + } while (zerr == Z_OK); + + assert(zerr == Z_STREAM_END); /* other errors should've been caught */ + + *pCRC32 = crc; + +z_bail: + deflateEnd(&zstream); /* free up any allocated structures */ + +bail: + delete[] inBuf; + delete[] outBuf; + + return result; +} + +/* + * Mark an entry as deleted. + * + * We will eventually need to crunch the file down, but if several files + * are being removed (perhaps as part of an "update" process) we can make + * things considerably faster by deferring the removal to "flush" time. + */ +status_t ZipFile::remove(ZipEntry* pEntry) +{ + /* + * Should verify that pEntry is actually part of this archive, and + * not some stray ZipEntry from a different file. + */ + + /* mark entry as deleted, and mark archive as dirty */ + pEntry->setDeleted(); + mNeedCDRewrite = true; + return NO_ERROR; +} + +/* + * Flush any pending writes. + * + * In particular, this will crunch out deleted entries, and write the + * Central Directory and EOCD if we have stomped on them. + */ +status_t ZipFile::flush(void) +{ + status_t result = NO_ERROR; + long eocdPosn; + int i, count; + + if (mReadOnly) + return INVALID_OPERATION; + if (!mNeedCDRewrite) + return NO_ERROR; + + assert(mZipFp != NULL); + + result = crunchArchive(); + if (result != NO_ERROR) + return result; + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) + return UNKNOWN_ERROR; + + count = mEntries.size(); + for (i = 0; i < count; i++) { + ZipEntry* pEntry = mEntries[i]; + pEntry->mCDE.write(mZipFp); + } + + eocdPosn = ftell(mZipFp); + mEOCD.mCentralDirSize = eocdPosn - mEOCD.mCentralDirOffset; + + mEOCD.write(mZipFp); + + /* + * If we had some stuff bloat up during compression and get replaced + * with plain files, or if we deleted some entries, there's a lot + * of wasted space at the end of the file. Remove it now. + */ + if (ftruncate(fileno(mZipFp), ftell(mZipFp)) != 0) { + ALOGW("ftruncate failed %ld: %s\n", ftell(mZipFp), strerror(errno)); + // not fatal + } + + /* should we clear the "newly added" flag in all entries now? */ + + mNeedCDRewrite = false; + return NO_ERROR; +} + +/* + * Crunch deleted files out of an archive by shifting the later files down. + * + * Because we're not using a temp file, we do the operation inside the + * current file. + */ +status_t ZipFile::crunchArchive(void) +{ + status_t result = NO_ERROR; + int i, count; + long delCount, adjust; + +#if 0 + printf("CONTENTS:\n"); + for (i = 0; i < (int) mEntries.size(); i++) { + printf(" %d: lfhOff=%ld del=%d\n", + i, mEntries[i]->getLFHOffset(), mEntries[i]->getDeleted()); + } + printf(" END is %ld\n", (long) mEOCD.mCentralDirOffset); +#endif + + /* + * Roll through the set of files, shifting them as appropriate. We + * could probably get a slight performance improvement by sliding + * multiple files down at once (because we could use larger reads + * when operating on batches of small files), but it's not that useful. + */ + count = mEntries.size(); + delCount = adjust = 0; + for (i = 0; i < count; i++) { + ZipEntry* pEntry = mEntries[i]; + long span; + + if (pEntry->getLFHOffset() != 0) { + long nextOffset; + + /* Get the length of this entry by finding the offset + * of the next entry. Directory entries don't have + * file offsets, so we need to find the next non-directory + * entry. + */ + nextOffset = 0; + for (int ii = i+1; nextOffset == 0 && ii < count; ii++) + nextOffset = mEntries[ii]->getLFHOffset(); + if (nextOffset == 0) + nextOffset = mEOCD.mCentralDirOffset; + span = nextOffset - pEntry->getLFHOffset(); + + assert(span >= ZipEntry::LocalFileHeader::kLFHLen); + } else { + /* This is a directory entry. It doesn't have + * any actual file contents, so there's no need to + * move anything. + */ + span = 0; + } + + //printf("+++ %d: off=%ld span=%ld del=%d [count=%d]\n", + // i, pEntry->getLFHOffset(), span, pEntry->getDeleted(), count); + + if (pEntry->getDeleted()) { + adjust += span; + delCount++; + + delete pEntry; + mEntries.erase(mEntries.begin() + i); + + /* adjust loop control */ + count--; + i--; + } else if (span != 0 && adjust > 0) { + /* shuffle this entry back */ + //printf("+++ Shuffling '%s' back %ld\n", + // pEntry->getFileName(), adjust); + result = filemove(mZipFp, pEntry->getLFHOffset() - adjust, + pEntry->getLFHOffset(), span); + if (result != NO_ERROR) { + /* this is why you use a temp file */ + ALOGE("error during crunch - archive is toast\n"); + return result; + } + + pEntry->setLFHOffset(pEntry->getLFHOffset() - adjust); + } + } + + /* + * Fix EOCD info. We have to wait until the end to do some of this + * because we use mCentralDirOffset to determine "span" for the + * last entry. + */ + mEOCD.mCentralDirOffset -= adjust; + mEOCD.mNumEntries -= delCount; + mEOCD.mTotalNumEntries -= delCount; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + + assert(mEOCD.mNumEntries == mEOCD.mTotalNumEntries); + assert(mEOCD.mNumEntries == count); + + return result; +} + +/* + * Works like memmove(), but on pieces of a file. + */ +status_t ZipFile::filemove(FILE* fp, off_t dst, off_t src, size_t n) +{ + if (dst == src || n <= 0) + return NO_ERROR; + + unsigned char readBuf[32768]; + + if (dst < src) { + /* shift stuff toward start of file; must read from start */ + while (n != 0) { + size_t getSize = sizeof(readBuf); + if (getSize > n) + getSize = n; + + if (fseek(fp, (long) src, SEEK_SET) != 0) { + ALOGD("filemove src seek %ld failed\n", (long) src); + return UNKNOWN_ERROR; + } + + if (fread(readBuf, 1, getSize, fp) != getSize) { + ALOGD("filemove read %ld off=%ld failed\n", + (long) getSize, (long) src); + return UNKNOWN_ERROR; + } + + if (fseek(fp, (long) dst, SEEK_SET) != 0) { + ALOGD("filemove dst seek %ld failed\n", (long) dst); + return UNKNOWN_ERROR; + } + + if (fwrite(readBuf, 1, getSize, fp) != getSize) { + ALOGD("filemove write %ld off=%ld failed\n", + (long) getSize, (long) dst); + return UNKNOWN_ERROR; + } + + src += getSize; + dst += getSize; + n -= getSize; + } + } else { + /* shift stuff toward end of file; must read from end */ + assert(false); // write this someday, maybe + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + + +/* + * Get the modification time from a file descriptor. + */ +time_t ZipFile::getModTime(int fd) +{ + struct stat sb; + + if (fstat(fd, &sb) < 0) { + ALOGD("HEY: fstat on fd %d failed\n", fd); + return (time_t) -1; + } + + return sb.st_mtime; +} + + +#if 0 /* this is a bad idea */ +/* + * Get a copy of the Zip file descriptor. + * + * We don't allow this if the file was opened read-write because we tend + * to leave the file contents in an uncertain state between calls to + * flush(). The duplicated file descriptor should only be valid for reads. + */ +int ZipFile::getZipFd(void) const +{ + if (!mReadOnly) + return INVALID_OPERATION; + assert(mZipFp != NULL); + + int fd; + fd = dup(fileno(mZipFp)); + if (fd < 0) { + ALOGD("didn't work, errno=%d\n", errno); + } + + return fd; +} +#endif + + +#if 0 +/* + * Expand data. + */ +bool ZipFile::uncompress(const ZipEntry* pEntry, void* buf) const +{ + return false; +} +#endif + +// free the memory when you're done +void* ZipFile::uncompress(const ZipEntry* entry) +{ + size_t unlen = entry->getUncompressedLen(); + size_t clen = entry->getCompressedLen(); + + void* buf = malloc(unlen); + if (buf == NULL) { + return NULL; + } + + fseek(mZipFp, 0, SEEK_SET); + + off_t offset = entry->getFileOffset(); + if (fseek(mZipFp, offset, SEEK_SET) != 0) { + goto bail; + } + + switch (entry->getCompressionMethod()) + { + case ZipEntry::kCompressStored: { + ssize_t amt = fread(buf, 1, unlen, mZipFp); + if (amt != (ssize_t)unlen) { + goto bail; + } +#if 0 + printf("data...\n"); + const unsigned char* p = (unsigned char*)buf; + const unsigned char* end = p+unlen; + for (int i=0; i<32 && p < end; i++) { + printf("0x%08x ", (int)(offset+(i*0x10))); + for (int j=0; j<0x10 && p < end; j++) { + printf(" %02x", *p); + p++; + } + printf("\n"); + } +#endif + + } + break; + case ZipEntry::kCompressDeflated: { + if (!ZipUtils::inflateToBuffer(mZipFp, buf, unlen, clen)) { + goto bail; + } + } + break; + default: + goto bail; + } + return buf; + +bail: + free(buf); + return NULL; +} + + +/* + * =========================================================================== + * ZipFile::EndOfCentralDir + * =========================================================================== + */ + +/* + * Read the end-of-central-dir fields. + * + * "buf" should be positioned at the EOCD signature, and should contain + * the entire EOCD area including the comment. + */ +status_t ZipFile::EndOfCentralDir::readBuf(const unsigned char* buf, int len) +{ + /* don't allow re-use */ + assert(mComment == NULL); + + if (len < kEOCDLen) { + /* looks like ZIP file got truncated */ + ALOGD(" Zip EOCD: expected >= %d bytes, found %d\n", + kEOCDLen, len); + return INVALID_OPERATION; + } + + /* this should probably be an assert() */ + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) + return UNKNOWN_ERROR; + + mDiskNumber = ZipEntry::getShortLE(&buf[0x04]); + mDiskWithCentralDir = ZipEntry::getShortLE(&buf[0x06]); + mNumEntries = ZipEntry::getShortLE(&buf[0x08]); + mTotalNumEntries = ZipEntry::getShortLE(&buf[0x0a]); + mCentralDirSize = ZipEntry::getLongLE(&buf[0x0c]); + mCentralDirOffset = ZipEntry::getLongLE(&buf[0x10]); + mCommentLen = ZipEntry::getShortLE(&buf[0x14]); + + // TODO: validate mCentralDirOffset + + if (mCommentLen > 0) { + if (kEOCDLen + mCommentLen > len) { + ALOGD("EOCD(%d) + comment(%d) exceeds len (%d)\n", + kEOCDLen, mCommentLen, len); + return UNKNOWN_ERROR; + } + mComment = new unsigned char[mCommentLen]; + memcpy(mComment, buf + kEOCDLen, mCommentLen); + } + + return NO_ERROR; +} + +/* + * Write an end-of-central-directory section. + */ +status_t ZipFile::EndOfCentralDir::write(FILE* fp) +{ + unsigned char buf[kEOCDLen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mDiskNumber); + ZipEntry::putShortLE(&buf[0x06], mDiskWithCentralDir); + ZipEntry::putShortLE(&buf[0x08], mNumEntries); + ZipEntry::putShortLE(&buf[0x0a], mTotalNumEntries); + ZipEntry::putLongLE(&buf[0x0c], mCentralDirSize); + ZipEntry::putLongLE(&buf[0x10], mCentralDirOffset); + ZipEntry::putShortLE(&buf[0x14], mCommentLen); + + if (fwrite(buf, 1, kEOCDLen, fp) != kEOCDLen) + return UNKNOWN_ERROR; + if (mCommentLen > 0) { + assert(mComment != NULL); + if (fwrite(mComment, mCommentLen, 1, fp) != mCommentLen) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +/* + * Dump the contents of an EndOfCentralDir object. + */ +void ZipFile::EndOfCentralDir::dump(void) const +{ + ALOGD(" EndOfCentralDir contents:\n"); + ALOGD(" diskNum=%u diskWCD=%u numEnt=%u totalNumEnt=%u\n", + mDiskNumber, mDiskWithCentralDir, mNumEntries, mTotalNumEntries); + ALOGD(" centDirSize=%lu centDirOff=%lu commentLen=%u\n", + mCentralDirSize, mCentralDirOffset, mCommentLen); +} + +} // namespace aapt diff --git a/tools/aapt2/ZipFile.h b/tools/aapt2/ZipFile.h new file mode 100644 index 0000000..9de92dd --- /dev/null +++ b/tools/aapt2/ZipFile.h @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2006 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. + */ + +// +// General-purpose Zip archive access. This class allows both reading and +// writing to Zip archives, including deletion of existing entries. +// +#ifndef __LIBS_ZIPFILE_H +#define __LIBS_ZIPFILE_H + +#include "BigBuffer.h" +#include "ZipEntry.h" + +#include <stdio.h> +#include <utils/Errors.h> +#include <vector> + +namespace aapt { + +using android::status_t; + +/* + * Manipulate a Zip archive. + * + * Some changes will not be visible in the until until "flush" is called. + * + * The correct way to update a file archive is to make all changes to a + * copy of the archive in a temporary file, and then unlink/rename over + * the original after everything completes. Because we're only interested + * in using this for packaging, we don't worry about such things. Crashing + * after making changes and before flush() completes could leave us with + * an unusable Zip archive. + */ +class ZipFile { +public: + ZipFile(void) + : mZipFp(NULL), mReadOnly(false), mNeedCDRewrite(false) + {} + ~ZipFile(void) { + if (!mReadOnly) + flush(); + if (mZipFp != NULL) + fclose(mZipFp); + discardEntries(); + } + + /* + * Open a new or existing archive. + */ + enum { + kOpenReadOnly = 0x01, + kOpenReadWrite = 0x02, + kOpenCreate = 0x04, // create if it doesn't exist + kOpenTruncate = 0x08, // if it exists, empty it + }; + status_t open(const char* zipFileName, int flags); + + /* + * Add a file to the end of the archive. Specify whether you want the + * library to try to store it compressed. + * + * If "storageName" is specified, the archive will use that instead + * of "fileName". + * + * If there is already an entry with the same name, the call fails. + * Existing entries with the same name must be removed first. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const char* fileName, int compressionMethod, + ZipEntry** ppEntry) + { + return add(fileName, fileName, compressionMethod, ppEntry); + } + status_t add(const char* fileName, const char* storageName, + int compressionMethod, ZipEntry** ppEntry) + { + return addCommon(fileName, NULL, 0, storageName, + ZipEntry::kCompressStored, + compressionMethod, ppEntry); + } + + /* + * Add a file that is already compressed with gzip. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t addGzip(const char* fileName, const char* storageName, + ZipEntry** ppEntry) + { + return addCommon(fileName, NULL, 0, storageName, + ZipEntry::kCompressDeflated, + ZipEntry::kCompressDeflated, ppEntry); + } + + /* + * Add a file from an in-memory data buffer. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const void* data, size_t size, const char* storageName, + int compressionMethod, ZipEntry** ppEntry) + { + return addCommon(NULL, data, size, storageName, + ZipEntry::kCompressStored, + compressionMethod, ppEntry); + } + + status_t add(const BigBuffer& data, const char* storageName, + int compressionMethod, ZipEntry** ppEntry); + + /* + * Add an entry by copying it from another zip file. If storageName is + * non-NULL, the entry will be inserted with the name storageName, otherwise + * it will have the same name as the source entry. If "padding" is + * nonzero, the specified number of bytes will be added to the "extra" + * field in the header. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, + const char* storageName, int padding, ZipEntry** ppEntry); + + /* + * Mark an entry as having been removed. It is not actually deleted + * from the archive or our internal data structures until flush() is + * called. + */ + status_t remove(ZipEntry* pEntry); + + /* + * Flush changes. If mNeedCDRewrite is set, this writes the central dir. + */ + status_t flush(void); + + /* + * Expand the data into the buffer provided. The buffer must hold + * at least <uncompressed len> bytes. Variation expands directly + * to a file. + * + * Returns "false" if an error was encountered in the compressed data. + */ + //bool uncompress(const ZipEntry* pEntry, void* buf) const; + //bool uncompress(const ZipEntry* pEntry, FILE* fp) const; + void* uncompress(const ZipEntry* pEntry); + + /* + * Get an entry, by name. Returns NULL if not found. + * + * Does not return entries pending deletion. + */ + ZipEntry* getEntryByName(const char* fileName) const; + + /* + * Get the Nth entry in the archive. + * + * This will return an entry that is pending deletion. + */ + int getNumEntries(void) const { return mEntries.size(); } + ZipEntry* getEntryByIndex(int idx) const; + +private: + /* these are private and not defined */ + ZipFile(const ZipFile& src); + ZipFile& operator=(const ZipFile& src); + + class EndOfCentralDir { + public: + EndOfCentralDir(void) : + mDiskNumber(0), + mDiskWithCentralDir(0), + mNumEntries(0), + mTotalNumEntries(0), + mCentralDirSize(0), + mCentralDirOffset(0), + mCommentLen(0), + mComment(NULL) + {} + virtual ~EndOfCentralDir(void) { + delete[] mComment; + } + + status_t readBuf(const unsigned char* buf, int len); + status_t write(FILE* fp); + + //unsigned long mSignature; + unsigned short mDiskNumber; + unsigned short mDiskWithCentralDir; + unsigned short mNumEntries; + unsigned short mTotalNumEntries; + unsigned long mCentralDirSize; + unsigned long mCentralDirOffset; // offset from first disk + unsigned short mCommentLen; + unsigned char* mComment; + + enum { + kSignature = 0x06054b50, + kEOCDLen = 22, // EndOfCentralDir len, excl. comment + + kMaxCommentLen = 65535, // longest possible in ushort + kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, + + }; + + void dump(void) const; + }; + + + /* read all entries in the central dir */ + status_t readCentralDir(void); + + /* crunch deleted entries out */ + status_t crunchArchive(void); + + /* clean up mEntries */ + void discardEntries(void); + + /* common handler for all "add" functions */ + status_t addCommon(const char* fileName, const void* data, size_t size, + const char* storageName, int sourceType, int compressionMethod, + ZipEntry** ppEntry); + + /* copy all of "srcFp" into "dstFp" */ + status_t copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32); + /* copy all of "data" into "dstFp" */ + status_t copyDataToFp(FILE* dstFp, + const void* data, size_t size, unsigned long* pCRC32); + /* copy some of "srcFp" into "dstFp" */ + status_t copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, + unsigned long* pCRC32); + /* like memmove(), but on parts of a single file */ + status_t filemove(FILE* fp, off_t dest, off_t src, size_t n); + /* compress all of "srcFp" into "dstFp", using Deflate */ + status_t compressFpToFp(FILE* dstFp, FILE* srcFp, + const void* data, size_t size, unsigned long* pCRC32); + + /* get modification date from a file descriptor */ + time_t getModTime(int fd); + + /* + * We use stdio FILE*, which gives us buffering but makes dealing + * with files >2GB awkward. Until we support Zip64, we're fine. + */ + FILE* mZipFp; // Zip file pointer + + /* one of these per file */ + EndOfCentralDir mEOCD; + + /* did we open this read-only? */ + bool mReadOnly; + + /* set this when we trash the central dir */ + bool mNeedCDRewrite; + + /* + * One ZipEntry per entry in the zip file. I'm using pointers instead + * of objects because it's easier than making operator= work for the + * classes and sub-classes. + */ + std::vector<ZipEntry*> mEntries; +}; + +}; // namespace aapt + +#endif // __LIBS_ZIPFILE_H diff --git a/tools/aapt2/data/AndroidManifest.xml b/tools/aapt2/data/AndroidManifest.xml new file mode 100644 index 0000000..8533c28 --- /dev/null +++ b/tools/aapt2/data/AndroidManifest.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app"> + <application + android:name=".Activity"> + </application> +</manifest> diff --git a/tools/aapt2/data/Makefile b/tools/aapt2/data/Makefile new file mode 100644 index 0000000..3387135 --- /dev/null +++ b/tools/aapt2/data/Makefile @@ -0,0 +1,83 @@ +## +# Environment dependent variables +## + +AAPT := aapt2 +ZIPALIGN := zipalign -f 4 +FRAMEWORK := ../../../../../out/target/common/obj/APPS/framework-res_intermediates/package-export.apk + +## +# Project depenedent variables +## + +LOCAL_PACKAGE := com.android.app +LOCAL_RESOURCE_DIR := res +LOCAL_LIBS := lib/out/package.apk +LOCAL_OUT := out +LOCAL_GEN := out/gen +LOCAL_PROGUARD := out/proguard.rule + +## +# AAPT2 custom rules. +## + +PRIVATE_APK_UNALIGNED := $(LOCAL_OUT)/package-unaligned.apk +PRIVATE_APK_ALIGNED := $(LOCAL_OUT)/package.apk + +# Eg: framework.apk, etc. +PRIVATE_INCLUDES := $(FRAMEWORK) +$(info PRIVATE_INCLUDES = $(PRIVATE_INCLUDES)) + +# Eg: gen/com/android/app/R.java +PRIVATE_R_JAVA := $(LOCAL_GEN)/$(subst .,/,$(LOCAL_PACKAGE))/R.java +$(info PRIVATE_R_JAVA = $(PRIVATE_R_JAVA)) + +# Eg: res/drawable/icon.png, res/values/styles.xml +PRIVATE_RESOURCES := $(shell find $(LOCAL_RESOURCE_DIR) -mindepth 1 -maxdepth 2 -type f) +$(info PRIVATE_RESOURCES = $(PRIVATE_RESOURCES)) + +# Eg: drawable, values, layouts +PRIVATE_RESOURCE_TYPES := \ + $(patsubst $(LOCAL_RESOURCE_DIR)/%/,%,$(sort $(dir $(PRIVATE_RESOURCES)))) +$(info PRIVATE_RESOURCE_TYPES = $(PRIVATE_RESOURCE_TYPES)) + +# Eg: out/values-v4.apk, out/drawable-xhdpi.apk +PRIVATE_INTERMEDIATE_TABLES := $(patsubst %,$(LOCAL_OUT)/%.apk,$(PRIVATE_RESOURCE_TYPES)) +$(info PRIVATE_INTERMEDIATE_TABLES = $(PRIVATE_INTERMEDIATE_TABLES)) + +# Generates rules for collect phase. +# $1: Resource type (values-v4) +# returns: out/values-v4.apk: res/values-v4/styles.xml res/values-v4/colors.xml +define make-collect-rule +$(LOCAL_OUT)/$1.apk: $(filter $(LOCAL_RESOURCE_DIR)/$1/%,$(PRIVATE_RESOURCES)) + $(AAPT) compile --package $(LOCAL_PACKAGE) -o $$@ $$^ +endef + +# Collect: out/values-v4.apk <- res/values-v4/styles.xml res/values-v4/colors.xml +$(foreach d,$(PRIVATE_RESOURCE_TYPES),$(eval $(call make-collect-rule,$d))) + +# Link: out/package-unaligned.apk <- out/values-v4.apk out/drawable-v4.apk +$(PRIVATE_APK_UNALIGNED): $(PRIVATE_INTERMEDIATE_TABLES) $(PRIVATE_INCLUDES) $(LOCAL_LIBS) AndroidManifest.xml + $(AAPT) link --manifest AndroidManifest.xml $(addprefix -I ,$(PRIVATE_INCLUDES)) --java $(LOCAL_GEN) -o $@ $(PRIVATE_INTERMEDIATE_TABLES) $(LOCAL_LIBS) --proguard $(LOCAL_PROGUARD) -v + +# R.java: gen/com/android/app/R.java <- out/resources.arsc +# No action since R.java is generated when out/resources.arsc is. +$(PRIVATE_R_JAVA): $(PRIVATE_APK_UNALIGNED) + +# Assemble: zip out/resources.arsc AndroidManifest.xml and res/**/* +$(PRIVATE_APK_ALIGNED): $(PRIVATE_APK_UNALIGNED) + $(ZIPALIGN) $< $@ + +# Create the out directory if needed. +dummy := $(shell test -d $(LOCAL_OUT) || mkdir -p $(LOCAL_OUT)) + +.PHONY: java +java: $(PRIVATE_R_JAVA) + +.PHONY: assemble +assemble: $(PRIVATE_APK_ALIGNED) + +.PHONY: all +all: assemble java + +.DEFAULT_GOAL := all diff --git a/tools/aapt2/data/lib/AndroidManifest.xml b/tools/aapt2/data/lib/AndroidManifest.xml new file mode 100644 index 0000000..08b468e --- /dev/null +++ b/tools/aapt2/data/lib/AndroidManifest.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.appcompat"> + + <uses-feature android:name="bloooop" /> +</manifest> diff --git a/tools/aapt2/data/lib/Makefile b/tools/aapt2/data/lib/Makefile new file mode 100644 index 0000000..372c225 --- /dev/null +++ b/tools/aapt2/data/lib/Makefile @@ -0,0 +1,81 @@ +## +# Environment dependent variables +## + +AAPT := aapt2 +ZIPALIGN := zipalign -f 4 +FRAMEWORK := ../../../../../../out/target/common/obj/APPS/framework-res_intermediates/package-export.apk + +## +# Project depenedent variables +## + +LOCAL_PACKAGE := android.appcompat +LOCAL_RESOURCE_DIR := res +LOCAL_OUT := out +LOCAL_GEN := out/gen + +## +# AAPT2 custom rules. +## + +PRIVATE_APK_UNALIGNED := $(LOCAL_OUT)/package-unaligned.apk +PRIVATE_APK_ALIGNED := $(LOCAL_OUT)/package.apk + +# Eg: framework.apk, etc. +PRIVATE_LIBS := $(FRAMEWORK) +$(info PRIVATE_LIBS = $(PRIVATE_LIBS)) + +# Eg: gen/com/android/app/R.java +PRIVATE_R_JAVA := $(LOCAL_GEN)/$(subst .,/,$(LOCAL_PACKAGE))/R.java +$(info PRIVATE_R_JAVA = $(PRIVATE_R_JAVA)) + +# Eg: res/drawable/icon.png, res/values/styles.xml +PRIVATE_RESOURCES := $(shell find $(LOCAL_RESOURCE_DIR) -mindepth 1 -maxdepth 2 -type f) +$(info PRIVATE_RESOURCES = $(PRIVATE_RESOURCES)) + +# Eg: drawable, values, layouts +PRIVATE_RESOURCE_TYPES := \ + $(patsubst $(LOCAL_RESOURCE_DIR)/%/,%,$(sort $(dir $(PRIVATE_RESOURCES)))) +$(info PRIVATE_RESOURCE_TYPES = $(PRIVATE_RESOURCE_TYPES)) + +# Eg: out/values-v4.apk, out/drawable-xhdpi.apk +PRIVATE_INTERMEDIATE_TABLES := $(patsubst %,$(LOCAL_OUT)/%.apk,$(PRIVATE_RESOURCE_TYPES)) +$(info PRIVATE_INTERMEDIATE_TABLES = $(PRIVATE_INTERMEDIATE_TABLES)) + +# Generates rules for collect phase. +# $1: Resource type (values-v4) +# returns: out/values-v4.apk: res/values-v4/styles.xml res/values-v4/colors.xml +define make-collect-rule +$(LOCAL_OUT)/$1.apk: $(filter $(LOCAL_RESOURCE_DIR)/$1/%,$(PRIVATE_RESOURCES)) + $(AAPT) compile --package $(LOCAL_PACKAGE) -o $$@ $$^ +endef + +# Collect: out/values-v4.apk <- res/values-v4/styles.xml res/values-v4/colors.xml +$(foreach d,$(PRIVATE_RESOURCE_TYPES),$(eval $(call make-collect-rule,$d))) + +# Link: out/package-unaligned.apk <- out/values-v4.apk out/drawable-v4.apk +$(PRIVATE_APK_UNALIGNED): $(PRIVATE_INTERMEDIATE_TABLES) $(PRIVATE_LIBS) AndroidManifest.xml + $(AAPT) link --manifest AndroidManifest.xml $(addprefix -I ,$(PRIVATE_LIBS)) --java $(LOCAL_GEN) -o $@ $(PRIVATE_INTERMEDIATE_TABLES) --static-lib + +# R.java: gen/com/android/app/R.java <- out/resources.arsc +# No action since R.java is generated when out/resources.arsc is. +$(PRIVATE_R_JAVA): $(PRIVATE_APK_UNALIGNED) + +# Assemble: zip out/resources.arsc AndroidManifest.xml and res/**/* +$(PRIVATE_APK_ALIGNED): $(PRIVATE_APK_UNALIGNED) + $(ZIPALIGN) $< $@ + +# Create the out directory if needed. +dummy := $(shell test -d $(LOCAL_OUT) || mkdir -p $(LOCAL_OUT)) + +.PHONY: java +java: $(PRIVATE_R_JAVA) + +.PHONY: assemble +assemble: $(PRIVATE_APK_ALIGNED) + +.PHONY: all +all: assemble java + +.DEFAULT_GOAL := all diff --git a/tools/aapt2/data/lib/res/layout/main.xml b/tools/aapt2/data/lib/res/layout/main.xml new file mode 100644 index 0000000..187ed2d --- /dev/null +++ b/tools/aapt2/data/lib/res/layout/main.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"/> diff --git a/tools/aapt2/data/lib/res/raw/hello.txt b/tools/aapt2/data/lib/res/raw/hello.txt new file mode 100644 index 0000000..44fc22b --- /dev/null +++ b/tools/aapt2/data/lib/res/raw/hello.txt @@ -0,0 +1 @@ +Oh howdy there diff --git a/tools/aapt2/data/lib/res/values/styles.xml b/tools/aapt2/data/lib/res/values/styles.xml new file mode 100644 index 0000000..4ce6333 --- /dev/null +++ b/tools/aapt2/data/lib/res/values/styles.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="Platform.AppCompat" parent="@android:style/Theme"> + <item name="android:windowNoTitle">true</item> + </style> + + <bool name="allow">true</bool> +</resources> diff --git a/tools/aapt2/data/res/drawable/icon.png b/tools/aapt2/data/res/drawable/icon.png Binary files differnew file mode 100644 index 0000000..4bff9b9 --- /dev/null +++ b/tools/aapt2/data/res/drawable/icon.png diff --git a/tools/aapt2/data/res/drawable/image.xml b/tools/aapt2/data/res/drawable/image.xml new file mode 100644 index 0000000..9b38739 --- /dev/null +++ b/tools/aapt2/data/res/drawable/image.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector /> diff --git a/tools/aapt2/data/res/drawable/test.9.png b/tools/aapt2/data/res/drawable/test.9.png Binary files differnew file mode 100644 index 0000000..33daa11 --- /dev/null +++ b/tools/aapt2/data/res/drawable/test.9.png diff --git a/tools/aapt2/data/res/layout/main.xml b/tools/aapt2/data/res/layout/main.xml new file mode 100644 index 0000000..50a51d9 --- /dev/null +++ b/tools/aapt2/data/res/layout/main.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:support="http://schemas.android.com/apk/res/android.appcompat" + android:id="@+id/view" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <fragment class="android.test.sample.App$Inner" /> + + <variable name="user" type="com.android.User" /> + + <View xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/me" + android:layout_width="1dp" + android:onClick="doClick" + android:text="@{user.name}" + android:layout_height="match_parent" + app:layout_width="@support:bool/allow" + app:flags="complex|weak" + android:colorAccent="#ffffff"/> +</LinearLayout> diff --git a/tools/aapt2/data/res/values-v4/styles.xml b/tools/aapt2/data/res/values-v4/styles.xml new file mode 100644 index 0000000..979a82a --- /dev/null +++ b/tools/aapt2/data/res/values-v4/styles.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="App" parent="android:Theme.Material"> + <item name="android:colorAccent">@color/accent</item> + <item name="android:text">Hey</item> + </style> +</resources> diff --git a/tools/aapt2/data/res/values/colors.xml b/tools/aapt2/data/res/values/colors.xml new file mode 100644 index 0000000..89db5fb --- /dev/null +++ b/tools/aapt2/data/res/values/colors.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="primary">#f44336</color> + <color name="primary_dark">#b71c1c</color> + <color name="accent">#fdd835</color> +</resources> diff --git a/tools/aapt2/data/res/values/styles.xml b/tools/aapt2/data/res/values/styles.xml new file mode 100644 index 0000000..d0b19a3 --- /dev/null +++ b/tools/aapt2/data/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:lib="http://schemas.android.com/apk/res/android.appcompat"> + <style name="App" parent="android.appcompat:Platform.AppCompat"> + <item name="android:background">@color/primary</item> + <item name="android:colorPrimary">@color/primary</item> + <item name="android:colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorAccent">@color/accent</item> + </style> + <attr name="custom" format="reference" /> + <style name="Pop"> + <item name="custom">@drawable/image</item> + <item name="android:focusable">@lib:bool/allow</item> + </style> + <string name="yo">@string/wow</string> + + <declare-styleable name="View"> + <attr name="custom" /> + <attr name="decor"> + <enum name="no-border" value="0"/> + <enum name="border" value="1"/> + <enum name="shadow" value="2"/> + </attr> + </declare-styleable> + +</resources> diff --git a/tools/aapt2/data/res/values/test.xml b/tools/aapt2/data/res/values/test.xml new file mode 100644 index 0000000..d3ead34 --- /dev/null +++ b/tools/aapt2/data/res/values/test.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="hooha"><font bgcolor="#ffffff">Hey guys!</font> <xliff:g>My</xliff:g> name is <b>Adam</b>. How <b><i>are</i></b> you?</string> + <public name="hooha" type="string" id="0x7f020001"/> + <string name="wow">@android:string/ok</string> + <public name="image" type="drawable" id="0x7f060000" /> + <attr name="layout_width" format="boolean" /> + <attr name="flags"> + <flag name="complex" value="1" /> + <flag name="pub" value="2" /> + <flag name="weak" value="4" /> + </attr> +</resources> diff --git a/tools/aapt2/data/resources.arsc b/tools/aapt2/data/resources.arsc Binary files differnew file mode 100644 index 0000000..6a416df --- /dev/null +++ b/tools/aapt2/data/resources.arsc diff --git a/tools/aapt2/data/resources_base.arsc b/tools/aapt2/data/resources_base.arsc Binary files differnew file mode 100644 index 0000000..f9d0610 --- /dev/null +++ b/tools/aapt2/data/resources_base.arsc diff --git a/tools/aapt2/data/resources_hdpi.arsc b/tools/aapt2/data/resources_hdpi.arsc Binary files differnew file mode 100644 index 0000000..97232a3 --- /dev/null +++ b/tools/aapt2/data/resources_hdpi.arsc diff --git a/tools/aapt2/process.dot b/tools/aapt2/process.dot new file mode 100644 index 0000000..4741952 --- /dev/null +++ b/tools/aapt2/process.dot @@ -0,0 +1,108 @@ +digraph aapt { + out_package [label="out/default/package.apk"]; + out_fr_package [label="out/fr/package.apk"]; + out_table_aligned [label="out/default/resources-aligned.arsc"]; + out_table_fr_aligned [label="out/fr/resources-aligned.arsc"]; + out_res_layout_main_xml [label="out/res/layout/main.xml"]; + out_res_layout_v21_main_xml [color=red,label="out/res/layout-v21/main.xml"]; + out_res_layout_fr_main_xml [label="out/res/layout-fr/main.xml"]; + out_res_layout_fr_v21_main_xml [color=red,label="out/res/layout-fr-v21/main.xml"]; + out_table [label="out/default/resources.arsc"]; + out_fr_table [label="out/fr/resources.arsc"]; + out_values_table [label="out/values/resources.arsc"]; + out_layout_table [label="out/layout/resources.arsc"]; + out_values_fr_table [label="out/values-fr/resources.arsc"]; + out_layout_fr_table [label="out/layout-fr/resources.arsc"]; + res_values_strings_xml [label="res/values/strings.xml"]; + res_values_attrs_xml [label="res/values/attrs.xml"]; + res_layout_main_xml [label="res/layout/main.xml"]; + res_layout_fr_main_xml [label="res/layout-fr/main.xml"]; + res_values_fr_strings_xml [label="res/values-fr/strings.xml"]; + + lib_apk_resources_arsc [label="lib.apk:resources.arsc",color=green]; + lib_apk_res_layout_main_xml [label="lib.apk:res/layout/main.xml",color=green]; + lib_apk_res_drawable_icon_png [label="lib.apk:res/drawable/icon.png",color=green]; + lib_apk_fr_res_layout_main_xml [label="lib.apk:res/layout-fr/main.xml",color=green]; + lib_apk_fr_res_drawable_icon_png [label="lib.apk:res/drawable-fr/icon.png",color=green]; + out_res_layout_lib_main_xml [label="out/res/layout/lib-main.xml"]; + + out_package -> package_default; + out_fr_package -> package_fr; + + package_default [shape=box,label="Assemble",color=blue]; + package_default -> out_table_aligned; + package_default -> out_res_layout_main_xml; + package_default -> out_res_layout_v21_main_xml [color=red]; + package_default -> out_res_layout_lib_main_xml; + + package_fr [shape=box,label="Assemble",color=blue]; + package_fr -> out_table_fr_aligned; + package_fr -> out_res_layout_fr_main_xml; + package_fr -> out_res_layout_fr_v21_main_xml [color=red]; + + out_table_aligned -> align_tables; + out_table_fr_aligned -> align_tables; + + align_tables [shape=box,label="Align",color=blue]; + align_tables -> out_table; + align_tables -> out_fr_table; + + out_table -> link_tables; + + link_tables [shape=box,label="Link",color=blue]; + link_tables -> out_values_table; + link_tables -> out_layout_table; + link_tables -> lib_apk_resources_arsc; + + out_values_table -> compile_values; + + compile_values [shape=box,label="Collect",color=blue]; + compile_values -> res_values_strings_xml; + compile_values -> res_values_attrs_xml; + + out_layout_table -> collect_xml; + + collect_xml [shape=box,label="Collect",color=blue]; + collect_xml -> res_layout_main_xml; + + out_fr_table -> link_fr_tables; + + link_fr_tables [shape=box,label="Link",color=blue]; + link_fr_tables -> out_values_fr_table; + link_fr_tables -> out_layout_fr_table; + link_fr_tables -> lib_apk_resources_arsc; + + out_values_fr_table -> compile_values_fr; + + compile_values_fr [shape=box,label="Collect",color=blue]; + compile_values_fr -> res_values_fr_strings_xml; + + out_layout_fr_table -> collect_xml_fr; + + collect_xml_fr [shape=box,label="Collect",color=blue]; + collect_xml_fr -> res_layout_fr_main_xml; + + compile_res_layout_main_xml [shape=box,label="Compile",color=blue]; + + out_res_layout_main_xml -> compile_res_layout_main_xml; + + out_res_layout_v21_main_xml -> compile_res_layout_main_xml [color=red]; + + compile_res_layout_main_xml -> res_layout_main_xml; + compile_res_layout_main_xml -> out_table_aligned; + + compile_res_layout_fr_main_xml [shape=box,label="Compile",color=blue]; + + out_res_layout_fr_main_xml -> compile_res_layout_fr_main_xml; + + out_res_layout_fr_v21_main_xml -> compile_res_layout_fr_main_xml [color=red]; + + compile_res_layout_fr_main_xml -> res_layout_fr_main_xml; + compile_res_layout_fr_main_xml -> out_table_fr_aligned; + + out_res_layout_lib_main_xml -> compile_res_layout_lib_main_xml; + + compile_res_layout_lib_main_xml [shape=box,label="Compile",color=blue]; + compile_res_layout_lib_main_xml -> out_table_aligned; + compile_res_layout_lib_main_xml -> lib_apk_res_layout_main_xml; +} diff --git a/tools/aapt2/public_attr_map.py b/tools/aapt2/public_attr_map.py new file mode 100644 index 0000000..92136a8 --- /dev/null +++ b/tools/aapt2/public_attr_map.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +import sys +import xml.etree.ElementTree as ET + +def findSdkLevelForAttribute(id): + intId = int(id, 16) + packageId = 0x000000ff & (intId >> 24) + typeId = 0x000000ff & (intId >> 16) + entryId = 0x0000ffff & intId + + if packageId != 0x01 or typeId != 0x01: + return 0 + + levels = [(1, 0x021c), (2, 0x021d), (3, 0x0269), (4, 0x028d), + (5, 0x02ad), (6, 0x02b3), (7, 0x02b5), (8, 0x02bd), + (9, 0x02cb), (11, 0x0361), (12, 0x0366), (13, 0x03a6), + (16, 0x03ae), (17, 0x03cc), (18, 0x03da), (19, 0x03f1), + (20, 0x03f6), (21, 0x04ce)] + for level, attrEntryId in levels: + if entryId <= attrEntryId: + return level + return 22 + + +tree = None +with open(sys.argv[1], 'rt') as f: + tree = ET.parse(f) + +attrs = [] +for node in tree.iter('public'): + if node.get('type') == 'attr': + sdkLevel = findSdkLevelForAttribute(node.get('id', '0')) + if sdkLevel > 1 and sdkLevel < 22: + attrs.append("{{ u\"{}\", {} }}".format(node.get('name'), sdkLevel)) + +print "#include <string>" +print "#include <unordered_map>" +print +print "namespace aapt {" +print +print "static std::unordered_map<std::u16string, size_t> sAttrMap = {" +print ",\n ".join(attrs) +print "};" +print +print "size_t findAttributeSdkLevel(const std::u16string& name) {" +print " auto iter = sAttrMap.find(name);" +print " if (iter != sAttrMap.end()) {" +print " return iter->second;" +print " }" +print " return 0;" +print "}" +print +print "} // namespace aapt" +print diff --git a/tools/aapt2/todo.txt b/tools/aapt2/todo.txt new file mode 100644 index 0000000..acc8bfb --- /dev/null +++ b/tools/aapt2/todo.txt @@ -0,0 +1,29 @@ +XML Files +X Collect declared IDs +X Build StringPool +X Flatten + +Resource Table Operations +X Build Resource Table (with StringPool) from XML. +X Modify Resource Table. +X - Copy and transform resources. +X - Pre-17/21 attr correction. +X Perform analysis of types. +X Flatten. +X Assign resource IDs. +X Assign public resource IDs. +X Merge resource tables +- Assign private attributes to different typespace. +- Align resource tables + +Splits +- Collect all resources (ids from layouts). +- Generate resource table from base resources. +- Generate resource table from individual resources of the required type. +- Align resource tables (same type/name = same ID). + +Fat Apk +X Collect all resources (ids from layouts). +X Generate resource tables for all configurations. +- Align individual resource tables. +- Merge resource tables. diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py index 393d2ec..df76bc9 100644 --- a/tools/apilint/apilint.py +++ b/tools/apilint/apilint.py @@ -26,14 +26,17 @@ $ git blame api/current.txt -t -e > /tmp/currentblame.txt $ apilint.py /tmp/currentblame.txt previous.txt --no-color """ -import re, sys, collections, traceback +import re, sys, collections, traceback, argparse BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) +ALLOW_GOOGLE = False +USE_COLOR = True + def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes - if "--no-color" in sys.argv: return "" + if not USE_COLOR: return "" codes = [] if reset: codes.append("0") else: @@ -48,8 +51,9 @@ def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): class Field(): - def __init__(self, clazz, raw, blame): + def __init__(self, clazz, line, raw, blame): self.clazz = clazz + self.line = line self.raw = raw.strip(" {;") self.blame = blame @@ -73,8 +77,9 @@ class Field(): class Method(): - def __init__(self, clazz, raw, blame): + def __init__(self, clazz, line, raw, blame): self.clazz = clazz + self.line = line self.raw = raw.strip(" {;") self.blame = blame @@ -110,8 +115,9 @@ class Method(): class Class(): - def __init__(self, pkg, raw, blame): + def __init__(self, pkg, line, raw, blame): self.pkg = pkg + self.line = line self.raw = raw.strip(" {;") self.blame = blame self.ctors = [] @@ -129,10 +135,14 @@ class Class(): if "extends" in raw: self.extends = raw[raw.index("extends")+1] + self.extends_path = self.extends.split(".") else: self.extends = None + self.extends_path = [] self.fullname = self.pkg.name + "." + self.fullname + self.fullname_path = self.fullname.split(".") + self.name = self.fullname[self.fullname.rindex(".")+1:] def __repr__(self): @@ -140,75 +150,110 @@ class Class(): class Package(): - def __init__(self, raw, blame): + def __init__(self, line, raw, blame): + self.line = line self.raw = raw.strip(" {;") self.blame = blame raw = raw.split() self.name = raw[raw.index("package")+1] + self.name_path = self.name.split(".") def __repr__(self): return self.raw -def parse_api(fn): +def _parse_stream(f, clazz_cb=None): + line = 0 api = {} pkg = None clazz = None blame = None re_blame = re.compile("^([a-z0-9]{7,}) \(<([^>]+)>.+?\) (.+?)$") - - with open(fn) as f: - for raw in f.readlines(): - raw = raw.rstrip() - match = re_blame.match(raw) - if match is not None: - blame = match.groups()[0:2] - raw = match.groups()[2] - else: - blame = None - - if raw.startswith("package"): - pkg = Package(raw, blame) - elif raw.startswith(" ") and raw.endswith("{"): - clazz = Class(pkg, raw, blame) + for raw in f: + line += 1 + raw = raw.rstrip() + match = re_blame.match(raw) + if match is not None: + blame = match.groups()[0:2] + raw = match.groups()[2] + else: + blame = None + + if raw.startswith("package"): + pkg = Package(line, raw, blame) + elif raw.startswith(" ") and raw.endswith("{"): + # When provided with class callback, we treat as incremental + # parse and don't build up entire API + if clazz and clazz_cb: + clazz_cb(clazz) + clazz = Class(pkg, line, raw, blame) + if not clazz_cb: api[clazz.fullname] = clazz - elif raw.startswith(" ctor"): - clazz.ctors.append(Method(clazz, raw, blame)) - elif raw.startswith(" method"): - clazz.methods.append(Method(clazz, raw, blame)) - elif raw.startswith(" field"): - clazz.fields.append(Field(clazz, raw, blame)) + elif raw.startswith(" ctor"): + clazz.ctors.append(Method(clazz, line, raw, blame)) + elif raw.startswith(" method"): + clazz.methods.append(Method(clazz, line, raw, blame)) + elif raw.startswith(" field"): + clazz.fields.append(Field(clazz, line, raw, blame)) + + # Handle last trailing class + if clazz and clazz_cb: + clazz_cb(clazz) return api +class Failure(): + def __init__(self, sig, clazz, detail, error, rule, msg): + self.sig = sig + self.error = error + self.rule = rule + self.msg = msg + + if error: + self.head = "Error %s" % (rule) if rule else "Error" + dump = "%s%s:%s %s" % (format(fg=RED, bg=BLACK, bold=True), self.head, format(reset=True), msg) + else: + self.head = "Warning %s" % (rule) if rule else "Warning" + dump = "%s%s:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), self.head, format(reset=True), msg) + + self.line = clazz.line + blame = clazz.blame + if detail is not None: + dump += "\n in " + repr(detail) + self.line = detail.line + blame = detail.blame + dump += "\n in " + repr(clazz) + dump += "\n in " + repr(clazz.pkg) + dump += "\n at line " + repr(self.line) + if blame is not None: + dump += "\n last modified by %s in %s" % (blame[1], blame[0]) + + self.dump = dump + + def __repr__(self): + return self.dump + + failures = {} -def _fail(clazz, detail, msg): +def _fail(clazz, detail, error, rule, msg): """Records an API failure to be processed later.""" global failures sig = "%s-%s-%s" % (clazz.fullname, repr(detail), msg) sig = sig.replace(" deprecated ", " ") - res = msg - blame = clazz.blame - if detail is not None: - res += "\n in " + repr(detail) - blame = detail.blame - res += "\n in " + repr(clazz) - res += "\n in " + repr(clazz.pkg) - if blame is not None: - res += "\n last modified by %s in %s" % (blame[1], blame[0]) - failures[sig] = res + failures[sig] = Failure(sig, clazz, detail, error, rule, msg) + -def warn(clazz, detail, msg): - _fail(clazz, detail, "%sWarning:%s %s" % (format(fg=YELLOW, bg=BLACK, bold=True), format(reset=True), msg)) +def warn(clazz, detail, rule, msg): + _fail(clazz, detail, False, rule, msg) -def error(clazz, detail, msg): - _fail(clazz, detail, "%sError:%s %s" % (format(fg=RED, bg=BLACK, bold=True), format(reset=True), msg)) +def error(clazz, detail, rule, msg): + _fail(clazz, detail, True, rule, msg) def verify_constants(clazz): @@ -218,13 +263,13 @@ def verify_constants(clazz): for f in clazz.fields: if "static" in f.split and "final" in f.split: if re.match("[A-Z0-9_]+", f.name) is None: - error(clazz, f, "Constant field names should be FOO_NAME") + error(clazz, f, "C2", "Constant field names must be FOO_NAME") def verify_enums(clazz): """Enums are bad, mmkay?""" if "extends java.lang.Enum" in clazz.raw: - error(clazz, None, "Enums are not allowed") + error(clazz, None, "F5", "Enums are not allowed") def verify_class_names(clazz): @@ -234,9 +279,9 @@ def verify_class_names(clazz): if re.match("android\.R\.[a-z]+", clazz.fullname): return if re.search("[A-Z]{2,}", clazz.name) is not None: - warn(clazz, None, "Class name style should be Mtp not MTP") + warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP") if re.match("[^A-Z]", clazz.name): - error(clazz, None, "Class must start with uppercase char") + error(clazz, None, "S1", "Class must start with uppercase char") def verify_method_names(clazz): @@ -247,9 +292,9 @@ def verify_method_names(clazz): for m in clazz.methods: if re.search("[A-Z]{2,}", m.name) is not None: - warn(clazz, m, "Method name style should be getMtu() instead of getMTU()") + warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()") if re.match("[^a-z]", m.name): - error(clazz, m, "Method name must start with lowercase char") + error(clazz, m, "S1", "Method name must start with lowercase char") def verify_callbacks(clazz): @@ -259,17 +304,17 @@ def verify_callbacks(clazz): if clazz.fullname == "android.speech.tts.SynthesisCallback": return if clazz.name.endswith("Callbacks"): - error(clazz, None, "Class name must not be plural") + error(clazz, None, "L1", "Callback class names should be singular") if clazz.name.endswith("Observer"): - warn(clazz, None, "Class should be named FooCallback") + warn(clazz, None, "L1", "Class should be named FooCallback") if clazz.name.endswith("Callback"): if "interface" in clazz.split: - error(clazz, None, "Callback must be abstract class to enable extension in future API levels") + error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels") for m in clazz.methods: if not re.match("on[A-Z][a-z]*", m.name): - error(clazz, m, "Callback method names must be onFoo() style") + error(clazz, m, "L1", "Callback method names must be onFoo() style") def verify_listeners(clazz): @@ -281,16 +326,16 @@ def verify_listeners(clazz): if clazz.name.endswith("Listener"): if " abstract class " in clazz.raw: - error(clazz, None, "Listener should be an interface, otherwise renamed Callback") + error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback") for m in clazz.methods: if not re.match("on[A-Z][a-z]*", m.name): - error(clazz, m, "Listener method names must be onFoo() style") + error(clazz, m, "L1", "Listener method names must be onFoo() style") if len(clazz.methods) == 1 and clazz.name.startswith("On"): m = clazz.methods[0] if (m.name + "Listener").lower() != clazz.name.lower(): - error(clazz, m, "Single listener method name should match class name") + error(clazz, m, "L1", "Single listener method name must match class name") def verify_actions(clazz): @@ -308,7 +353,7 @@ def verify_actions(clazz): if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower(): if not f.name.startswith("ACTION_"): - error(clazz, f, "Intent action constant name must be ACTION_FOO") + error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO") else: if clazz.fullname == "android.content.Intent": prefix = "android.intent.action" @@ -320,7 +365,7 @@ def verify_actions(clazz): prefix = clazz.pkg.name + ".action" expected = prefix + "." + f.name[7:] if f.value != expected: - error(clazz, f, "Inconsistent action value; expected %s" % (expected)) + error(clazz, f, "C4", "Inconsistent action value; expected %s" % (expected)) def verify_extras(clazz): @@ -340,7 +385,7 @@ def verify_extras(clazz): if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower(): if not f.name.startswith("EXTRA_"): - error(clazz, f, "Intent extra must be EXTRA_FOO") + error(clazz, f, "C3", "Intent extra must be EXTRA_FOO") else: if clazz.pkg.name == "android.content" and clazz.name == "Intent": prefix = "android.intent.extra" @@ -350,7 +395,7 @@ def verify_extras(clazz): prefix = clazz.pkg.name + ".extra" expected = prefix + "." + f.name[6:] if f.value != expected: - error(clazz, f, "Inconsistent extra value; expected %s" % (expected)) + error(clazz, f, "C4", "Inconsistent extra value; expected %s" % (expected)) def verify_equals(clazz): @@ -359,7 +404,7 @@ def verify_equals(clazz): eq = "equals" in methods hc = "hashCode" in methods if eq != hc: - error(clazz, None, "Must override both equals and hashCode; missing one") + error(clazz, None, "M8", "Must override both equals and hashCode; missing one") def verify_parcelable(clazz): @@ -370,17 +415,17 @@ def verify_parcelable(clazz): describe = [ i for i in clazz.methods if i.name == "describeContents" ] if len(creator) == 0 or len(write) == 0 or len(describe) == 0: - error(clazz, None, "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one") + error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one") def verify_protected(clazz): - """Verify that no protected methods are allowed.""" + """Verify that no protected methods or fields are allowed.""" for m in clazz.methods: if "protected" in m.split: - error(clazz, m, "No protected methods; must be public") + error(clazz, m, "M7", "Protected methods not allowed; must be public") for f in clazz.fields: if "protected" in f.split: - error(clazz, f, "No protected fields; must be public") + error(clazz, f, "M7", "Protected fields not allowed; must be public") def verify_fields(clazz): @@ -410,18 +455,18 @@ def verify_fields(clazz): elif clazz.fullname.startswith("android.util.Mutable"): pass else: - error(clazz, f, "Bare fields must be marked final; consider adding accessors") + error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable") if not "static" in f.split: if not re.match("[a-z]([a-zA-Z]+)?", f.name): - error(clazz, f, "Non-static fields must be named with myField style") + error(clazz, f, "S1", "Non-static fields must be named using myField style") if re.match("[ms][A-Z]", f.name): - error(clazz, f, "Don't expose your internal objects") + error(clazz, f, "F1", "Internal objects must not be exposed") if re.match("[A-Z_]+", f.name): if "static" not in f.split or "final" not in f.split: - error(clazz, f, "Constants must be marked static final") + error(clazz, f, "C2", "Constants must be marked static final") def verify_register(clazz): @@ -434,34 +479,34 @@ def verify_register(clazz): if m.name.startswith("register"): other = "unregister" + m.name[8:] if other not in methods: - error(clazz, m, "Missing unregister method") + error(clazz, m, "L2", "Missing unregister method") if m.name.startswith("unregister"): other = "register" + m.name[10:] if other not in methods: - error(clazz, m, "Missing register method") + error(clazz, m, "L2", "Missing register method") if m.name.startswith("add") or m.name.startswith("remove"): - error(clazz, m, "Callback methods should be named register/unregister") + error(clazz, m, "L3", "Callback methods should be named register/unregister") if "Listener" in m.raw: if m.name.startswith("add"): other = "remove" + m.name[3:] if other not in methods: - error(clazz, m, "Missing remove method") + error(clazz, m, "L2", "Missing remove method") if m.name.startswith("remove") and not m.name.startswith("removeAll"): other = "add" + m.name[6:] if other not in methods: - error(clazz, m, "Missing add method") + error(clazz, m, "L2", "Missing add method") if m.name.startswith("register") or m.name.startswith("unregister"): - error(clazz, m, "Listener methods should be named add/remove") + error(clazz, m, "L3", "Listener methods should be named add/remove") def verify_sync(clazz): """Verify synchronized methods aren't exposed.""" for m in clazz.methods: if "synchronized" in m.split: - error(clazz, m, "Internal lock exposed") + error(clazz, m, "M5", "Internal locks must not be exposed") def verify_intent_builder(clazz): @@ -473,7 +518,7 @@ def verify_intent_builder(clazz): if m.name.startswith("create") and m.name.endswith("Intent"): pass else: - error(clazz, m, "Methods creating an Intent should be named createFooIntent()") + warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()") def verify_helper_classes(clazz): @@ -483,57 +528,45 @@ def verify_helper_classes(clazz): if "extends android.app.Service" in clazz.raw: test_methods = True if not clazz.name.endswith("Service"): - error(clazz, None, "Inconsistent class name; should be FooService") + error(clazz, None, "CL4", "Inconsistent class name; should be FooService") found = False for f in clazz.fields: if f.name == "SERVICE_INTERFACE": found = True if f.value != clazz.fullname: - error(clazz, f, "Inconsistent interface constant; expected %s" % (clazz.fullname)) - - if not found: - warn(clazz, None, "Missing SERVICE_INTERFACE constant") - - if "abstract" in clazz.split and not clazz.fullname.startswith("android.service."): - warn(clazz, None, "Services extended by developers should be under android.service") + error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) if "extends android.content.ContentProvider" in clazz.raw: test_methods = True if not clazz.name.endswith("Provider"): - error(clazz, None, "Inconsistent class name; should be FooProvider") + error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider") found = False for f in clazz.fields: if f.name == "PROVIDER_INTERFACE": found = True if f.value != clazz.fullname: - error(clazz, f, "Inconsistent interface name; expected %s" % (clazz.fullname)) - - if not found: - warn(clazz, None, "Missing PROVIDER_INTERFACE constant") - - if "abstract" in clazz.split and not clazz.fullname.startswith("android.provider."): - warn(clazz, None, "Providers extended by developers should be under android.provider") + error(clazz, f, "C4", "Inconsistent interface constant; expected %s" % (clazz.fullname)) if "extends android.content.BroadcastReceiver" in clazz.raw: test_methods = True if not clazz.name.endswith("Receiver"): - error(clazz, None, "Inconsistent class name; should be FooReceiver") + error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver") if "extends android.app.Activity" in clazz.raw: test_methods = True if not clazz.name.endswith("Activity"): - error(clazz, None, "Inconsistent class name; should be FooActivity") + error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity") if test_methods: for m in clazz.methods: if "final" in m.split: continue if not re.match("on[A-Z]", m.name): if "abstract" in m.split: - error(clazz, m, "Methods implemented by developers must be named onFoo()") + warn(clazz, m, None, "Methods implemented by developers should be named onFoo()") else: - warn(clazz, m, "If implemented by developer, should be named onFoo(); otherwise consider marking final") + warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final") def verify_builder(clazz): @@ -543,7 +576,7 @@ def verify_builder(clazz): if not clazz.name.endswith("Builder"): return if clazz.name != "Builder": - warn(clazz, None, "Builder should be defined as inner class") + warn(clazz, None, None, "Builder should be defined as inner class") has_build = False for m in clazz.methods: @@ -555,26 +588,26 @@ def verify_builder(clazz): if m.name.startswith("clear"): continue if m.name.startswith("with"): - error(clazz, m, "Builder methods names must follow setFoo() style") + warn(clazz, m, None, "Builder methods names should use setFoo() style") if m.name.startswith("set"): if not m.typ.endswith(clazz.fullname): - warn(clazz, m, "Methods should return the builder") + warn(clazz, m, "M4", "Methods must return the builder object") if not has_build: - warn(clazz, None, "Missing build() method") + warn(clazz, None, None, "Missing build() method") def verify_aidl(clazz): """Catch people exposing raw AIDL.""" if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw: - error(clazz, None, "Exposing raw AIDL interface") + error(clazz, None, None, "Raw AIDL interfaces must not be exposed") def verify_internal(clazz): """Catch people exposing internal classes.""" if clazz.pkg.name.startswith("com.android"): - error(clazz, None, "Exposing internal class") + error(clazz, None, None, "Internal classes must not be exposed") def verify_layering(clazz): @@ -609,38 +642,56 @@ def verify_layering(clazz): for f in clazz.fields: ir = rank(f.typ) if ir and ir < cr: - warn(clazz, f, "Field type violates package layering") + warn(clazz, f, "FW6", "Field type violates package layering") for m in clazz.methods: ir = rank(m.typ) if ir and ir < cr: - warn(clazz, m, "Method return type violates package layering") + warn(clazz, m, "FW6", "Method return type violates package layering") for arg in m.args: ir = rank(arg) if ir and ir < cr: - warn(clazz, m, "Method argument type violates package layering") + warn(clazz, m, "FW6", "Method argument type violates package layering") -def verify_boolean(clazz, api): - """Catches people returning boolean from getFoo() style methods. - Ignores when matching setFoo() is present.""" +def verify_boolean(clazz): + """Verifies that boolean accessors are named correctly. + For example, hasFoo() and setHasFoo().""" - methods = [ m.name for m in clazz.methods ] + def is_get(m): return len(m.args) == 0 and m.typ == "boolean" + def is_set(m): return len(m.args) == 1 and m.args[0] == "boolean" + + gets = [ m for m in clazz.methods if is_get(m) ] + sets = [ m for m in clazz.methods if is_set(m) ] - builder = clazz.fullname + ".Builder" - builder_methods = [] - if builder in api: - builder_methods = [ m.name for m in api[builder].methods ] + def error_if_exists(methods, trigger, expected, actual): + for m in methods: + if m.name == actual: + error(clazz, m, "M6", "Symmetric method for %s must be named %s" % (trigger, expected)) for m in clazz.methods: - if m.typ == "boolean" and m.name.startswith("get") and m.name != "get" and len(m.args) == 0: - setter = "set" + m.name[3:] - if setter in methods: - pass - elif builder is not None and setter in builder_methods: - pass - else: - warn(clazz, m, "Methods returning boolean should be named isFoo, hasFoo, areFoo") + if is_get(m): + if re.match("is[A-Z]", m.name): + target = m.name[2:] + expected = "setIs" + target + error_if_exists(sets, m.name, expected, "setHas" + target) + elif re.match("has[A-Z]", m.name): + target = m.name[3:] + expected = "setHas" + target + error_if_exists(sets, m.name, expected, "setIs" + target) + error_if_exists(sets, m.name, expected, "set" + target) + elif re.match("get[A-Z]", m.name): + target = m.name[3:] + expected = "set" + target + error_if_exists(sets, m.name, expected, "setIs" + target) + error_if_exists(sets, m.name, expected, "setHas" + target) + + if is_set(m): + if re.match("set[A-Z]", m.name): + target = m.name[3:] + expected = "get" + target + error_if_exists(sets, m.name, expected, "is" + target) + error_if_exists(sets, m.name, expected, "has" + target) def verify_collections(clazz): @@ -651,10 +702,10 @@ def verify_collections(clazz): "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"] for m in clazz.methods: if m.typ in bad: - error(clazz, m, "Return type is concrete collection; should be interface") + error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface") for arg in m.args: if arg in bad: - error(clazz, m, "Argument is concrete collection; should be interface") + error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface") def verify_flags(clazz): @@ -669,49 +720,291 @@ def verify_flags(clazz): scope = f.name[0:f.name.index("FLAG_")] if val & known[scope]: - warn(clazz, f, "Found overlapping flag constant value") + warn(clazz, f, "C1", "Found overlapping flag constant value") known[scope] |= val -def verify_style(api): - """Find all style issues in the given API level.""" +def verify_exception(clazz): + """Verifies that methods don't throw generic exceptions.""" + for m in clazz.methods: + if "throws java.lang.Exception" in m.raw or "throws java.lang.Throwable" in m.raw or "throws java.lang.Error" in m.raw: + error(clazz, m, "S1", "Methods must not throw generic exceptions") + + +def verify_google(clazz): + """Verifies that APIs never reference Google.""" + + if re.search("google", clazz.raw, re.IGNORECASE): + error(clazz, None, None, "Must never reference Google") + + test = [] + test.extend(clazz.ctors) + test.extend(clazz.fields) + test.extend(clazz.methods) + + for t in test: + if re.search("google", t.raw, re.IGNORECASE): + error(clazz, t, None, "Must never reference Google") + + +def verify_bitset(clazz): + """Verifies that we avoid using heavy BitSet.""" + + for f in clazz.fields: + if f.typ == "java.util.BitSet": + error(clazz, f, None, "Field type must not be heavy BitSet") + + for m in clazz.methods: + if m.typ == "java.util.BitSet": + error(clazz, m, None, "Return type must not be heavy BitSet") + for arg in m.args: + if arg == "java.util.BitSet": + error(clazz, m, None, "Argument type must not be heavy BitSet") + + +def verify_manager(clazz): + """Verifies that FooManager is only obtained from Context.""" + + if not clazz.name.endswith("Manager"): return + + for c in clazz.ctors: + error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors") + + +def verify_boxed(clazz): + """Verifies that methods avoid boxed primitives.""" + + boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"] + + for c in clazz.ctors: + for arg in c.args: + if arg in boxed: + error(clazz, c, "M11", "Must avoid boxed primitives") + + for f in clazz.fields: + if f.typ in boxed: + error(clazz, f, "M11", "Must avoid boxed primitives") + + for m in clazz.methods: + if m.typ in boxed: + error(clazz, m, "M11", "Must avoid boxed primitives") + for arg in m.args: + if arg in boxed: + error(clazz, m, "M11", "Must avoid boxed primitives") + + +def verify_static_utils(clazz): + """Verifies that helper classes can't be constructed.""" + if clazz.fullname.startswith("android.opengl"): return + if re.match("android\.R\.[a-z]+", clazz.fullname): return + + if len(clazz.fields) > 0: return + if len(clazz.methods) == 0: return + + for m in clazz.methods: + if "static" not in m.split: + return + + # At this point, we have no fields, and all methods are static + if len(clazz.ctors) > 0: + error(clazz, None, None, "Fully-static utility classes must not have constructor") + + +def verify_overload_args(clazz): + """Verifies that method overloads add new arguments at the end.""" + if clazz.fullname.startswith("android.opengl"): return + + overloads = collections.defaultdict(list) + for m in clazz.methods: + if "deprecated" in m.split: continue + overloads[m.name].append(m) + + for name, methods in overloads.items(): + if len(methods) <= 1: continue + + # Look for arguments common across all overloads + def cluster(args): + count = collections.defaultdict(int) + res = set() + for i in range(len(args)): + a = args[i] + res.add("%s#%d" % (a, count[a])) + count[a] += 1 + return res + + common_args = cluster(methods[0].args) + for m in methods: + common_args = common_args & cluster(m.args) + + if len(common_args) == 0: continue + + # Require that all common arguments are present at start of signature + locked_sig = None + for m in methods: + sig = m.args[0:len(common_args)] + if not common_args.issubset(cluster(sig)): + warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args))) + elif not locked_sig: + locked_sig = sig + elif locked_sig != sig: + error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig))) + + +def verify_callback_handlers(clazz): + """Verifies that methods adding listener/callback have overload + for specifying delivery thread.""" + + # Ignore UI packages which assume main thread + skip = [ + "animation", + "view", + "graphics", + "transition", + "widget", + "webkit", + ] + for s in skip: + if s in clazz.pkg.name_path: return + if s in clazz.extends_path: return + + # Ignore UI classes which assume main thread + if "app" in clazz.pkg.name_path or "app" in clazz.extends_path: + for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]: + if s in clazz.fullname: return + if "content" in clazz.pkg.name_path or "content" in clazz.extends_path: + for s in ["Loader"]: + if s in clazz.fullname: return + + found = {} + by_name = collections.defaultdict(list) + for m in clazz.methods: + if m.name.startswith("unregister"): continue + if m.name.startswith("remove"): continue + if re.match("on[A-Z]+", m.name): continue + + by_name[m.name].append(m) + + for a in m.args: + if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"): + found[m.name] = m + + for f in found.values(): + takes_handler = False + for m in by_name[f.name]: + if "android.os.Handler" in m.args: + takes_handler = True + if not takes_handler: + warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Handler") + + +def verify_context_first(clazz): + """Verifies that methods accepting a Context keep it the first argument.""" + examine = clazz.ctors + clazz.methods + for m in examine: + if len(m.args) > 1 and m.args[0] != "android.content.Context": + if "android.content.Context" in m.args[1:]: + error(clazz, m, "M3", "Context is distinct, so it must be the first argument") + + +def verify_listener_last(clazz): + """Verifies that methods accepting a Listener or Callback keep them as last arguments.""" + examine = clazz.ctors + clazz.methods + for m in examine: + if "Listener" in m.name or "Callback" in m.name: continue + found = False + for a in m.args: + if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"): + found = True + elif found and a != "android.os.Handler": + warn(clazz, m, "M3", "Listeners should always be at end of argument list") + + +def verify_resource_names(clazz): + """Verifies that resource names have consistent case.""" + if not re.match("android\.R\.[a-z]+", clazz.fullname): return + + # Resources defined by files are foo_bar_baz + if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]: + for f in clazz.fields: + if re.match("[a-z1-9_]+$", f.name): continue + error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style") + + # Resources defined inside files are fooBarBaz + if clazz.name in ["array","attr","id","bool","fraction","integer"]: + for f in clazz.fields: + if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue + if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue + if re.match("state_[a-z_]*$", f.name): continue + + if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue + error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style") + + # Styles are FooBar_Baz + if clazz.name in ["style"]: + for f in clazz.fields: + if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue + error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style") + + +def examine_clazz(clazz): + """Find all style issues in the given class.""" + if clazz.pkg.name.startswith("java"): return + if clazz.pkg.name.startswith("junit"): return + if clazz.pkg.name.startswith("org.apache"): return + if clazz.pkg.name.startswith("org.xml"): return + if clazz.pkg.name.startswith("org.json"): return + if clazz.pkg.name.startswith("org.w3c"): return + + verify_constants(clazz) + verify_enums(clazz) + verify_class_names(clazz) + verify_method_names(clazz) + verify_callbacks(clazz) + verify_listeners(clazz) + verify_actions(clazz) + verify_extras(clazz) + verify_equals(clazz) + verify_parcelable(clazz) + verify_protected(clazz) + verify_fields(clazz) + verify_register(clazz) + verify_sync(clazz) + verify_intent_builder(clazz) + verify_helper_classes(clazz) + verify_builder(clazz) + verify_aidl(clazz) + verify_internal(clazz) + verify_layering(clazz) + verify_boolean(clazz) + verify_collections(clazz) + verify_flags(clazz) + verify_exception(clazz) + if not ALLOW_GOOGLE: verify_google(clazz) + verify_bitset(clazz) + verify_manager(clazz) + verify_boxed(clazz) + verify_static_utils(clazz) + verify_overload_args(clazz) + verify_callback_handlers(clazz) + verify_context_first(clazz) + verify_listener_last(clazz) + verify_resource_names(clazz) + + +def examine_stream(stream): + """Find all style issues in the given API stream.""" global failures + failures = {} + _parse_stream(stream, examine_clazz) + return failures + +def examine_api(api): + """Find all style issues in the given parsed API.""" + global failures failures = {} for key in sorted(api.keys()): - clazz = api[key] - - if clazz.pkg.name.startswith("java"): continue - if clazz.pkg.name.startswith("junit"): continue - if clazz.pkg.name.startswith("org.apache"): continue - if clazz.pkg.name.startswith("org.xml"): continue - if clazz.pkg.name.startswith("org.json"): continue - if clazz.pkg.name.startswith("org.w3c"): continue - - verify_constants(clazz) - verify_enums(clazz) - verify_class_names(clazz) - verify_method_names(clazz) - verify_callbacks(clazz) - verify_listeners(clazz) - verify_actions(clazz) - verify_extras(clazz) - verify_equals(clazz) - verify_parcelable(clazz) - verify_protected(clazz) - verify_fields(clazz) - verify_register(clazz) - verify_sync(clazz) - verify_intent_builder(clazz) - verify_helper_classes(clazz) - verify_builder(clazz) - verify_aidl(clazz) - verify_internal(clazz) - verify_layering(clazz) - verify_boolean(clazz, api) - verify_collections(clazz) - verify_flags(clazz) - + examine_clazz(api[key]) return failures @@ -749,49 +1042,71 @@ def verify_compat(cur, prev): prev_clazz = prev[key] if not class_exists(cur, prev_clazz): - error(prev_clazz, None, "Class removed or incompatible change") + error(prev_clazz, None, None, "Class removed or incompatible change") continue cur_clazz = cur[key] for test in prev_clazz.ctors: if not ctor_exists(cur, cur_clazz, test): - error(prev_clazz, prev_ctor, "Constructor removed or incompatible change") + error(prev_clazz, prev_ctor, None, "Constructor removed or incompatible change") methods = all_methods(prev, prev_clazz) for test in methods: if not method_exists(cur, cur_clazz, test): - error(prev_clazz, test, "Method removed or incompatible change") + error(prev_clazz, test, None, "Method removed or incompatible change") for test in prev_clazz.fields: if not field_exists(cur, cur_clazz, test): - error(prev_clazz, test, "Field removed or incompatible change") + error(prev_clazz, test, None, "Field removed or incompatible change") return failures -cur = parse_api(sys.argv[1]) -cur_fail = verify_style(cur) - -if len(sys.argv) > 2: - prev = parse_api(sys.argv[2]) - prev_fail = verify_style(prev) - - # ignore errors from previous API level - for p in prev_fail: - if p in cur_fail: - del cur_fail[p] - - # look for compatibility issues - compat_fail = verify_compat(cur, prev) - - print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) - for f in sorted(compat_fail): - print compat_fail[f] +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Enforces common Android public API design \ + patterns. It ignores lint messages from a previous API level, if provided.") + parser.add_argument("current.txt", type=argparse.FileType('r'), help="current.txt") + parser.add_argument("previous.txt", nargs='?', type=argparse.FileType('r'), default=None, + help="previous.txt") + parser.add_argument("--no-color", action='store_const', const=True, + help="Disable terminal colors") + parser.add_argument("--allow-google", action='store_const', const=True, + help="Allow references to Google") + args = vars(parser.parse_args()) + + if args['no_color']: + USE_COLOR = False + + if args['allow_google']: + ALLOW_GOOGLE = True + + current_file = args['current.txt'] + previous_file = args['previous.txt'] + + with current_file as f: + cur_fail = examine_stream(f) + if not previous_file is None: + with previous_file as f: + prev_fail = examine_stream(f) + + # ignore errors from previous API level + for p in prev_fail: + if p in cur_fail: + del cur_fail[p] + + """ + # NOTE: disabled because of memory pressure + # look for compatibility issues + compat_fail = verify_compat(cur, prev) + + print "%s API compatibility issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) + for f in sorted(compat_fail): + print compat_fail[f] + print + """ + + print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) + for f in sorted(cur_fail): + print cur_fail[f] print - - -print "%s API style issues %s\n" % ((format(fg=WHITE, bg=BLUE, bold=True), format(reset=True))) -for f in sorted(cur_fail): - print cur_fail[f] - print diff --git a/tools/layoutlib/.idea/libraries/asm_4_0.xml b/tools/layoutlib/.idea/libraries/asm_4_0.xml deleted file mode 100644 index 7df287f..0000000 --- a/tools/layoutlib/.idea/libraries/asm_4_0.xml +++ /dev/null @@ -1,11 +0,0 @@ -<component name="libraryTable"> - <library name="asm-4.0"> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/asm/asm-4.0.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/asm/src.zip!/" /> - </SOURCES> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/libraries/guava.xml b/tools/layoutlib/.idea/libraries/guava.xml deleted file mode 100644 index eb60719..0000000 --- a/tools/layoutlib/.idea/libraries/guava.xml +++ /dev/null @@ -1,11 +0,0 @@ -<component name="libraryTable"> - <library name="guava"> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/15.0/guava-15.0.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/15.0/guava-15.0-sources.jar!/" /> - </SOURCES> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/libraries/icu4j.xml b/tools/layoutlib/.idea/libraries/icu4j.xml deleted file mode 100644 index dbe0bd7..0000000 --- a/tools/layoutlib/.idea/libraries/icu4j.xml +++ /dev/null @@ -1,11 +0,0 @@ -<component name="libraryTable"> - <library name="icu4j"> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/icu4j/icu4j.jar!/" /> - </CLASSES> - <JAVADOC> - <root url="http://icu-project.org/apiref/icu4j50rc/" /> - </JAVADOC> - <SOURCES /> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/libraries/kxml2_2_3_0.xml b/tools/layoutlib/.idea/libraries/kxml2_2_3_0.xml deleted file mode 100644 index 2a65050..0000000 --- a/tools/layoutlib/.idea/libraries/kxml2_2_3_0.xml +++ /dev/null @@ -1,11 +0,0 @@ -<component name="libraryTable"> - <library name="kxml2-2.3.0"> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/kxml2/kxml2-2.3.0.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES> - <root url="file://$PROJECT_DIR$/../../../../libcore/xml/src/main/java" /> - </SOURCES> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/libraries/ninepatch_prebuilt.xml b/tools/layoutlib/.idea/libraries/ninepatch_prebuilt.xml deleted file mode 100644 index f34f7dd..0000000 --- a/tools/layoutlib/.idea/libraries/ninepatch_prebuilt.xml +++ /dev/null @@ -1,11 +0,0 @@ -<component name="libraryTable"> - <library name="ninepatch-prebuilt"> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/ninepatch/ninepatch-prebuilt.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES> - <root url="file://$ANDROID_SRC$/tools/base/ninepatch/src/main/java" /> - </SOURCES> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/libraries/tools_common_prebuilt.xml b/tools/layoutlib/.idea/libraries/tools_common_prebuilt.xml deleted file mode 100644 index b325ad4..0000000 --- a/tools/layoutlib/.idea/libraries/tools_common_prebuilt.xml +++ /dev/null @@ -1,14 +0,0 @@ -<component name="libraryTable"> - <library name="tools-common-prebuilt"> - <ANNOTATIONS> - <root url="file://$PROJECT_DIR$" /> - </ANNOTATIONS> - <CLASSES> - <root url="jar://$PROJECT_DIR$/../../../../prebuilts/misc/common/tools-common/tools-common-prebuilt.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES> - <root url="file://$ANDROID_SRC$/tools/base/common/src/main/java" /> - </SOURCES> - </library> -</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/misc.xml b/tools/layoutlib/.idea/misc.xml index 94bcd36..b474bdc 100644 --- a/tools/layoutlib/.idea/misc.xml +++ b/tools/layoutlib/.idea/misc.xml @@ -10,27 +10,29 @@ <type id="android" /> </component> <component name="NullableNotNullManager"> - <option name="myDefaultNullable" value="com.android.annotations.Nullable" /> - <option name="myDefaultNotNull" value="com.android.annotations.NonNull" /> + <option name="myDefaultNullable" value="android.annotation.Nullable" /> + <option name="myDefaultNotNull" value="android.annotation.NonNull" /> <option name="myNullables"> <value> - <list size="5"> + <list size="6"> <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" /> <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" /> <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" /> <item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" /> <item index="4" class="java.lang.String" itemvalue="com.android.annotations.Nullable" /> + <item index="5" class="java.lang.String" itemvalue="android.annotation.Nullable" /> </list> </value> </option> <option name="myNotNulls"> <value> - <list size="5"> + <list size="6"> <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" /> <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" /> <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" /> <item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" /> <item index="4" class="java.lang.String" itemvalue="com.android.annotations.NonNull" /> + <item index="5" class="java.lang.String" itemvalue="android.annotation.NonNull" /> </list> </value> </option> diff --git a/tools/layoutlib/.idea/runConfigurations/Create.xml b/tools/layoutlib/.idea/runConfigurations/Create.xml index ff173e5..58f057a 100644 --- a/tools/layoutlib/.idea/runConfigurations/Create.xml +++ b/tools/layoutlib/.idea/runConfigurations/Create.xml @@ -3,7 +3,7 @@ <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" /> <option name="MAIN_CLASS_NAME" value="com.android.tools.layoutlib.create.Main" /> <option name="VM_PARAMETERS" value="" /> - <option name="PROGRAM_PARAMETERS" value="out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/ext_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/ext_intermediates/javalib.jar" /> + <option name="PROGRAM_PARAMETERS" value="out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/icu4j-icudata-jarjar_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/icu4j-icutzdata-jarjar_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/ext_intermediates/classes.jar out/target/common/obj/JAVA_LIBRARIES/ext_intermediates/javalib.jar" /> <option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$/../../../../" /> <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" /> <option name="ALTERNATIVE_JRE_PATH" value="" /> diff --git a/tools/layoutlib/Android.mk b/tools/layoutlib/Android.mk index 9300401..61ddb04 100644 --- a/tools/layoutlib/Android.mk +++ b/tools/layoutlib/Android.mk @@ -37,6 +37,10 @@ built_ext_dep := $(call java-lib-deps,ext) built_ext_classes := $(call java-lib-files,ext) built_ext_data := $(call intermediates-dir-for, \ JAVA_LIBRARIES,ext,,COMMON)/javalib.jar +built_icudata_dep := $(call java-lib-deps,icu4j-icudata-jarjar) +built_icudata_data := $(call java-lib-files,icu4j-icudata-jarjar) +built_icutzdata_dep := $(call java-lib-deps,icu4j-icutzdata-jarjar) +built_icutzdata_data := $(call java-lib-files,icu4j-icutzdata-jarjar) built_layoutlib_create_jar := $(call intermediates-dir-for, \ JAVA_LIBRARIES,layoutlib_create,HOST)/javalib.jar @@ -56,6 +60,8 @@ $(LOCAL_BUILT_MODULE): $(built_core_dep) \ $(built_framework_dep) \ $(built_ext_dep) \ $(built_ext_data) \ + $(built_icudata_dep) \ + $(built_icutzdata_dep) \ $(built_layoutlib_create_jar) $(hide) echo "host layoutlib_create: $@" $(hide) mkdir -p $(dir $@) @@ -66,6 +72,8 @@ $(LOCAL_BUILT_MODULE): $(built_core_dep) \ $(built_core_classes) \ $(built_framework_classes) \ $(built_ext_classes) \ + $(built_icudata_data) \ + $(built_icutzdata_data) \ $(built_ext_data) $(hide) ls -l $(built_framework_classes) diff --git a/tools/layoutlib/bridge/Android.mk b/tools/layoutlib/bridge/Android.mk index cfd597e..0dbdd56 100644 --- a/tools/layoutlib/bridge/Android.mk +++ b/tools/layoutlib/bridge/Android.mk @@ -22,8 +22,6 @@ LOCAL_JAVACFLAGS := -source 6 -target 6 LOCAL_JAVA_LIBRARIES := \ - kxml2-2.3.0 \ - icu4j \ layoutlib_api-prebuilt \ tools-common-prebuilt diff --git a/tools/layoutlib/bridge/bridge.iml b/tools/layoutlib/bridge/bridge.iml index 0f96916..af2fe7f 100644 --- a/tools/layoutlib/bridge/bridge.iml +++ b/tools/layoutlib/bridge/bridge.iml @@ -24,15 +24,57 @@ </content> <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="icu4j" level="project" /> - <orderEntry type="library" name="kxml2-2.3.0" level="project" /> <orderEntry type="library" name="layoutlib_api-prebuilt" level="project" /> - <orderEntry type="library" name="ninepatch-prebuilt" level="project" /> - <orderEntry type="library" name="tools-common-prebuilt" level="project" /> + <orderEntry type="module-library"> + <library name="ninepatch-prebuilt"> + <CLASSES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/ninepatch/ninepatch-prebuilt.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES> + <root url="file://$ANDROID_SRC$/tools/base/ninepatch/src/main/java" /> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library"> + <library name="tools-common-prebuilt"> + <ANNOTATIONS> + <root url="file://$MODULE_DIR$/.." /> + </ANNOTATIONS> + <CLASSES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/tools-common/tools-common-prebuilt.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES> + <root url="file://$ANDROID_SRC$/tools/base/common/src/main/java" /> + </SOURCES> + </library> + </orderEntry> <orderEntry type="library" name="framework.jar" level="project" /> - <orderEntry type="library" scope="TEST" name="guava" level="project" /> <orderEntry type="module-library" scope="TEST"> - <library> + <library name="kxml2-2.3.0"> + <CLASSES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/kxml2/kxml2-2.3.0.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES> + <root url="file://$MODULE_DIR$/../../../../../libcore/xml/src/main/java" /> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library name="guava"> + <CLASSES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/15.0/guava-15.0.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/15.0/guava-15.0-sources.jar!/" /> + </SOURCES> + </library> + </orderEntry> + <orderEntry type="module-library" scope="TEST"> + <library name="sdk-common"> <CLASSES> <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/sdk-common/sdk-common.jar!/" /> </CLASSES> @@ -44,5 +86,4 @@ </orderEntry> <orderEntry type="library" scope="TEST" name="JUnit4" level="application" /> </component> -</module> - +</module>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_battery_100.png b/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_battery_100.png Binary files differdeleted file mode 100644 index f17189a..0000000 --- a/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_battery_100.png +++ /dev/null diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_battery_100.png b/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_battery_100.png Binary files differdeleted file mode 100644 index 2a9757d..0000000 --- a/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_battery_100.png +++ /dev/null diff --git a/tools/layoutlib/bridge/resources/bars/navigation_bar.xml b/tools/layoutlib/bridge/resources/bars/navigation_bar.xml index 599ca08..55bd1d2 100644 --- a/tools/layoutlib/bridge/resources/bars/navigation_bar.xml +++ b/tools/layoutlib/bridge/resources/bars/navigation_bar.xml @@ -1,20 +1,49 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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. + --> + <merge xmlns:android="http://schemas.android.com/apk/res/android"> - <TextView + <View android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1"/> + android:visibility="invisible"/> <ImageView android:layout_height="wrap_content" - android:layout_width="wrap_content"/> + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_weight="1" + android:visibility="invisible"/> <ImageView android:layout_height="wrap_content" - android:layout_width="wrap_content"/> + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_weight="1" + android:visibility="invisible"/> <ImageView android:layout_height="wrap_content" - android:layout_width="wrap_content"/> - <TextView + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1"/> + android:visibility="invisible"/> </merge> diff --git a/tools/layoutlib/bridge/resources/bars/navigation_bar600dp.xml b/tools/layoutlib/bridge/resources/bars/navigation_bar600dp.xml new file mode 100644 index 0000000..e208a0d --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/navigation_bar600dp.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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. + --> + +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <View + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:visibility="invisible"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:visibility="invisible"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:visibility="invisible"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="centerInside"/> + <View + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:visibility="invisible"/> +</merge> diff --git a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_back.png Binary files differindex b28624f..d2760bb 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_home.png Binary files differindex 3f3e288..df43e21 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_home.png +++ b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_recent.png Binary files differindex 06dcd20..6fab1d6 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_recent.png +++ b/tools/layoutlib/bridge/resources/bars/v21/hdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-hdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-hdpi/ic_sysbar_back.png Binary files differindex e464347..2fcfdde 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-hdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-hdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-mdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-mdpi/ic_sysbar_back.png Binary files differindex 1b578a6..48708a5 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-mdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-mdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xhdpi/ic_sysbar_back.png Binary files differindex 373e84a..3d73184 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xhdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xhdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xxhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xxhdpi/ic_sysbar_back.png Binary files differindex 6b19593..786935d 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xxhdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/ldrtl-xxhdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_back.png Binary files differindex f878093..1d8c3af 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_home.png Binary files differindex 8e9583b..66de0ec 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_home.png +++ b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_recent.png Binary files differindex e2a89c3..30c65f5 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_recent.png +++ b/tools/layoutlib/bridge/resources/bars/v21/mdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_back.png Binary files differindex ec2951d..a356285 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_home.png Binary files differindex 254f757..ba2d0b2 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_home.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_recent.png Binary files differindex 8a8e941..94a74b1 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_recent.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xhdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_back.png Binary files differindex 77969b8..29da099 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_back.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_home.png Binary files differindex d60229f..59b32f2 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_home.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_recent.png Binary files differindex a261f85..ba66d27 100644 --- a/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_recent.png +++ b/tools/layoutlib/bridge/resources/bars/v21/xxhdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/xhdpi/stat_sys_battery_100.png b/tools/layoutlib/bridge/resources/bars/xhdpi/stat_sys_battery_100.png Binary files differdeleted file mode 100644 index 555bcd9..0000000 --- a/tools/layoutlib/bridge/resources/bars/xhdpi/stat_sys_battery_100.png +++ /dev/null diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java index 2c2c672..163fbcb 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java @@ -16,6 +16,8 @@ package android.content.res; +import com.android.SdkConstants; +import com.android.ide.common.rendering.api.ArrayResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.LayoutlibCallback; import com.android.ide.common.rendering.api.ResourceValue; @@ -32,6 +34,8 @@ import com.android.util.Pair; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -42,6 +46,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; +import java.util.Iterator; /** * @@ -169,7 +174,7 @@ public final class BridgeResources extends Resources { } @Override - public int getColor(int id) throws NotFoundException { + public int getColor(int id, Theme theme) throws NotFoundException { Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); if (value != null) { @@ -192,22 +197,21 @@ public final class BridgeResources extends Resources { } } - // id was not found or not resolved. Throw a NotFoundException. - throwException(id); - - // this is not used since the method above always throws - return 0; + // Suppress possible NPE. getColorStateList will never return null, it will instead + // throw an exception, but intelliJ can't figure that out + //noinspection ConstantConditions + return getColorStateList(id, theme).getDefaultColor(); } @Override - public ColorStateList getColorStateList(int id) throws NotFoundException { + public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException { Pair<String, ResourceValue> resValue = getResourceValue(id, mPlatformResourceFlag); if (resValue != null) { ColorStateList stateList = ResourceHelper.getColorStateList(resValue.getSecond(), mContext); if (stateList != null) { - return stateList; + return stateList.obtainForTheme(theme); } } @@ -242,6 +246,145 @@ public final class BridgeResources extends Resources { } @Override + public CharSequence[] getTextArray(int id) throws NotFoundException { + ResourceValue resValue = getArrayResourceValue(id); + if (resValue == null) { + // Error already logged by getArrayResourceValue. + return new CharSequence[0]; + } else if (!(resValue instanceof ArrayResourceValue)) { + return new CharSequence[]{ + resolveReference(resValue.getValue(), resValue.isFramework())}; + } + ArrayResourceValue arv = ((ArrayResourceValue) resValue); + return fillValues(arv, new CharSequence[arv.getElementCount()]); + } + + @Override + public String[] getStringArray(int id) throws NotFoundException { + ResourceValue resValue = getArrayResourceValue(id); + if (resValue == null) { + // Error already logged by getArrayResourceValue. + return new String[0]; + } else if (!(resValue instanceof ArrayResourceValue)) { + return new String[]{ + resolveReference(resValue.getValue(), resValue.isFramework())}; + } + ArrayResourceValue arv = ((ArrayResourceValue) resValue); + return fillValues(arv, new String[arv.getElementCount()]); + } + + /** + * Resolve each element in resValue and copy them to {@code values}. The values copied are + * always Strings. The ideal signature for the method should be <T super String>, but java + * generics don't support it. + */ + private <T extends CharSequence> T[] fillValues(ArrayResourceValue resValue, T[] values) { + int i = 0; + for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) { + @SuppressWarnings("unchecked") + T s = (T) resolveReference(iterator.next(), resValue.isFramework()); + values[i] = s; + } + return values; + } + + @Override + public int[] getIntArray(int id) throws NotFoundException { + ResourceValue rv = getArrayResourceValue(id); + if (rv == null) { + // Error already logged by getArrayResourceValue. + return new int[0]; + } else if (!(rv instanceof ArrayResourceValue)) { + // This is an older IDE that can only give us the first element of the array. + String firstValue = resolveReference(rv.getValue(), rv.isFramework()); + try { + return new int[]{getInt(firstValue)}; + } catch (NumberFormatException e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, + "Integer resource array contains non-integer value: " + + firstValue, null); + return new int[1]; + } + } + ArrayResourceValue resValue = ((ArrayResourceValue) rv); + int[] values = new int[resValue.getElementCount()]; + int i = 0; + for (Iterator<String> iterator = resValue.iterator(); iterator.hasNext(); i++) { + String element = resolveReference(iterator.next(), resValue.isFramework()); + try { + values[i] = getInt(element); + } catch (NumberFormatException e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, + "Integer resource array contains non-integer value: " + element, null); + } + } + return values; + } + + /** + * Try to find the ArrayResourceValue for the given id. + * <p/> + * If the ResourceValue found is not of type {@link ResourceType#ARRAY}, the method logs an + * error and return null. However, if the ResourceValue found has type {@code + * ResourceType.ARRAY}, but the value is not an instance of {@link ArrayResourceValue}, the + * method returns the ResourceValue. This happens on older versions of the IDE, which did not + * parse the array resources properly. + * <p/> + * @throws NotFoundException if no resource if found + */ + @Nullable + private ResourceValue getArrayResourceValue(int id) throws NotFoundException { + Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag); + + if (v != null) { + ResourceValue resValue = v.getSecond(); + + assert resValue != null; + if (resValue != null) { + final ResourceType type = resValue.getResourceType(); + if (type != ResourceType.ARRAY) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE, + String.format( + "Resource with id 0x%1$X is not an array resource, but %2$s", + id, type == null ? "null" : type.getDisplayName()), + null); + return null; + } + if (!(resValue instanceof ArrayResourceValue)) { + Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED, + "Obtaining resource arrays via getTextArray, getStringArray or getIntArray is not fully supported in this version of the IDE.", + null); + } + return resValue; + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @NonNull + private String resolveReference(@NonNull String ref, boolean forceFrameworkOnly) { + if (ref.startsWith(SdkConstants.PREFIX_RESOURCE_REF) || ref.startsWith + (SdkConstants.PREFIX_THEME_REF)) { + ResourceValue rv = + mContext.getRenderResources().findResValue(ref, forceFrameworkOnly); + rv = mContext.getRenderResources().resolveResValue(rv); + if (rv != null) { + return rv.getValue(); + } else { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE, + "Unable to resolve resource " + ref, null); + } + } + // Not a reference. + return ref; + } + + @Override public XmlResourceParser getLayout(int id) throws NotFoundException { Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag); @@ -431,13 +574,8 @@ public final class BridgeResources extends Resources { if (resValue != null) { String v = resValue.getValue(); if (v != null) { - int radix = 10; - if (v.startsWith("0x")) { - v = v.substring(2); - radix = 16; - } try { - return Integer.parseInt(v, radix); + return getInt(v); } catch (NumberFormatException e) { // return exception below } @@ -610,7 +748,6 @@ public final class BridgeResources extends Resources { } } - @Override public InputStream openRawResource(int id) throws NotFoundException { Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); @@ -691,7 +828,7 @@ public final class BridgeResources extends Resources { resourceInfo = mLayoutlibCallback.resolveResourceId(id); } - String message = null; + String message; if (resourceInfo != null) { message = String.format( "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.", @@ -703,4 +840,15 @@ public final class BridgeResources extends Resources { throw new NotFoundException(message); } + + private int getInt(String v) throws NumberFormatException { + int radix = 10; + if (v.startsWith("0x")) { + v = v.substring(2); + radix = 16; + } else if (v.startsWith("0")) { + radix = 8; + } + return Integer.parseInt(v, radix); + } } diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index 7d4271b..2e515fb 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -16,7 +16,6 @@ package android.content.res; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.AttrResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderResources; @@ -33,6 +32,7 @@ import com.android.resources.ResourceType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.Nullable; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; @@ -45,7 +45,24 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Map; -import static com.android.ide.common.rendering.api.RenderResources.*; +import static android.util.TypedValue.TYPE_ATTRIBUTE; +import static android.util.TypedValue.TYPE_DIMENSION; +import static android.util.TypedValue.TYPE_FLOAT; +import static android.util.TypedValue.TYPE_INT_BOOLEAN; +import static android.util.TypedValue.TYPE_INT_COLOR_ARGB4; +import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8; +import static android.util.TypedValue.TYPE_INT_COLOR_RGB4; +import static android.util.TypedValue.TYPE_INT_COLOR_RGB8; +import static android.util.TypedValue.TYPE_INT_DEC; +import static android.util.TypedValue.TYPE_INT_HEX; +import static android.util.TypedValue.TYPE_NULL; +import static android.util.TypedValue.TYPE_REFERENCE; +import static android.util.TypedValue.TYPE_STRING; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_EMPTY; +import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_NULL; +import static com.android.ide.common.rendering.api.RenderResources.REFERENCE_UNDEFINED; /** * Custom implementation of TypedArray to handle non compiled resources. @@ -223,7 +240,7 @@ public final class BridgeTypedArray extends TypedArray { String s = getString(index); try { if (s != null) { - return XmlUtils.convertValueToInt(s, defValue); + return convertValueToInt(s, defValue); } } catch (NumberFormatException e) { Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, @@ -320,7 +337,8 @@ public final class BridgeTypedArray extends TypedArray { BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( parser, mContext, resValue.isFramework()); try { - return ColorStateList.createFromXml(mContext.getResources(), blockParser); + return ColorStateList.createFromXml(mContext.getResources(), blockParser, + mContext.getTheme()); } finally { blockParser.ensurePopped(); } @@ -444,7 +462,7 @@ public final class BridgeTypedArray extends TypedArray { @Override public int getDimensionPixelSize(int index, int defValue) { try { - return getDimension(index); + return getDimension(index, null); } catch (RuntimeException e) { String s = getString(index); @@ -474,12 +492,12 @@ public final class BridgeTypedArray extends TypedArray { @Override public int getLayoutDimension(int index, String name) { try { - // this will throw an exception - return getDimension(index); + // this will throw an exception if not found. + return getDimension(index, name); } catch (RuntimeException e) { if (LayoutInflater_Delegate.sIsInInclude) { - throw new RuntimeException(); + throw new RuntimeException("Layout Dimension '" + name + "' not found."); } Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, @@ -494,9 +512,13 @@ public final class BridgeTypedArray extends TypedArray { return getDimensionPixelSize(index, defValue); } - private int getDimension(int index) { + /** @param name attribute name, used for error reporting. */ + private int getDimension(int index, @Nullable String name) { String s = getString(index); if (s == null) { + if (name != null) { + throw new RuntimeException("Attribute '" + name + "' not found"); + } throw new RuntimeException(); } // Check if the value is a magic constant that doesn't require a unit. @@ -758,6 +780,60 @@ public final class BridgeTypedArray extends TypedArray { return s != null && ResourceHelper.parseFloatAttribute(mNames[index], s, outValue, false); } + @Override + @SuppressWarnings("ResultOfMethodCallIgnored") + public int getType(int index) { + String value = getString(index); + if (value == null) { + return TYPE_NULL; + } + if (value.startsWith(PREFIX_RESOURCE_REF)) { + return TYPE_REFERENCE; + } + if (value.startsWith(PREFIX_THEME_REF)) { + return TYPE_ATTRIBUTE; + } + try { + // Don't care about the value. Only called to check if an exception is thrown. + convertValueToInt(value, 0); + if (value.startsWith("0x") || value.startsWith("0X")) { + return TYPE_INT_HEX; + } + // is it a color? + if (value.startsWith("#")) { + int length = value.length() - 1; + if (length == 3) { // rgb + return TYPE_INT_COLOR_RGB4; + } + if (length == 4) { // argb + return TYPE_INT_COLOR_ARGB4; + } + if (length == 6) { // rrggbb + return TYPE_INT_COLOR_RGB8; + } + if (length == 8) { // aarrggbb + return TYPE_INT_COLOR_ARGB8; + } + } + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return TYPE_INT_BOOLEAN; + } + return TYPE_INT_DEC; + } catch (NumberFormatException ignored) { + try { + Float.parseFloat(value); + return TYPE_FLOAT; + } catch (NumberFormatException ignore) { + } + // Might be a dimension. + if (ResourceHelper.parseFloatAttribute(null, value, new TypedValue(), false)) { + return TYPE_DIMENSION; + } + } + // TODO: handle fractions. + return TYPE_STRING; + } + /** * Determines whether there is an attribute at <var>index</var>. * @@ -867,6 +943,52 @@ public final class BridgeTypedArray extends TypedArray { return null; } + /** + * Copied from {@link XmlUtils#convertValueToInt(CharSequence, int)}, but adapted to account + * for aapt, and the fact that host Java VM's Integer.parseInt("XXXXXXXX", 16) cannot handle + * "XXXXXXXX" > 80000000. + */ + private static int convertValueToInt(@Nullable String charSeq, int defValue) { + if (null == charSeq) + return defValue; + + int sign = 1; + int index = 0; + int len = charSeq.length(); + int base = 10; + + if ('-' == charSeq.charAt(0)) { + sign = -1; + index++; + } + + if ('0' == charSeq.charAt(index)) { + // Quick check for a zero by itself + if (index == (len - 1)) + return 0; + + char c = charSeq.charAt(index + 1); + + if ('x' == c || 'X' == c) { + index += 2; + base = 16; + } else { + index++; + // Leave the base as 10. aapt removes the preceding zero, and thus when framework + // sees the value, it only gets the decimal value. + } + } else if ('#' == charSeq.charAt(index)) { + return ResourceHelper.getColor(charSeq) * sign; + } else if ("true".equals(charSeq) || "TRUE".equals(charSeq)) { + return -1; + } else if ("false".equals(charSeq) || "FALSE".equals(charSeq)) { + return 0; + } + + // Use Long, since we want to handle hex ints > 80000000. + return ((int)Long.parseLong(charSeq.substring(index), base)) * sign; + } + static TypedArray obtain(Resources res, int len) { return res instanceof BridgeResources ? new BridgeTypedArray(((BridgeResources) res), null, len, true) : null; diff --git a/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java index 4bd83e9..f1e8fc2 100644 --- a/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java +++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java @@ -16,7 +16,6 @@ package android.content.res; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.common.rendering.api.StyleResourceValue; import com.android.layoutlib.bridge.android.BridgeContext; @@ -25,8 +24,10 @@ import com.android.layoutlib.bridge.impl.RenderSessionImpl; import com.android.resources.ResourceType; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.Nullable; import android.content.res.Resources.NotFoundException; import android.content.res.Resources.Theme; +import android.content.res.Resources.ThemeKey; import android.util.AttributeSet; import android.util.TypedValue; @@ -110,22 +111,16 @@ public class Resources_Theme_Delegate { private static boolean setupResources(Theme thisTheme) { // Key is a space-separated list of theme ids applied that have been merged into the // BridgeContext's theme to make thisTheme. - String[] appliedStyles = thisTheme.getKey().split(" "); + final ThemeKey key = thisTheme.getKey(); + final int[] resId = key.mResId; + final boolean[] force = key.mForce; + boolean changed = false; - for (String s : appliedStyles) { - if (s.isEmpty()) { - continue; - } - // See the definition of force parameter in Theme.applyStyle(). - boolean force = false; - if (s.charAt(s.length() - 1) == '!') { - force = true; - s = s.substring(0, s.length() - 1); - } - int styleId = Integer.parseInt(s, 16); - StyleResourceValue style = resolveStyle(styleId); + for (int i = 0, N = key.mCount; i < N; i++) { + StyleResourceValue style = resolveStyle(resId[i]); if (style != null) { - RenderSessionImpl.getCurrentContext().getRenderResources().applyStyle(style, force); + RenderSessionImpl.getCurrentContext().getRenderResources().applyStyle( + style, force[i]); changed = true; } diff --git a/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java index a4a3b7d..21f36ce 100644 --- a/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java +++ b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java @@ -19,6 +19,12 @@ package android.graphics; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; +import android.graphics.Paint_Delegate.FontInfo; +import android.icu.lang.UScript; +import android.icu.lang.UScriptRun; +import android.icu.text.Bidi; +import android.icu.text.BidiRun; + import java.awt.Font; import java.awt.Graphics2D; import java.awt.Toolkit; @@ -29,13 +35,6 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import com.ibm.icu.lang.UScript; -import com.ibm.icu.lang.UScriptRun; -import com.ibm.icu.text.Bidi; -import com.ibm.icu.text.BidiRun; - -import android.graphics.Paint_Delegate.FontInfo; - /** * Render the text by breaking it into various scripts and using the right font for each script. * Can be used to measure the text without actually drawing it. diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java index 9cf777d..d858953 100644 --- a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java @@ -16,13 +16,13 @@ package android.graphics; -import com.android.annotations.Nullable; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.ninepatch.NinePatchChunk; import com.android.resources.Density; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.Nullable; import android.content.res.BridgeResources.NinePatchInputStream; import android.graphics.BitmapFactory.Options; import android.graphics.Bitmap_Delegate.BitmapCreateFlags; diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java index e9b5d6e..af47aeb 100644 --- a/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java @@ -23,7 +23,15 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.graphics.Shader.TileMode; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; import java.awt.image.ColorModel; +import java.awt.image.Raster; /** * Delegate implementing the native methods of android.graphics.BitmapShader @@ -67,9 +75,9 @@ public class BitmapShader_Delegate extends Shader_Delegate { // ---- native methods ---- @LayoutlibDelegate - /*package*/ static long nativeCreate(long native_bitmap, int shaderTileModeX, + /*package*/ static long nativeCreate(Bitmap androidBitmap, int shaderTileModeX, int shaderTileModeY) { - Bitmap_Delegate bitmap = Bitmap_Delegate.getDelegate(native_bitmap); + Bitmap_Delegate bitmap = Bitmap_Delegate.getDelegate(androidBitmap); if (bitmap == null) { return 0; } @@ -83,17 +91,17 @@ public class BitmapShader_Delegate extends Shader_Delegate { // ---- Private delegate/helper methods ---- - private BitmapShader_Delegate(java.awt.image.BufferedImage image, + private BitmapShader_Delegate(BufferedImage image, TileMode tileModeX, TileMode tileModeY) { mJavaPaint = new BitmapShaderPaint(image, tileModeX, tileModeY); } private class BitmapShaderPaint implements java.awt.Paint { - private final java.awt.image.BufferedImage mImage; + private final BufferedImage mImage; private final TileMode mTileModeX; private final TileMode mTileModeY; - BitmapShaderPaint(java.awt.image.BufferedImage image, + BitmapShaderPaint(BufferedImage image, TileMode tileModeX, TileMode tileModeY) { mImage = image; mTileModeX = tileModeX; @@ -101,29 +109,24 @@ public class BitmapShader_Delegate extends Shader_Delegate { } @Override - public java.awt.PaintContext createContext( - java.awt.image.ColorModel colorModel, - java.awt.Rectangle deviceBounds, - java.awt.geom.Rectangle2D userBounds, - java.awt.geom.AffineTransform xform, - java.awt.RenderingHints hints) { - - java.awt.geom.AffineTransform canvasMatrix; + public PaintContext createContext(ColorModel colorModel, Rectangle deviceBounds, + Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) { + AffineTransform canvasMatrix; try { canvasMatrix = xform.createInverse(); - } catch (java.awt.geom.NoninvertibleTransformException e) { + } catch (NoninvertibleTransformException e) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, "Unable to inverse matrix in BitmapShader", e, null /*data*/); - canvasMatrix = new java.awt.geom.AffineTransform(); + canvasMatrix = new AffineTransform(); } - java.awt.geom.AffineTransform localMatrix = getLocalMatrix(); + AffineTransform localMatrix = getLocalMatrix(); try { localMatrix = localMatrix.createInverse(); - } catch (java.awt.geom.NoninvertibleTransformException e) { + } catch (NoninvertibleTransformException e) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, "Unable to inverse matrix in BitmapShader", e, null /*data*/); - localMatrix = new java.awt.geom.AffineTransform(); + localMatrix = new AffineTransform(); } if (!colorModel.isCompatibleRaster(mImage.getRaster())) { @@ -134,16 +137,16 @@ public class BitmapShader_Delegate extends Shader_Delegate { return new BitmapShaderContext(canvasMatrix, localMatrix, colorModel); } - private class BitmapShaderContext implements java.awt.PaintContext { + private class BitmapShaderContext implements PaintContext { - private final java.awt.geom.AffineTransform mCanvasMatrix; - private final java.awt.geom.AffineTransform mLocalMatrix; - private final java.awt.image.ColorModel mColorModel; + private final AffineTransform mCanvasMatrix; + private final AffineTransform mLocalMatrix; + private final ColorModel mColorModel; public BitmapShaderContext( - java.awt.geom.AffineTransform canvasMatrix, - java.awt.geom.AffineTransform localMatrix, - java.awt.image.ColorModel colorModel) { + AffineTransform canvasMatrix, + AffineTransform localMatrix, + ColorModel colorModel) { mCanvasMatrix = canvasMatrix; mLocalMatrix = localMatrix; mColorModel = colorModel; @@ -154,13 +157,13 @@ public class BitmapShader_Delegate extends Shader_Delegate { } @Override - public java.awt.image.ColorModel getColorModel() { + public ColorModel getColorModel() { return mColorModel; } @Override - public java.awt.image.Raster getRaster(int x, int y, int w, int h) { - java.awt.image.BufferedImage image = new java.awt.image.BufferedImage( + public Raster getRaster(int x, int y, int w, int h) { + BufferedImage image = new BufferedImage( mColorModel, mColorModel.createCompatibleWritableRaster(w, h), mColorModel.isAlphaPremultiplied(), null); diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java index 8d24d38..0737682 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java @@ -22,6 +22,7 @@ import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.resources.Density; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.Nullable; import android.graphics.Bitmap.Config; import android.os.Parcel; @@ -76,19 +77,18 @@ public final class Bitmap_Delegate { // ---- Public Helper methods ---- /** - * Returns the native delegate associated to a given {@link Bitmap_Delegate} object. - */ - public static Bitmap_Delegate getDelegate(Bitmap bitmap) { - return sManager.getDelegate(bitmap.mNativeBitmap); - } - - /** * Returns the native delegate associated to a given an int referencing a {@link Bitmap} object. */ public static Bitmap_Delegate getDelegate(long native_bitmap) { return sManager.getDelegate(native_bitmap); } + @Nullable + public static Bitmap_Delegate getDelegate(@Nullable Bitmap bitmap) { + // refSkPixelRef is a hack to get the native pointer: see #nativeRefPixelRef() + return bitmap == null ? null : getDelegate(bitmap.refSkPixelRef()); + } + /** * Creates and returns a {@link Bitmap} initialized with the given file content. * @@ -187,31 +187,7 @@ public final class Bitmap_Delegate { return createBitmap(delegate, createFlags, density.getDpiValue()); } - /** - * Returns the {@link BufferedImage} used by the delegate of the given {@link Bitmap}. - */ - public static BufferedImage getImage(Bitmap bitmap) { - // get the delegate from the native int. - Bitmap_Delegate delegate = sManager.getDelegate(bitmap.mNativeBitmap); - if (delegate == null) { - return null; - } - - return delegate.mImage; - } - - public static int getBufferedImageType(int nativeBitmapConfig) { - switch (Config.nativeToConfig(nativeBitmapConfig)) { - case ALPHA_8: - return BufferedImage.TYPE_INT_ARGB; - case RGB_565: - return BufferedImage.TYPE_INT_ARGB; - case ARGB_4444: - return BufferedImage.TYPE_INT_ARGB; - case ARGB_8888: - return BufferedImage.TYPE_INT_ARGB; - } - + private static int getBufferedImageType() { return BufferedImage.TYPE_INT_ARGB; } @@ -238,10 +214,6 @@ public final class Bitmap_Delegate { return mHasAlpha && mConfig != Config.RGB_565; } - public boolean hasMipMap() { - // TODO: check if more checks are required as in hasAlpha. - return mHasMipMap; - } /** * Update the generationId. * @@ -256,7 +228,7 @@ public final class Bitmap_Delegate { @LayoutlibDelegate /*package*/ static Bitmap nativeCreate(int[] colors, int offset, int stride, int width, int height, int nativeConfig, boolean isMutable) { - int imageType = getBufferedImageType(nativeConfig); + int imageType = getBufferedImageType(); // create the image BufferedImage image = new BufferedImage(width, height, imageType); @@ -284,7 +256,7 @@ public final class Bitmap_Delegate { int width = srcImage.getWidth(); int height = srcImage.getHeight(); - int imageType = getBufferedImageType(nativeConfig); + int imageType = getBufferedImageType(); // create the image BufferedImage image = new BufferedImage(width, height, imageType); @@ -302,6 +274,13 @@ public final class Bitmap_Delegate { } @LayoutlibDelegate + /*package*/ static Bitmap nativeCopyAshmem(long nativeSrcBitmap) { + // Unused method; no implementation provided. + assert false; + return null; + } + + @LayoutlibDelegate /*package*/ static void nativeDestructor(long nativeBitmap) { sManager.removeJavaReferenceFor(nativeBitmap); } @@ -373,22 +352,16 @@ public final class Bitmap_Delegate { /*package*/ static boolean nativeHasAlpha(long nativeBitmap) { // get the delegate from the native int. Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); - if (delegate == null) { - return true; - } + return delegate == null || delegate.mHasAlpha; - return delegate.mHasAlpha; } @LayoutlibDelegate /*package*/ static boolean nativeHasMipMap(long nativeBitmap) { // get the delegate from the native int. Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); - if (delegate == null) { - return true; - } + return delegate == null || delegate.mHasMipMap; - return delegate.mHasMipMap; } @LayoutlibDelegate @@ -509,11 +482,6 @@ public final class Bitmap_Delegate { } @LayoutlibDelegate - /*package*/ static void nativePrepareToDraw(long nativeBitmap) { - // nothing to be done here. - } - - @LayoutlibDelegate /*package*/ static boolean nativeIsPremultiplied(long nativeBitmap) { // get the delegate from the native int. Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); @@ -599,6 +567,14 @@ public final class Bitmap_Delegate { return Arrays.equals(argb1, argb2); } + // Only used by AssetAtlasService, which we don't care about. + @LayoutlibDelegate + /*package*/ static long nativeRefPixelRef(long nativeBitmap) { + // Hack: This is called by Bitmap.refSkPixelRef() and LayoutLib uses that method to get + // the native pointer from a Bitmap. So, we return nativeBitmap here. + return nativeBitmap; + } + // ---- Private delegate/helper methods ---- private Bitmap_Delegate(BufferedImage image, Config config) { diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java index 4d2d100..f8b3739 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -23,6 +23,7 @@ import com.android.layoutlib.bridge.impl.GcSnapshot; import com.android.layoutlib.bridge.impl.PorterDuffUtility; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.Nullable; import android.graphics.Bitmap.Config; import android.text.TextUtils; @@ -114,7 +115,11 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static long initRaster(long nativeBitmapOrZero) { + /*package*/ static long initRaster(@Nullable Bitmap bitmap) { + long nativeBitmapOrZero = 0; + if (bitmap != null) { + nativeBitmapOrZero = bitmap.refSkPixelRef(); + } if (nativeBitmapOrZero > 0) { // get the Bitmap from the int Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nativeBitmapOrZero); @@ -132,8 +137,7 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ - static void native_setBitmap(long canvas, long bitmap, boolean copyState) { + /*package*/ static void native_setBitmap(long canvas, Bitmap bitmap) { Canvas_Delegate canvasDelegate = sManager.getDelegate(canvas); Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap); if (canvasDelegate == null || bitmapDelegate==null) { @@ -220,7 +224,8 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void native_restore(long nativeCanvas) { + /*package*/ static void native_restore(long nativeCanvas, boolean throwOnUnderflow) { + // FIXME: implement throwOnUnderflow. // get the delegate from the native int. Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); if (canvasDelegate == null) { @@ -231,7 +236,9 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void native_restoreToCount(long nativeCanvas, int saveCount) { + /*package*/ static void native_restoreToCount(long nativeCanvas, int saveCount, + boolean throwOnUnderflow) { + // FIXME: implement throwOnUnderflow. // get the delegate from the native int. Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); if (canvasDelegate == null) { @@ -424,8 +431,7 @@ public final class Canvas_Delegate { canvasDelegate.mDrawFilter = DrawFilter_Delegate.getDelegate(nativeFilter); - if (canvasDelegate.mDrawFilter != null && - canvasDelegate.mDrawFilter.isSupported() == false) { + if (canvasDelegate.mDrawFilter != null && !canvasDelegate.mDrawFilter.isSupported()) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_DRAWFILTER, canvasDelegate.mDrawFilter.getSupportMessage(), null, null /*data*/); } @@ -441,7 +447,7 @@ public final class Canvas_Delegate { } Rectangle rect = canvasDelegate.getSnapshot().getClip().getBounds(); - if (rect != null && rect.isEmpty() == false) { + if (rect != null && !rect.isEmpty()) { bounds.left = rect.x; bounds.top = rect.y; bounds.right = rect.x + rect.width; @@ -717,7 +723,7 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, long bitmap, + /*package*/ static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap, float left, float top, long nativePaintOrZero, int canvasDensity, @@ -739,7 +745,7 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, long bitmap, + /*package*/ static void native_drawBitmap(Canvas thisCanvas, long nativeCanvas, Bitmap bitmap, float srcLeft, float srcTop, float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight, float dstBottom, long nativePaintOrZero, int screenDensity, int bitmapDensity) { @@ -780,7 +786,7 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeDrawBitmapMatrix(long nCanvas, long nBitmap, + /*package*/ static void nativeDrawBitmapMatrix(long nCanvas, Bitmap bitmap, long nMatrix, long nPaint) { // get the delegate from the native int. Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); @@ -792,7 +798,7 @@ public final class Canvas_Delegate { Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nPaint); // get the delegate from the native int. - Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nBitmap); + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap); if (bitmapDelegate == null) { return; } @@ -821,7 +827,7 @@ public final class Canvas_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeDrawBitmapMesh(long nCanvas, long nBitmap, + /*package*/ static void nativeDrawBitmapMesh(long nCanvas, Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, long nPaint) { // FIXME @@ -844,7 +850,7 @@ public final class Canvas_Delegate { @LayoutlibDelegate /*package*/ static void native_drawText(long nativeCanvas, char[] text, int index, int count, float startX, float startY, int flags, long paint, long typeface) { - drawText(nativeCanvas, text, index, count, startX, startY, flags == Canvas.DIRECTION_RTL, + drawText(nativeCanvas, text, index, count, startX, startY, (flags & 1) != 0, paint, typeface); } @@ -1038,8 +1044,7 @@ public final class Canvas_Delegate { } /** - * Restores the {@link GcSnapshot} to <var>saveCount</var> - * @param saveCount the saveCount + * Restores the top {@link GcSnapshot} */ private void restore() { mSnapshot = mSnapshot.restore(); @@ -1102,7 +1107,7 @@ public final class Canvas_Delegate { // before drawing it. if (bitmap.getConfig() == Bitmap.Config.ALPHA_8) { fixAlpha8Bitmap(image); - } else if (bitmap.hasAlpha() == false) { + } else if (!bitmap.hasAlpha()) { // hasAlpha is merely a rendering hint. There can in fact be alpha values // in the bitmap but it should be ignored at drawing time. // There is two ways to do this: @@ -1122,7 +1127,7 @@ public final class Canvas_Delegate { } // if we can't force SRC mode, then create a temp bitmap of TYPE_RGB - if (forceSrcMode[0] == false) { + if (!forceSrcMode[0]) { image = Bitmap_Delegate.createCopy(image, BufferedImage.TYPE_INT_RGB, 0xFF); } } diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java index 42de4ec..857e6d0 100644 --- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java @@ -16,14 +16,14 @@ package android.graphics; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.AssetRepository; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.res.AssetManager; import android.content.res.BridgeAssetManager; diff --git a/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java index e16dbda..e8d34d0 100644 --- a/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java @@ -90,7 +90,7 @@ public final class NinePatch_Delegate { if (oos != null) { try { oos.close(); - } catch (IOException e) { + } catch (IOException ignored) { } } } @@ -136,7 +136,7 @@ public final class NinePatch_Delegate { if (ois != null) { try { ois.close(); - } catch (IOException e) { + } catch (IOException ignored) { } } } @@ -150,15 +150,12 @@ public final class NinePatch_Delegate { @LayoutlibDelegate /*package*/ static boolean isNinePatchChunk(byte[] chunk) { NinePatchChunk chunkObject = getChunk(chunk); - if (chunkObject != null) { - return true; - } + return chunkObject != null; - return false; } @LayoutlibDelegate - /*package*/ static long validateNinePatchChunk(long bitmap, byte[] chunk) { + /*package*/ static long validateNinePatchChunk(byte[] chunk) { // the default JNI implementation only checks that the byte[] has the same // size as the C struct it represent. Since we cannot do the same check (serialization // will return different size depending on content), we do nothing. @@ -173,7 +170,7 @@ public final class NinePatch_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeDraw(long canvas_instance, RectF loc, long bitmap_instance, + /*package*/ static void nativeDraw(long canvas_instance, RectF loc, Bitmap bitmap_instance, long chunk, long paint_instance_or_null, int destDensity, int srcDensity) { draw(canvas_instance, (int) loc.left, (int) loc.top, (int) loc.right, (int) loc.bottom, @@ -182,7 +179,7 @@ public final class NinePatch_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeDraw(long canvas_instance, Rect loc, long bitmap_instance, + /*package*/ static void nativeDraw(long canvas_instance, Rect loc, Bitmap bitmap_instance, long chunk, long paint_instance_or_null, int destDensity, int srcDensity) { draw(canvas_instance, loc.left, loc.top, loc.right, loc.bottom, @@ -191,7 +188,7 @@ public final class NinePatch_Delegate { } @LayoutlibDelegate - /*package*/ static long nativeGetTransparentRegion(long bitmap, long chunk, Rect location) { + /*package*/ static long nativeGetTransparentRegion(Bitmap bitmap, long chunk, Rect location) { return 0; } @@ -199,7 +196,7 @@ public final class NinePatch_Delegate { private static void draw(long canvas_instance, final int left, final int top, final int right, final int bottom, - long bitmap_instance, long chunk, long paint_instance_or_null, + Bitmap bitmap_instance, long chunk, long paint_instance_or_null, final int destDensity, final int srcDensity) { // get the delegate from the native int. final Bitmap_Delegate bitmap_delegate = Bitmap_Delegate.getDelegate(bitmap_instance); diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java index 7b07404..57dd429 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -21,6 +21,8 @@ import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.FontFamily_Delegate.FontVariant; import android.graphics.Paint.FontMetrics; import android.graphics.Paint.FontMetricsInt; @@ -83,6 +85,8 @@ public class Paint_Delegate { private float mTextScaleX; private float mTextSkewX; private int mHintingMode = Paint.HINTING_ON; + private int mHyphenEdit; + private float mLetterSpacing; // not used in actual text rendering. // Variant of the font. A paint's variant can only be compact or elegant. private FontVariant mFontVariant = FontVariant.COMPACT; @@ -100,6 +104,7 @@ public class Paint_Delegate { // ---- Public Helper methods ---- + @Nullable public static Paint_Delegate getDelegate(long native_paint) { return sManager.getDelegate(native_paint); } @@ -251,7 +256,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static int getFlags(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -264,7 +269,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setFlags(Paint thisPaint, int flags) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -280,7 +285,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static int getHinting(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return Paint.HINTING_ON; } @@ -291,7 +296,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setHinting(Paint thisPaint, int mode) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -337,7 +342,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static int getColor(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -348,7 +353,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setColor(Paint thisPaint, int color) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -359,7 +364,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static int getAlpha(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -370,7 +375,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setAlpha(Paint thisPaint, int a) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -381,7 +386,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getStrokeWidth(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 1.f; } @@ -392,7 +397,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setStrokeWidth(Paint thisPaint, float width) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -403,7 +408,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getStrokeMiter(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 1.f; } @@ -414,7 +419,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setStrokeMiter(Paint thisPaint, float miter) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -441,14 +446,14 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static boolean isElegantTextHeight(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); return delegate != null && delegate.mFontVariant == FontVariant.ELEGANT; } @LayoutlibDelegate /*package*/ static void setElegantTextHeight(Paint thisPaint, boolean elegant) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -459,7 +464,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getTextSize(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 1.f; } @@ -470,7 +475,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setTextSize(Paint thisPaint, float textSize) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -482,7 +487,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getTextScaleX(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 1.f; } @@ -493,7 +498,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setTextScaleX(Paint thisPaint, float scaleX) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -505,7 +510,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getTextSkewX(Paint thisPaint) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 1.f; } @@ -516,7 +521,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static void setTextSkewX(Paint thisPaint, float skewX) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } @@ -528,7 +533,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float ascent(Paint thisPaint) { // get the delegate - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -545,7 +550,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float descent(Paint thisPaint) { // get the delegate - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -562,7 +567,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float getFontMetrics(Paint thisPaint, FontMetrics metrics) { // get the delegate - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -573,7 +578,7 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static int getFontMetricsInt(Paint thisPaint, FontMetricsInt fmi) { // get the delegate - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -599,7 +604,7 @@ public class Paint_Delegate { /*package*/ static float native_measureText(Paint thisPaint, char[] text, int index, int count, int bidiFlags) { // get the delegate - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return 0; } @@ -1088,18 +1093,107 @@ public class Paint_Delegate { @LayoutlibDelegate /*package*/ static float native_getLetterSpacing(long nativePaint) { - // TODO: throw a fidelity warning. - return 0; + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return 0; + } + return delegate.mLetterSpacing; } @LayoutlibDelegate /*package*/ static void native_setLetterSpacing(long nativePaint, float letterSpacing) { - // pass. + Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING, + "Paint.setLetterSpacing() not supported.", null, null); + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return; + } + delegate.mLetterSpacing = letterSpacing; } @LayoutlibDelegate /*package*/ static void native_setFontFeatureSettings(long nativePaint, String settings) { - // pass. + Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING, + "Paint.setFontFeatureSettings() not supported.", null, null); + } + + @LayoutlibDelegate + /*package*/ static int native_getHyphenEdit(long nativePaint) { + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return 0; + } + return delegate.mHyphenEdit; + } + + @LayoutlibDelegate + /*package*/ static void native_setHyphenEdit(long nativePaint, int hyphen) { + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return; + } + delegate.mHyphenEdit = hyphen; + } + + @LayoutlibDelegate + /*package*/ static boolean native_hasGlyph(long nativePaint, long nativeTypeface, int bidiFlags, + String string) { + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return false; + } + if (string.length() == 0) { + return false; + } + if (string.length() > 1) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_TEXT_RENDERING, + "Paint.hasGlyph() is not supported for ligatures.", null, null); + return false; + } + assert nativeTypeface == delegate.mNativeTypeface; + Typeface_Delegate typeface_delegate = Typeface_Delegate.getDelegate(nativeTypeface); + + char c = string.charAt(0); + for (Font font : typeface_delegate.getFonts(delegate.mFontVariant)) { + if (font.canDisplay(c)) { + return true; + } + } + return false; + } + + + @LayoutlibDelegate + /*package*/ static float native_getRunAdvance(long nativePaint, long nativeTypeface, + @NonNull char[] text, int start, int end, int contextStart, int contextEnd, + boolean isRtl, int offset) { + int count = end - start; + float[] advances = new float[count]; + native_getTextRunAdvances(nativePaint, nativeTypeface, text, start, count, + contextStart, contextEnd - contextStart, isRtl, advances, 0); + float sum = 0; + for (int i = 0; i < offset; i++) { + sum += advances[i]; + } + return sum; + } + + @LayoutlibDelegate + /*package*/ static int native_getOffsetForAdvance(long nativePaint, long nativeTypeface, + char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, + float advance) { + int count = end - start; + float[] advances = new float[count]; + native_getTextRunAdvances(nativePaint, nativeTypeface, text, start, count, + contextStart, contextEnd - contextStart, isRtl, advances, 0); + float sum = 0; + int i; + for (i = 0; i < count && sum < advance; i++) { + sum += advances[i]; + } + float distanceToI = sum - advance; + float distanceToIMinus1 = advance - (sum - advances[i]); + return distanceToI > distanceToIMinus1 ? i : i - 1; } // ---- Private delegate/helper methods ---- @@ -1137,7 +1231,7 @@ public class Paint_Delegate { } private void reset() { - mFlags = Paint.DEFAULT_PAINT_FLAGS; + mFlags = Paint.HIDDEN_DEFAULT_PAINT_FLAGS; mColor = 0xFF000000; mStyle = Paint.Style.FILL.nativeInt; mCap = Paint.Cap.BUTT.nativeInt; @@ -1232,7 +1326,7 @@ public class Paint_Delegate { private static void setFlag(Paint thisPaint, int flagMask, boolean flagValue) { // get the delegate from the native int. - Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + Paint_Delegate delegate = sManager.getDelegate(thisPaint.getNativeInstance()); if (delegate == null) { return; } diff --git a/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java index 14e9960..0d491a0 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java @@ -81,14 +81,15 @@ public abstract class Shader_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeSetLocalMatrix(long native_shader, long matrix_instance) { + /*package*/ static long nativeSetLocalMatrix(long native_shader, long matrix_instance) { // get the delegate from the native int. Shader_Delegate shaderDelegate = sManager.getDelegate(native_shader); if (shaderDelegate == null) { - return; + return native_shader; } shaderDelegate.mLocalMatrix = Matrix_Delegate.getDelegate(matrix_instance); + return native_shader; } // ---- Private delegate/helper methods ---- diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java index b9460b4..85e65e6 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java @@ -16,10 +16,10 @@ package android.graphics; -import com.android.annotations.NonNull; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.annotation.NonNull; import android.graphics.FontFamily_Delegate.FontVariant; import java.awt.Font; diff --git a/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java index 6247dae..38171dc 100644 --- a/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java @@ -19,8 +19,8 @@ package android.text; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; -import com.ibm.icu.text.Bidi; +import android.icu.text.Bidi; /** * Delegate used to provide new implementation for the native methods of {@link AndroidBidi} diff --git a/tools/layoutlib/bridge/src/android/text/GreedyLineBreaker.java b/tools/layoutlib/bridge/src/android/text/GreedyLineBreaker.java new file mode 100644 index 0000000..50289e9 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/GreedyLineBreaker.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +import android.annotation.NonNull; +import android.text.Primitive.PrimitiveType; +import android.text.StaticLayout.LineBreaks; + +import java.util.ArrayList; +import java.util.List; + +import static android.text.Primitive.PrimitiveType.PENALTY_INFINITY; + +// Based on the native implementation of GreedyLineBreaker in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +public class GreedyLineBreaker extends LineBreaker { + + public GreedyLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth, + @NonNull TabStops tabStops) { + super(primitives, lineWidth, tabStops); + } + + @Override + public void computeBreaks(@NonNull LineBreaks lineBreaks) { + BreakInfo breakInfo = new BreakInfo(); + int lineNum = 0; + float width = 0, printedWidth = 0; + boolean breakFound = false, goodBreakFound = false; + int breakIndex = 0, goodBreakIndex = 0; + float breakWidth = 0, goodBreakWidth = 0; + int firstTabIndex = Integer.MAX_VALUE; + + float maxWidth = mLineWidth.getLineWidth(lineNum); + + int numPrimitives = mPrimitives.size(); + // greedily fit as many characters as possible on each line + // loop over all primitives, and choose the best break point + // (if possible, a break point without splitting a word) + // after going over the maximum length + for (int i = 0; i < numPrimitives; i++) { + Primitive p = mPrimitives.get(i); + + // update the current line width + if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) { + width += p.width; + if (p.type == PrimitiveType.BOX) { + printedWidth = width; + } + } else if (p.type == PrimitiveType.VARIABLE) { + width = mTabStops.width(width); + // keep track of first tab character in the region we are examining + // so we can determine whether or not a line contains a tab + firstTabIndex = Math.min(firstTabIndex, i); + } + + // find the best break point for the characters examined so far + if (printedWidth > maxWidth) { + //noinspection StatementWithEmptyBody + if (breakFound || goodBreakFound) { + if (goodBreakFound) { + // a true line break opportunity existed in the characters examined so far, + // so there is no need to split a word + i = goodBreakIndex; // no +1 because of i++ + lineNum++; + maxWidth = mLineWidth.getLineWidth(lineNum); + breakInfo.mBreaksList.add(mPrimitives.get(goodBreakIndex).location); + breakInfo.mWidthsList.add(goodBreakWidth); + breakInfo.mFlagsList.add(firstTabIndex < goodBreakIndex); + firstTabIndex = Integer.MAX_VALUE; + } else { + // must split a word because there is no other option + i = breakIndex; // no +1 because of i++ + lineNum++; + maxWidth = mLineWidth.getLineWidth(lineNum); + breakInfo.mBreaksList.add(mPrimitives.get(breakIndex).location); + breakInfo.mWidthsList.add(breakWidth); + breakInfo.mFlagsList.add(firstTabIndex < breakIndex); + firstTabIndex = Integer.MAX_VALUE; + } + printedWidth = width = 0; + goodBreakFound = breakFound = false; + goodBreakWidth = breakWidth = 0; + continue; + } else { + // no choice, keep going... must make progress by putting at least one + // character on a line, even if part of that character is cut off -- + // there is no other option + } + } + + // update possible break points + if (p.type == PrimitiveType.PENALTY && + p.penalty < PENALTY_INFINITY) { + // this does not handle penalties with width + + // handle forced line break + if (p.penalty == -PENALTY_INFINITY) { + lineNum++; + maxWidth = mLineWidth.getLineWidth(lineNum); + breakInfo.mBreaksList.add(p.location); + breakInfo.mWidthsList.add(printedWidth); + breakInfo.mFlagsList.add(firstTabIndex < i); + firstTabIndex = Integer.MAX_VALUE; + printedWidth = width = 0; + goodBreakFound = breakFound = false; + goodBreakWidth = breakWidth = 0; + continue; + } + if (i > breakIndex && (printedWidth <= maxWidth || !breakFound)) { + breakFound = true; + breakIndex = i; + breakWidth = printedWidth; + } + if (i > goodBreakIndex && printedWidth <= maxWidth) { + goodBreakFound = true; + goodBreakIndex = i; + goodBreakWidth = printedWidth; + } + } else if (p.type == PrimitiveType.WORD_BREAK) { + // only do this if necessary -- we don't want to break words + // when possible, but sometimes it is unavoidable + if (i > breakIndex && (printedWidth <= maxWidth || !breakFound)) { + breakFound = true; + breakIndex = i; + breakWidth = printedWidth; + } + } + } + + if (breakFound || goodBreakFound) { + // output last break if there are more characters to output + if (goodBreakFound) { + breakInfo.mBreaksList.add(mPrimitives.get(goodBreakIndex).location); + breakInfo.mWidthsList.add(goodBreakWidth); + breakInfo.mFlagsList.add(firstTabIndex < goodBreakIndex); + } else { + breakInfo.mBreaksList.add(mPrimitives.get(breakIndex).location); + breakInfo.mWidthsList.add(breakWidth); + breakInfo.mFlagsList.add(firstTabIndex < breakIndex); + } + } + breakInfo.copyTo(lineBreaks); + } + + private static class BreakInfo { + List<Integer> mBreaksList = new ArrayList<Integer>(); + List<Float> mWidthsList = new ArrayList<Float>(); + List<Boolean> mFlagsList = new ArrayList<Boolean>(); + + public void copyTo(LineBreaks lineBreaks) { + if (lineBreaks.breaks.length != mBreaksList.size()) { + lineBreaks.breaks = new int[mBreaksList.size()]; + lineBreaks.widths = new float[mWidthsList.size()]; + lineBreaks.flags = new int[mFlagsList.size()]; + } + + int i = 0; + for (int b : mBreaksList) { + lineBreaks.breaks[i] = b; + i++; + } + i = 0; + for (float b : mWidthsList) { + lineBreaks.widths[i] = b; + i++; + } + i = 0; + for (boolean b : mFlagsList) { + lineBreaks.flags[i] = b ? TAB_MASK : 0; + i++; + } + + mBreaksList = null; + mWidthsList = null; + mFlagsList = null; + } + } +} diff --git a/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java b/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java new file mode 100644 index 0000000..5a59597 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/Hyphenator_Delegate.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 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. + */ + +package android.text; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.io.File; + +/** + * Delegate that overrides implementation for certain methods in {@link android.text.StaticLayout} + * <p/> + * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced + * by calls to methods of the same name in this delegate class. + */ +public class Hyphenator_Delegate { + + private static final DelegateManager<Hyphenator_Delegate> sDelegateManager = new + DelegateManager<Hyphenator_Delegate>(Hyphenator_Delegate.class); + + @LayoutlibDelegate + /*package*/ static File getSystemHyphenatorLocation() { + // FIXME + return null; + } + + /*package*/ static long loadHyphenator(String patternData) { + return sDelegateManager.addNewDelegate(new Hyphenator_Delegate()); + } +} diff --git a/tools/layoutlib/bridge/src/android/text/LineBreaker.java b/tools/layoutlib/bridge/src/android/text/LineBreaker.java new file mode 100644 index 0000000..06e9c84 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/LineBreaker.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +import android.annotation.NonNull; +import android.text.StaticLayout.LineBreaks; + +import java.util.Collections; +import java.util.List; + +// Based on the native implementation of LineBreaker in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +public abstract class LineBreaker { + + protected static final int TAB_MASK = 0x20000000; // keep in sync with StaticLayout + + protected final @NonNull List<Primitive> mPrimitives; + protected final @NonNull LineWidth mLineWidth; + protected final @NonNull TabStops mTabStops; + + public LineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth, + @NonNull TabStops tabStops) { + mPrimitives = Collections.unmodifiableList(primitives); + mLineWidth = lineWidth; + mTabStops = tabStops; + } + + public abstract void computeBreaks(@NonNull LineBreaks breakInfo); +} diff --git a/tools/layoutlib/bridge/src/android/text/LineWidth.java b/tools/layoutlib/bridge/src/android/text/LineWidth.java new file mode 100644 index 0000000..2ea886d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/LineWidth.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +// Based on the native implementation of LineWidth in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +public class LineWidth { + private final float mFirstWidth; + private final int mFirstWidthLineCount; + private float mRestWidth; + + public LineWidth(float firstWidth, int firstWidthLineCount, float restWidth) { + mFirstWidth = firstWidth; + mFirstWidthLineCount = firstWidthLineCount; + mRestWidth = restWidth; + } + + public float getLineWidth(int line) { + return (line < mFirstWidthLineCount) ? mFirstWidth : mRestWidth; + } +} diff --git a/tools/layoutlib/bridge/src/android/text/OptimizingLineBreaker.java b/tools/layoutlib/bridge/src/android/text/OptimizingLineBreaker.java new file mode 100644 index 0000000..ed8e33a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/OptimizingLineBreaker.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +import android.annotation.NonNull; +import android.text.Primitive.PrimitiveType; +import android.text.StaticLayout.LineBreaks; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import static android.text.Primitive.PrimitiveType.PENALTY_INFINITY; + + +// Based on the native implementation of OptimizingLineBreaker in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +/** + * A more complex version of line breaking where we try to prevent the right edge from being too + * jagged. + */ +public class OptimizingLineBreaker extends LineBreaker { + + public OptimizingLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth, + @NonNull TabStops tabStops) { + super(primitives, lineWidth, tabStops); + } + + @Override + public void computeBreaks(@NonNull LineBreaks breakInfo) { + int numBreaks = mPrimitives.size(); + assert numBreaks > 0; + if (numBreaks == 1) { + // This can be true only if it's an empty paragraph. + Primitive p = mPrimitives.get(0); + assert p.type == PrimitiveType.PENALTY; + breakInfo.breaks = new int[]{0}; + breakInfo.widths = new float[]{p.width}; + breakInfo.flags = new int[]{0}; + return; + } + Node[] opt = new Node[numBreaks]; + opt[0] = new Node(-1, 0, 0, 0, false); + opt[numBreaks - 1] = new Node(-1, 0, 0, 0, false); + + ArrayList<Integer> active = new ArrayList<Integer>(); + active.add(0); + int lastBreak = 0; + for (int i = 0; i < numBreaks; i++) { + Primitive p = mPrimitives.get(i); + if (p.type == PrimitiveType.PENALTY) { + boolean finalBreak = (i + 1 == numBreaks); + Node bestBreak = null; + + for (ListIterator<Integer> it = active.listIterator(); it.hasNext(); + /* incrementing done in loop */) { + int pos = it.next(); + int lines = opt[pos].mPrevCount; + float maxWidth = mLineWidth.getLineWidth(lines); + // we have to compute metrics every time -- + // we can't really pre-compute this stuff and just deal with breaks + // because of the way tab characters work, this makes it computationally + // harder, but this way, we can still optimize while treating tab characters + // correctly + LineMetrics lineMetrics = computeMetrics(pos, i); + if (lineMetrics.mPrintedWidth <= maxWidth) { + float demerits = computeDemerits(maxWidth, lineMetrics.mPrintedWidth, + finalBreak, p.penalty) + opt[pos].mDemerits; + if (bestBreak == null || demerits < bestBreak.mDemerits) { + if (bestBreak == null) { + bestBreak = new Node(pos, opt[pos].mPrevCount + 1, demerits, + lineMetrics.mPrintedWidth, lineMetrics.mHasTabs); + } else { + bestBreak.mPrev = pos; + bestBreak.mPrevCount = opt[pos].mPrevCount + 1; + bestBreak.mDemerits = demerits; + bestBreak.mWidth = lineMetrics.mPrintedWidth; + bestBreak.mHasTabs = lineMetrics.mHasTabs; + } + } + } else { + it.remove(); + } + } + if (p.penalty == -PENALTY_INFINITY) { + active.clear(); + } + if (bestBreak != null) { + opt[i] = bestBreak; + active.add(i); + lastBreak = i; + } + if (active.isEmpty()) { + // we can't give up! + LineMetrics lineMetrics = new LineMetrics(); + int lines = opt[lastBreak].mPrevCount; + float maxWidth = mLineWidth.getLineWidth(lines); + int breakIndex = desperateBreak(lastBreak, numBreaks, maxWidth, lineMetrics); + opt[breakIndex] = new Node(lastBreak, lines + 1, 0 /*doesn't matter*/, + lineMetrics.mWidth, lineMetrics.mHasTabs); + active.add(breakIndex); + lastBreak = breakIndex; + i = breakIndex; // incremented by i++ + } + } + } + + int idx = numBreaks - 1; + int count = opt[idx].mPrevCount; + resize(breakInfo, count); + while (opt[idx].mPrev != -1) { + count--; + assert count >=0; + + breakInfo.breaks[count] = mPrimitives.get(idx).location; + breakInfo.widths[count] = opt[idx].mWidth; + breakInfo.flags [count] = opt[idx].mHasTabs ? TAB_MASK : 0; + idx = opt[idx].mPrev; + } + } + + private static void resize(LineBreaks lineBreaks, int size) { + if (lineBreaks.breaks.length == size) { + return; + } + int[] breaks = new int[size]; + float[] widths = new float[size]; + int[] flags = new int[size]; + + int toCopy = Math.min(size, lineBreaks.breaks.length); + System.arraycopy(lineBreaks.breaks, 0, breaks, 0, toCopy); + System.arraycopy(lineBreaks.widths, 0, widths, 0, toCopy); + System.arraycopy(lineBreaks.flags, 0, flags, 0, toCopy); + + lineBreaks.breaks = breaks; + lineBreaks.widths = widths; + lineBreaks.flags = flags; + } + + @NonNull + private LineMetrics computeMetrics(int start, int end) { + boolean f = false; + float w = 0, pw = 0; + for (int i = start; i < end; i++) { + Primitive p = mPrimitives.get(i); + if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) { + w += p.width; + if (p.type == PrimitiveType.BOX) { + pw = w; + } + } else if (p.type == PrimitiveType.VARIABLE) { + w = mTabStops.width(w); + f = true; + } + } + return new LineMetrics(w, pw, f); + } + + private static float computeDemerits(float maxWidth, float width, boolean finalBreak, + float penalty) { + float deviation = finalBreak ? 0 : maxWidth - width; + return (deviation * deviation) + penalty; + } + + /** + * @return the last break position or -1 if failed. + */ + @SuppressWarnings("ConstantConditions") // method too complex to be analyzed. + private int desperateBreak(int start, int limit, float maxWidth, + @NonNull LineMetrics lineMetrics) { + float w = 0, pw = 0; + boolean breakFound = false; + int breakIndex = 0, firstTabIndex = Integer.MAX_VALUE; + for (int i = start; i < limit; i++) { + Primitive p = mPrimitives.get(i); + + if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) { + w += p.width; + if (p.type == PrimitiveType.BOX) { + pw = w; + } + } else if (p.type == PrimitiveType.VARIABLE) { + w = mTabStops.width(w); + firstTabIndex = Math.min(firstTabIndex, i); + } + + if (pw > maxWidth && breakFound) { + break; + } + + // must make progress + if (i > start && + (p.type == PrimitiveType.PENALTY || p.type == PrimitiveType.WORD_BREAK)) { + breakFound = true; + breakIndex = i; + } + } + + if (breakFound) { + lineMetrics.mWidth = w; + lineMetrics.mPrintedWidth = pw; + lineMetrics.mHasTabs = (start <= firstTabIndex && firstTabIndex < breakIndex); + return breakIndex; + } else { + return -1; + } + } + + private static class LineMetrics { + /** Actual width of the line. */ + float mWidth; + /** Width of the line minus trailing whitespace. */ + float mPrintedWidth; + boolean mHasTabs; + + public LineMetrics() { + } + + public LineMetrics(float width, float printedWidth, boolean hasTabs) { + mWidth = width; + mPrintedWidth = printedWidth; + mHasTabs = hasTabs; + } + } + + /** + * A struct to store the info about a break. + */ + @SuppressWarnings("SpellCheckingInspection") // For the word struct. + private static class Node { + // -1 for the first node. + int mPrev; + // number of breaks so far. + int mPrevCount; + float mDemerits; + float mWidth; + boolean mHasTabs; + + public Node(int prev, int prevCount, float demerits, float width, boolean hasTabs) { + mPrev = prev; + mPrevCount = prevCount; + mDemerits = demerits; + mWidth = width; + mHasTabs = hasTabs; + } + } +} diff --git a/tools/layoutlib/bridge/src/android/text/Primitive.java b/tools/layoutlib/bridge/src/android/text/Primitive.java new file mode 100644 index 0000000..37ed072 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/Primitive.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +import android.annotation.NonNull; + +// Based on the native implementation of Primitive in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +public class Primitive { + public final @NonNull PrimitiveType type; + public final int location; + // The following fields don't make sense for all types. + // Box and Glue have width only. + // Penalty has both width and penalty. + // Word_break has penalty only. + public final float width; + public final float penalty; + + /** + * Use {@code PrimitiveType#getNewPrimitive()} + */ + private Primitive(@NonNull PrimitiveType type, int location, float width, float penalty) { + this.type = type; + this.location = location; + this.width = width; + this.penalty = penalty; + } + + public static enum PrimitiveType { + /** + * Something with a constant width that is to be typeset - like a character. + */ + BOX, + /** + * Blank space with fixed width. + */ + GLUE, + /** + * Aesthetic cost indicating how desirable breaking at this point will be. A penalty of + * {@link #PENALTY_INFINITY} means a forced non-break, whereas a penalty of negative + * {@code #PENALTY_INFINITY} means a forced break. + * <p/> + * Currently, it only stores penalty with values 0 or -infinity. + */ + PENALTY, + /** + * For tabs - variable width space. + */ + VARIABLE, + /** + * Possible breakpoints within a word. Think of this as a high cost {@link #PENALTY}. + */ + WORD_BREAK; + + public Primitive getNewPrimitive(int location) { + assert this == VARIABLE; + return new Primitive(this, location, 0f, 0f); + } + + public Primitive getNewPrimitive(int location, float value) { + assert this == BOX || this == GLUE || this == WORD_BREAK; + if (this == BOX || this == GLUE) { + return new Primitive(this, location, value, 0f); + } else { + return new Primitive(this, location, 0f, value); + } + } + + public Primitive getNewPrimitive(int location, float width, float penalty) { + assert this == PENALTY; + return new Primitive(this, location, width, penalty); + } + + // forced non-break, negative infinity is forced break. + public static final float PENALTY_INFINITY = 1e7f; + } +} + diff --git a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java index b0d79a8..1b0ba51 100644 --- a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java @@ -1,14 +1,22 @@ package android.text; +import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; -import java.text.CharacterIterator; +import android.annotation.NonNull; +import android.graphics.BidiRenderer; +import android.graphics.Paint; +import android.graphics.Paint_Delegate; +import android.graphics.RectF; +import android.icu.text.BreakIterator; +import android.icu.util.ULocale; +import android.text.Primitive.PrimitiveType; +import android.text.StaticLayout.LineBreaks; + +import java.util.ArrayList; import java.util.Arrays; -import java.util.Locale; +import java.util.List; -import com.ibm.icu.lang.UCharacter; -import com.ibm.icu.text.BreakIterator; -import com.ibm.icu.util.ULocale; import javax.swing.text.Segment; /** @@ -20,36 +28,206 @@ import javax.swing.text.Segment; */ public class StaticLayout_Delegate { + private static final char CHAR_SPACE = 0x20; + private static final char CHAR_TAB = 0x09; + private static final char CHAR_NEWLINE = 0x0A; + private static final char CHAR_ZWSP = 0x200B; // Zero width space. + + // ---- Builder delegate manager ---- + private static final DelegateManager<Builder> sBuilderManager = + new DelegateManager<Builder>(Builder.class); + + @LayoutlibDelegate + /*package*/ static long nNewBuilder() { + return sBuilderManager.addNewDelegate(new Builder()); + } + + @LayoutlibDelegate + /*package*/ static void nFreeBuilder(long nativeBuilder) { + sBuilderManager.removeJavaReferenceFor(nativeBuilder); + } + + @LayoutlibDelegate + /*package*/ static void nFinishBuilder(long nativeBuilder) { + } + + @LayoutlibDelegate + /*package*/ static long nLoadHyphenator(String patternData) { + return Hyphenator_Delegate.loadHyphenator(patternData); + } + + @LayoutlibDelegate + /*package*/ static void nSetLocale(long nativeBuilder, String locale, long nativeHyphenator) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder != null) { + builder.mLocale = locale; + builder.mNativeHyphenator = nativeHyphenator; + } + } + + @LayoutlibDelegate + /*package*/ static void nSetIndents(long nativeBuilder, int[] indents) { + // TODO. + } + + @LayoutlibDelegate + /*package*/ static void nSetupParagraph(long nativeBuilder, char[] text, int length, + float firstWidth, int firstWidthLineCount, float restWidth, + int[] variableTabStops, int defaultTabStop, int breakStrategy, + int hyphenationFrequency) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder == null) { + return; + } + + builder.mText = text; + builder.mWidths = new float[length]; + builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth); + builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop); + } + + @LayoutlibDelegate + /*package*/ static float nAddStyleRun(long nativeBuilder, long nativePaint, long nativeTypeface, + int start, int end, boolean isRtl) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + + int bidiFlags = isRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR; + return builder == null ? 0 : + measureText(nativePaint, builder.mText, start, end - start, builder.mWidths, + bidiFlags); + } + + @LayoutlibDelegate + /*package*/ static void nAddMeasuredRun(long nativeBuilder, int start, int end, float[] widths) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder != null) { + System.arraycopy(widths, start, builder.mWidths, start, end - start); + } + } + + @LayoutlibDelegate + /*package*/ static void nAddReplacementRun(long nativeBuilder, int start, int end, float width) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder == null) { + return; + } + builder.mWidths[start] = width; + Arrays.fill(builder.mWidths, start + 1, end, 0.0f); + } + + @LayoutlibDelegate + /*package*/ static void nGetWidths(long nativeBuilder, float[] floatsArray) { + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder != null) { + System.arraycopy(builder.mWidths, 0, floatsArray, 0, builder.mWidths.length); + } + } + + @LayoutlibDelegate + /*package*/ static int nComputeLineBreaks(long nativeBuilder, + LineBreaks recycle, int[] recycleBreaks, float[] recycleWidths, + int[] recycleFlags, int recycleLength) { + + Builder builder = sBuilderManager.getDelegate(nativeBuilder); + if (builder == null) { + return 0; + } + + // compute all possible breakpoints. + int length = builder.mWidths.length; + BreakIterator it = BreakIterator.getLineInstance(new ULocale(builder.mLocale)); + it.setText(new Segment(builder.mText, 0, length)); + + // average word length in english is 5. So, initialize the possible breaks with a guess. + List<Integer> breaks = new ArrayList<Integer>((int) Math.ceil(length / 5d)); + int loc; + it.first(); + while ((loc = it.next()) != BreakIterator.DONE) { + breaks.add(loc); + } + + List<Primitive> primitives = + computePrimitives(builder.mText, builder.mWidths, length, breaks); + switch (builder.mBreakStrategy) { + case Layout.BREAK_STRATEGY_SIMPLE: + builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth, + builder.mTabStopCalculator); + break; + case Layout.BREAK_STRATEGY_HIGH_QUALITY: + // TODO +// break; + case Layout.BREAK_STRATEGY_BALANCED: + builder.mLineBreaker = new OptimizingLineBreaker(primitives, builder.mLineWidth, + builder.mTabStopCalculator); + break; + default: + throw new AssertionError("Unknown break strategy: " + builder.mBreakStrategy); + } + builder.mLineBreaker.computeBreaks(recycle); + return recycle.breaks.length; + } + /** - * Fills the recycle array with positions that are suitable to break the text at. The array - * must be terminated by '-1'. + * Compute metadata each character - things which help in deciding if it's possible to break + * at a point or not. */ - @LayoutlibDelegate - /*package*/ static int[] nLineBreakOpportunities(String locale, char[] text, int length, - int[] recycle) { - BreakIterator iterator = BreakIterator.getLineInstance(new ULocale(locale)); - Segment segment = new Segment(text, 0, length); - iterator.setText(segment); - if (recycle == null) { - // Because 42 is the answer to everything. - recycle = new int[42]; - } - int breakOpp = iterator.first(); - recycle[0] = breakOpp; - //noinspection ConstantConditions - assert BreakIterator.DONE == -1; - for (int i = 1; breakOpp != BreakIterator.DONE; ++i) { - if (i >= recycle.length) { - recycle = doubleSize(recycle); + @NonNull + private static List<Primitive> computePrimitives(@NonNull char[] text, @NonNull float[] widths, + int length, @NonNull List<Integer> breaks) { + // Initialize the list with a guess of the number of primitives: + // 2 Primitives per non-whitespace char and approx 5 chars per word (i.e. 83% chars) + List<Primitive> primitives = new ArrayList<Primitive>(((int) Math.ceil(length * 1.833))); + int breaksSize = breaks.size(); + int breakIndex = 0; + for (int i = 0; i < length; i++) { + char c = text[i]; + if (c == CHAR_SPACE || c == CHAR_ZWSP) { + primitives.add(PrimitiveType.GLUE.getNewPrimitive(i, widths[i])); + } else if (c == CHAR_TAB) { + primitives.add(PrimitiveType.VARIABLE.getNewPrimitive(i)); + } else if (c != CHAR_NEWLINE) { + while (breakIndex < breaksSize && breaks.get(breakIndex) < i) { + breakIndex++; + } + Primitive p; + if (widths[i] != 0) { + if (breakIndex < breaksSize && breaks.get(breakIndex) == i) { + p = PrimitiveType.PENALTY.getNewPrimitive(i, 0, 0); + } else { + p = PrimitiveType.WORD_BREAK.getNewPrimitive(i, 0); + } + primitives.add(p); + } + + primitives.add(PrimitiveType.BOX.getNewPrimitive(i, widths[i])); } - assert (i < recycle.length); - breakOpp = iterator.next(); - recycle[i] = breakOpp; } - return recycle; + // final break at end of everything + primitives.add( + PrimitiveType.PENALTY.getNewPrimitive(length, 0, -PrimitiveType.PENALTY_INFINITY)); + return primitives; } - private static int[] doubleSize(int[] array) { - return Arrays.copyOf(array, array.length * 2); + private static float measureText(long nativePaint, char []text, int index, int count, + float[] widths, int bidiFlags) { + Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint); + RectF bounds = new BidiRenderer(null, paint, text) + .renderText(index, index + count, bidiFlags, widths, 0, false); + return bounds.right - bounds.left; + } + + // TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker. + /** + * Java representation of the native Builder class. + */ + private static class Builder { + String mLocale; + char[] mText; + float[] mWidths; + LineBreaker mLineBreaker; + long mNativeHyphenator; + int mBreakStrategy; + LineWidth mLineWidth; + TabStops mTabStopCalculator; } } diff --git a/tools/layoutlib/bridge/src/android/text/TabStops.java b/tools/layoutlib/bridge/src/android/text/TabStops.java new file mode 100644 index 0000000..6c2f1e1 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/TabStops.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 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. + */ + +package android.text; + +import android.annotation.Nullable; + +// Based on the native implementation of TabStops in +// frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260 +public class TabStops { + @Nullable + private int[] mStops; + private final int mTabWidth; + + public TabStops(@Nullable int[] stops, int defaultTabWidth) { + mTabWidth = defaultTabWidth; + mStops = stops; + } + + public float width(float widthSoFar) { + if (mStops != null) { + for (int i : mStops) { + if (i > widthSoFar) { + return i; + } + } + } + // find the next tabStop after widthSoFar. + return (int) ((widthSoFar + mTabWidth) / mTabWidth) * mTabWidth; + } +} diff --git a/tools/layoutlib/bridge/src/android/util/Xml_Delegate.java b/tools/layoutlib/bridge/src/android/util/Xml_Delegate.java index a193330..213e848 100644 --- a/tools/layoutlib/bridge/src/android/util/Xml_Delegate.java +++ b/tools/layoutlib/bridge/src/android/util/Xml_Delegate.java @@ -17,9 +17,9 @@ package android.util; import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.impl.ParserFactory; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; -import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -37,11 +37,7 @@ public class Xml_Delegate { @LayoutlibDelegate /*package*/ static XmlPullParser newPullParser() { try { - KXmlParser parser = new KXmlParser(); - // The prebuilt kxml2 library with the IDE doesn't support DOCECL. -// parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - return parser; + return ParserFactory.instantiateParser(null); } catch (XmlPullParserException e) { throw new AssertionError(); } diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index 2e649c3..6767a07 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -22,6 +22,7 @@ import com.android.ide.common.rendering.api.MergeCookie; import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.common.rendering.api.ResourceValue; import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.impl.ParserFactory; @@ -53,9 +54,14 @@ public final class BridgeInflater extends LayoutInflater { */ private static final String[] sClassPrefixList = { "android.widget.", - "android.webkit." + "android.webkit.", + "android.app." }; + public static String[] getClassPrefixList() { + return sClassPrefixList; + } + protected BridgeInflater(LayoutInflater original, Context newContext) { super(original, newContext); newContext = getBaseContext(newContext); @@ -127,11 +133,11 @@ public final class BridgeInflater extends LayoutInflater { } @Override - public View createViewFromTag(View parent, String name, AttributeSet attrs, - boolean inheritContext) { + public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, + boolean ignoreThemeAttrs) { View view; try { - view = super.createViewFromTag(parent, name, attrs, inheritContext); + view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttrs); } catch (InflateException e) { // try to load the class from using the custom view loader try { @@ -229,6 +235,13 @@ public final class BridgeInflater extends LayoutInflater { if (viewKey != null) { bc.addViewKey(view, viewKey); } + String scrollPos = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); + if (scrollPos != null) { + if (scrollPos.endsWith("px")) { + int value = Integer.parseInt(scrollPos.substring(0, scrollPos.length() - 2)); + bc.setScrollYPos(view, value); + } + } } } diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java index 5176419..82012c1 100644 --- a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java +++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java @@ -17,6 +17,7 @@ package android.view; import android.graphics.Point; +import com.android.internal.app.IAssistScreenshotReceiver; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; @@ -215,6 +216,12 @@ public class IWindowManagerImpl implements IWindowManager { } @Override + public void overridePendingAppTransitionClipReveal(int startX, int startY, + int startWidth, int startHeight) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override public void overridePendingAppTransitionThumb(Bitmap srcThumb, int startX, int startY, IRemoteCallback startedCallback, boolean scaleUp) throws RemoteException { // TODO Auto-generated method stub @@ -269,8 +276,15 @@ public class IWindowManagerImpl implements IWindowManager { } @Override - public Bitmap screenshotApplications(IBinder arg0, int displayId, int arg1, - int arg2, boolean arg3) throws RemoteException { + public boolean requestAssistScreenshot(IAssistScreenshotReceiver receiver) + throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, + int maxHeight) throws RemoteException { // TODO Auto-generated method stub return null; } @@ -293,7 +307,7 @@ public class IWindowManagerImpl implements IWindowManager { } @Override - public void setAppGroupId(IBinder arg0, int arg1) throws RemoteException { + public void setAppTask(IBinder arg0, int arg1) throws RemoteException { // TODO Auto-generated method stub } @@ -362,6 +376,10 @@ public class IWindowManagerImpl implements IWindowManager { } @Override + public void setForcedDisplayScalingMode(int displayId, int mode) { + } + + @Override public void setInTouchMode(boolean arg0) throws RemoteException { // TODO Auto-generated method stub } diff --git a/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java b/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java index 7a73fae..27b406a 100644 --- a/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java +++ b/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java @@ -21,9 +21,11 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; +import android.util.TypedValue; import android.util.Xml; import java.io.IOException; @@ -36,9 +38,13 @@ import java.io.IOException; * */ public class LayoutInflater_Delegate { - private static final String TAG_MERGE = "merge"; + private static final String ATTR_LAYOUT = "layout"; + + private static final int[] ATTRS_THEME = new int[] { + com.android.internal.R.attr.theme }; + public static boolean sIsInInclude = false; /** @@ -49,7 +55,7 @@ public class LayoutInflater_Delegate { */ @LayoutlibDelegate /* package */ static void rInflate(LayoutInflater thisInflater, XmlPullParser parser, - View parent, final AttributeSet attrs, boolean finishInflate, boolean inheritContext) + View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { if (finishInflate == false) { @@ -61,7 +67,7 @@ public class LayoutInflater_Delegate { // ---- START DEFAULT IMPLEMENTATION. - thisInflater.rInflate_Original(parser, parent, attrs, finishInflate, inheritContext); + thisInflater.rInflate_Original(parser, parent, context, attrs, finishInflate); // ---- END DEFAULT IMPLEMENTATION. @@ -74,15 +80,50 @@ public class LayoutInflater_Delegate { } @LayoutlibDelegate - public static void parseInclude(LayoutInflater thisInflater, XmlPullParser parser, View parent, - AttributeSet attrs, boolean inheritContext) throws XmlPullParserException, IOException { - + public static void parseInclude(LayoutInflater thisInflater, XmlPullParser parser, + Context context, View parent, AttributeSet attrs) + throws XmlPullParserException, IOException { int type; if (parent instanceof ViewGroup) { - final int layout = attrs.getAttributeResourceValue(null, "layout", 0); + // Apply a theme wrapper, if requested. This is sort of a weird + // edge case, since developers think the <include> overwrites + // values in the AttributeSet of the included View. So, if the + // included View has a theme attribute, we'll need to ignore it. + final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); + final int themeResId = ta.getResourceId(0, 0); + final boolean hasThemeOverride = themeResId != 0; + if (hasThemeOverride) { + context = new ContextThemeWrapper(context, themeResId); + } + ta.recycle(); + + // If the layout is pointing to a theme attribute, we have to + // massage the value to get a resource identifier out of it. + int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0); + if (layout == 0) { + final String value = attrs.getAttributeValue(null, ATTR_LAYOUT); + if (value == null || value.length() <= 0) { + throw new InflateException("You must specify a layout in the" + + " include tag: <include layout=\"@layout/layoutID\" />"); + } + + // Attempt to resolve the "?attr/name" string to an identifier. + layout = context.getResources().getIdentifier(value.substring(1), null, null); + } + + // The layout might be referencing a theme attribute. + // ---- START CHANGES + if (layout != 0) { + final TypedValue tempValue = new TypedValue(); + if (context.getTheme().resolveAttribute(layout, tempValue, true)) { + layout = tempValue.resourceId; + } + } + // ---- END CHANGES + if (layout == 0) { - final String value = attrs.getAttributeValue(null, "layout"); + final String value = attrs.getAttributeValue(null, ATTR_LAYOUT); if (value == null) { throw new InflateException("You must specifiy a layout in the" + " include tag: <include layout=\"@layout/layoutID\" />"); @@ -111,13 +152,20 @@ public class LayoutInflater_Delegate { if (TAG_MERGE.equals(childName)) { // Inflate all children. - thisInflater.rInflate(childParser, parent, childAttrs, false, - inheritContext); + thisInflater.rInflate(childParser, parent, context, childAttrs, false); } else { final View view = thisInflater.createViewFromTag(parent, childName, - childAttrs, inheritContext); + context, childAttrs, hasThemeOverride); final ViewGroup group = (ViewGroup) parent; + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Include); + final int id = a.getResourceId( + com.android.internal.R.styleable.Include_id, View.NO_ID); + final int visibility = a.getInt( + com.android.internal.R.styleable.Include_visibility, -1); + a.recycle(); + // We try to load the layout params set in the <include /> tag. If // they don't exist, we will rely on the layout params set in the // included XML file. @@ -133,34 +181,20 @@ public class LayoutInflater_Delegate { // ---- END CHANGES params = group.generateLayoutParams(attrs); - - } catch (RuntimeException e) { - // ---- START CHANGES - sIsInInclude = false; - // ---- END CHANGES - - params = group.generateLayoutParams(childAttrs); + } catch (RuntimeException ignored) { + // Ignore, just fail over to child attrs. } finally { // ---- START CHANGES sIsInInclude = false; // ---- END CHANGES - - if (params != null) { - view.setLayoutParams(params); - } } + if (params == null) { + params = group.generateLayoutParams(childAttrs); + } + view.setLayoutParams(params); // Inflate all children. - thisInflater.rInflate(childParser, view, childAttrs, true, true); - - // Attempt to override the included layout's android:id with the - // one set on the <include /> tag itself. - TypedArray a = thisInflater.mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.View, 0, 0); - int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID); - // While we're at it, let's try to override android:visibility. - int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); - a.recycle(); + thisInflater.rInflateChildren(childParser, view, childAttrs, true); if (id != View.NO_ID) { view.setId(id); @@ -188,12 +222,6 @@ public class LayoutInflater_Delegate { throw new InflateException("<include /> can only be used inside of a ViewGroup"); } - final int currentDepth = parser.getDepth(); - while (((type = parser.next()) != XmlPullParser.END_TAG || - parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { - // Empty - } + LayoutInflater.consumeChildElements(parser); } - - } diff --git a/tools/layoutlib/bridge/src/android/view/ShadowPainter.java b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java index 2f93bc8..f09fffd 100644 --- a/tools/layoutlib/bridge/src/android/view/ShadowPainter.java +++ b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java @@ -16,7 +16,7 @@ package android.view; -import com.android.annotations.NonNull; +import android.annotation.NonNull; import java.awt.Graphics2D; import java.awt.Image; diff --git a/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java b/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java index e72a0db..51d32e3 100644 --- a/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java +++ b/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java @@ -46,16 +46,12 @@ public class ViewGroup_Delegate { /*package*/ static boolean drawChild(ViewGroup thisVG, Canvas canvas, View child, long drawingTime) { if (child.getZ() > thisVG.getZ()) { + // The background's bounds are set lazily. Make sure they are set correctly so that + // the outline obtained is correct. + child.setBackgroundBounds(); ViewOutlineProvider outlineProvider = child.getOutlineProvider(); - Outline outline = new Outline(); + Outline outline = child.mAttachInfo.mTmpOutline; outlineProvider.getOutline(child, outline); - if (outline.mPath == null && outline.mRect == null) { - // Sometimes, the bounds of the background drawable are not set until View.draw() - // is called. So, we set the bounds manually and try to get the outline again. - child.getBackground().setBounds(0, 0, child.mRight - child.mLeft, - child.mBottom - child.mTop); - outlineProvider.getOutline(child, outline); - } if (outline.mPath != null || (outline.mRect != null && !outline.mRect.isEmpty())) { int restoreTo = transformCanvas(thisVG, canvas, child); drawShadow(thisVG, canvas, child, outline); diff --git a/tools/layoutlib/bridge/src/android/view/View_Delegate.java b/tools/layoutlib/bridge/src/android/view/View_Delegate.java index 8215f7c..408ec54 100644 --- a/tools/layoutlib/bridge/src/android/view/View_Delegate.java +++ b/tools/layoutlib/bridge/src/android/view/View_Delegate.java @@ -16,8 +16,12 @@ package android.view; +import com.android.layoutlib.bridge.android.BridgeContext; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.content.Context; +import android.os.IBinder; + /** * Delegate used to provide new implementation of a select few methods of {@link View} * @@ -31,4 +35,13 @@ public class View_Delegate { /*package*/ static boolean isInEditMode(View thisView) { return true; } + + @LayoutlibDelegate + /*package*/ static IBinder getWindowToken(View thisView) { + Context baseContext = BridgeContext.getBaseContext(thisView.getContext()); + if (baseContext instanceof BridgeContext) { + return ((BridgeContext) baseContext).getBinder(); + } + return null; + } } diff --git a/tools/layoutlib/bridge/src/android/view/WindowCallback.java b/tools/layoutlib/bridge/src/android/view/WindowCallback.java index 78242a8..d691c8e 100644 --- a/tools/layoutlib/bridge/src/android/view/WindowCallback.java +++ b/tools/layoutlib/bridge/src/android/view/WindowCallback.java @@ -110,6 +110,11 @@ public class WindowCallback implements Window.Callback { } @Override + public boolean onSearchRequested(SearchEvent searchEvent) { + return onSearchRequested(); + } + + @Override public boolean onSearchRequested() { return false; } @@ -120,6 +125,11 @@ public class WindowCallback implements Window.Callback { } @Override + public ActionMode onWindowStartingActionMode(Callback callback, int type) { + return null; + } + + @Override public void onActionModeStarted(ActionMode mode) { } diff --git a/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java b/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java index d5170aa..edb5eff 100644 --- a/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java +++ b/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java @@ -16,9 +16,8 @@ package android.view.accessibility; -import com.android.annotations.NonNull; - import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.NonNull; import android.content.Context; import android.content.pm.ServiceInfo; import android.view.IWindow; diff --git a/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java b/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java deleted file mode 100644 index 6558b6a..0000000 --- a/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2012 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. - */ - -package com.android.internal.policy; - -import com.android.ide.common.rendering.api.LayoutLog; -import com.android.layoutlib.bridge.Bridge; -import com.android.layoutlib.bridge.impl.RenderAction; - -import android.content.Context; -import android.view.BridgeInflater; -import android.view.FallbackEventHandler; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.WindowManagerPolicy; - -/** - * Custom implementation of PolicyManager that does nothing to run in LayoutLib. - * - */ -public class PolicyManager { - - public static Window makeNewWindow(Context context) { - // this will likely crash somewhere beyond so we log it. - Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, - "Call to PolicyManager.makeNewWindow is not supported", null); - return null; - } - - public static LayoutInflater makeNewLayoutInflater(Context context) { - return new BridgeInflater(context, RenderAction.getCurrentContext().getLayoutlibCallback()); - } - - public static WindowManagerPolicy makeNewWindowManager() { - // this will likely crash somewhere beyond so we log it. - Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, - "Call to PolicyManager.makeNewWindowManager is not supported", null); - return null; - } - - public static FallbackEventHandler makeNewFallbackEventHandler(Context context) { - return new FallbackEventHandler() { - @Override - public void setView(View v) { - } - - @Override - public void preDispatchKeyEvent(KeyEvent event) { - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return false; - } - }; - } -} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java index c6d60f8..3ae10f2 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -16,10 +16,6 @@ package com.android.layoutlib.bridge; -import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; -import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; - -import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.rendering.api.DrawableParams; import com.android.ide.common.rendering.api.Features; @@ -36,13 +32,13 @@ import com.android.resources.ResourceType; import com.android.tools.layoutlib.create.MethodAdapter; import com.android.tools.layoutlib.create.OverrideMethod; import com.android.util.Pair; -import com.ibm.icu.util.ULocale; -import libcore.io.MemoryMappedFile_Delegate; +import android.annotation.NonNull; import android.content.res.BridgeAssetManager; import android.graphics.Bitmap; import android.graphics.FontFamily_Delegate; import android.graphics.Typeface_Delegate; +import android.icu.util.ULocale; import android.os.Looper; import android.os.Looper_Accessor; import android.view.View; @@ -60,6 +56,11 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; +import libcore.io.MemoryMappedFile_Delegate; + +import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; +import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; + /** * Main entry point of the LayoutLib Bridge. * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/AndroidLocale.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/AndroidLocale.java index ea5f1ea..e589d9e 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/AndroidLocale.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/AndroidLocale.java @@ -16,9 +16,9 @@ package com.android.layoutlib.bridge.android; -import java.util.Locale; +import android.icu.util.ULocale; -import com.ibm.icu.util.ULocale; +import java.util.Locale; /** * This class provides an alternate implementation for {@code java.util.Locale#toLanguageTag} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 2cbbeba..6be5a95 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -16,8 +16,7 @@ package com.android.layoutlib.bridge.android; -import android.os.IBinder; -import com.android.annotations.Nullable; +import com.android.SdkConstants; import com.android.ide.common.rendering.api.AssetRepository; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.LayoutLog; @@ -37,6 +36,8 @@ import com.android.util.Pair; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.Nullable; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -63,10 +64,15 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.net.Uri; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; +import android.os.IInterface; import android.os.Looper; +import android.os.Parcel; import android.os.PowerManager; +import android.os.RemoteException; import android.os.UserHandle; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -74,6 +80,7 @@ import android.util.TypedValue; import android.view.BridgeInflater; import android.view.Display; import android.view.DisplayAdjustments; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -81,6 +88,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.textservice.TextServicesManager; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -118,6 +126,7 @@ public final class BridgeContext extends Context { private final LayoutlibCallback mLayoutlibCallback; private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; + private final HashMap<View, Integer> mScrollYPos = new HashMap<View, Integer>(); private Resources.Theme mTheme; @@ -125,6 +134,7 @@ public final class BridgeContext extends Context { new IdentityHashMap<Object, Map<String,String>>(); // maps for dynamically generated id representing style objects (StyleResourceValue) + @Nullable private Map<Integer, StyleResourceValue> mDynamicIdToStyleMap; private Map<StyleResourceValue, Integer> mStyleToDynamicIdMap; private int mDynamicIdGenerator = 0x02030000; // Base id for R.style in custom namespace @@ -137,8 +147,33 @@ public final class BridgeContext extends Context { private final Stack<BridgeXmlBlockParser> mParserStack = new Stack<BridgeXmlBlockParser>(); private SharedPreferences mSharedPreferences; + private ClassLoader mClassLoader; + private IBinder mBinder; + + + /** + * Some applications that target both pre API 17 and post API 17, set the newer attrs to + * reference the older ones. For example, android:paddingStart will resolve to + * android:paddingLeft. This way the apps need to only define paddingLeft at any other place. + * This a map from value to attribute name. Warning for missing references shouldn't be logged + * if value and attr name pair is the same as an entry in this map. + */ + private static Map<String, String> RTL_ATTRS = new HashMap<String, String>(10); + + static { + RTL_ATTRS.put("?android:attr/paddingLeft", "paddingStart"); + RTL_ATTRS.put("?android:attr/paddingRight", "paddingEnd"); + RTL_ATTRS.put("?android:attr/layout_marginLeft", "layout_marginStart"); + RTL_ATTRS.put("?android:attr/layout_marginRight", "layout_marginEnd"); + RTL_ATTRS.put("?android:attr/layout_toLeft", "layout_toStartOf"); + RTL_ATTRS.put("?android:attr/layout_toRight", "layout_toEndOf"); + RTL_ATTRS.put("?android:attr/layout_alignParentLeft", "layout_alignParentStart"); + RTL_ATTRS.put("?android:attr/layout_alignParentRight", "layout_alignParentEnd"); + RTL_ATTRS.put("?android:attr/drawableLeft", "drawableStart"); + RTL_ATTRS.put("?android:attr/drawableRight", "drawableEnd"); + } - /** + /** * @param projectKey An Object identifying the project. This is used for the cache mechanism. * @param metrics the {@link DisplayMetrics}. * @param renderResources the configured resources (both framework and projects) for this @@ -461,7 +496,35 @@ public final class BridgeContext extends Context { @Override public ClassLoader getClassLoader() { - return this.getClass().getClassLoader(); + // The documentation for this method states that it should return a class loader one can + // use to retrieve classes in this package. However, when called by LayoutInflater, we do + // not want the class loader to return app's custom views. + // This is so that the IDE can instantiate the custom views and also generate proper error + // messages in case of failure. This also enables the IDE to fallback to MockView in case + // there's an exception thrown when trying to inflate the custom view. + // To work around this issue, LayoutInflater is modified via LayoutLib Create tool to + // replace invocations of this method to a new method: getFrameworkClassLoader(). Also, + // the method is injected into Context. The implementation of getFrameworkClassLoader() is: + // "return getClass().getClassLoader();". This means that when LayoutInflater asks for + // the context ClassLoader, it gets only LayoutLib's ClassLoader which doesn't have + // access to the apps's custom views. + // This method can now return the right ClassLoader, which CustomViews can use to do the + // right thing. + if (mClassLoader == null) { + mClassLoader = new ClassLoader(getClass().getClassLoader()) { + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + for (String prefix : BridgeInflater.getClassPrefixList()) { + if (name.startsWith(prefix)) { + // These are framework classes and should not be loaded from the app. + throw new ClassNotFoundException(name + " not found"); + } + } + return BridgeContext.this.mLayoutlibCallback.findClass(name); + } + }; + } + return mClassLoader; } @Override @@ -499,6 +562,34 @@ public final class BridgeContext extends Context { throw new UnsupportedOperationException("Unsupported Service: " + service); } + @Override + public String getSystemServiceName(Class<?> serviceClass) { + if (serviceClass.equals(LayoutInflater.class)) { + return LAYOUT_INFLATER_SERVICE; + } + + if (serviceClass.equals(TextServicesManager.class)) { + return TEXT_SERVICES_MANAGER_SERVICE; + } + + if (serviceClass.equals(WindowManager.class)) { + return WINDOW_SERVICE; + } + + if (serviceClass.equals(PowerManager.class)) { + return POWER_SERVICE; + } + + if (serviceClass.equals(DisplayManager.class)) { + return DISPLAY_SERVICE; + } + + if (serviceClass.equals(AccessibilityManager.class)) { + return ACCESSIBILITY_SERVICE; + } + + throw new UnsupportedOperationException("Unsupported Service: " + serviceClass); + } @Override public final BridgeTypedArray obtainStyledAttributes(int[] attrs) { @@ -664,44 +755,48 @@ public final class BridgeContext extends Context { } } } else if (defStyleRes != 0) { - boolean isFrameworkRes = true; - Pair<ResourceType, String> value = Bridge.resolveResourceId(defStyleRes); - if (value == null) { - value = mLayoutlibCallback.resolveResourceId(defStyleRes); - isFrameworkRes = false; - } + StyleResourceValue item = getStyleByDynamicId(defStyleRes); + if (item != null) { + defStyleValues = item; + } else { + boolean isFrameworkRes = true; + Pair<ResourceType, String> value = Bridge.resolveResourceId(defStyleRes); + if (value == null) { + value = mLayoutlibCallback.resolveResourceId(defStyleRes); + isFrameworkRes = false; + } - if (value != null) { - if ((value.getFirst() == ResourceType.STYLE)) { - // look for the style in all resources: - StyleResourceValue item = mRenderResources.getStyle(value.getSecond(), - isFrameworkRes); - if (item != null) { - if (defaultPropMap != null) { - defaultPropMap.put("style", item.getName()); + if (value != null) { + if ((value.getFirst() == ResourceType.STYLE)) { + // look for the style in all resources: + item = mRenderResources.getStyle(value.getSecond(), isFrameworkRes); + if (item != null) { + if (defaultPropMap != null) { + defaultPropMap.put("style", item.getName()); + } + + defStyleValues = item; + } else { + Bridge.getLog().error(null, + String.format( + "Style with id 0x%x (resolved to '%s') does not exist.", + defStyleRes, value.getSecond()), + null); } - - defStyleValues = item; } else { Bridge.getLog().error(null, String.format( - "Style with id 0x%x (resolved to '%s') does not exist.", - defStyleRes, value.getSecond()), + "Resource id 0x%x is not of type STYLE (instead %s)", + defStyleRes, value.getFirst().toString()), null); } } else { Bridge.getLog().error(null, String.format( - "Resource id 0x%x is not of type STYLE (instead %s)", - defStyleRes, value.getFirst().toString()), + "Failed to find style with id 0x%x in current theme", + defStyleRes), null); } - } else { - Bridge.getLog().error(null, - String.format( - "Failed to find style with id 0x%x in current theme", - defStyleRes), - null); } } @@ -762,6 +857,22 @@ public final class BridgeContext extends Context { } resValue = mRenderResources.resolveResValue(resValue); + + // If the value is a reference to another theme attribute that doesn't + // exist, we should log a warning and omit it. + String val = resValue.getValue(); + if (val != null && val.startsWith(SdkConstants.PREFIX_THEME_REF)) { + if (!attrName.equals(RTL_ATTRS.get(val)) || + getApplicationInfo().targetSdkVersion < + VERSION_CODES.JELLY_BEAN_MR1) { + // Only log a warning if the referenced value isn't one of the RTL + // attributes, or the app targets old API. + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR, + String.format("Failed to find '%s' in current theme.", val), + val); + } + resValue = null; + } } ta.bridgeSetValue(index, attrName, frameworkAttr, resValue); @@ -952,6 +1063,61 @@ public final class BridgeContext extends Context { return context; } + public IBinder getBinder() { + if (mBinder == null) { + // create a dummy binder. We only need it be not null. + mBinder = new IBinder() { + @Override + public String getInterfaceDescriptor() throws RemoteException { + return null; + } + + @Override + public boolean pingBinder() { + return false; + } + + @Override + public boolean isBinderAlive() { + return false; + } + + @Override + public IInterface queryLocalInterface(String descriptor) { + return null; + } + + @Override + public void dump(FileDescriptor fd, String[] args) throws RemoteException { + + } + + @Override + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { + + } + + @Override + public boolean transact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + return false; + } + + @Override + public void linkToDeath(DeathRecipient recipient, int flags) + throws RemoteException { + + } + + @Override + public boolean unlinkToDeath(DeathRecipient recipient, int flags) { + return false; + } + }; + } + return mBinder; + } + //------------ NOT OVERRIDEN -------------------- @Override @@ -991,6 +1157,12 @@ public final class BridgeContext extends Context { } @Override + public int checkSelfPermission(String arg0) { + // pass + return 0; + } + + @Override public int checkPermission(String arg0, int arg1, int arg2, IBinder arg3) { // pass return 0; @@ -1370,6 +1542,11 @@ public final class BridgeContext extends Context { // pass } + public void sendBroadcastAsUser(Intent intent, UserHandle user, + String receiverPermission, int appOp) { + // pass + } + @Override public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, @@ -1563,4 +1740,13 @@ public final class BridgeContext extends Context { // pass return new File[0]; } + + public void setScrollYPos(@NonNull View view, int scrollPos) { + mScrollYPos.put(view, scrollPos); + } + + public int getScrollYPos(@NonNull View view) { + Integer pos = mScrollYPos.get(view); + return pos != null ? pos : 0; + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java index c44a57c..8899e53 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -164,7 +164,8 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public void showInputMethodPickerFromClient(IInputMethodClient arg0) throws RemoteException { + public void showInputMethodPickerFromClient(IInputMethodClient arg0, + int arg1) throws RemoteException { // TODO Auto-generated method stub } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java index 9b755cd..085df85 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java @@ -147,6 +147,11 @@ public class BridgePowerManager implements IPowerManager { } @Override + public boolean isDeviceIdleMode() throws RemoteException { + return false; + } + + @Override public boolean isScreenBrightnessBoosted() throws RemoteException { return false; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java index fb5d44f..771c3c8 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java @@ -91,7 +91,11 @@ public final class BridgeWindow implements IWindow { } @Override - public void doneAnimating() { + public void onAnimationStarted(int remainingFrameCount) { + } + + @Override + public void onAnimationStopped() { } @Override diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java index 8575839..c92df6d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java @@ -191,12 +191,6 @@ public final class BridgeWindowSession implements IWindowSession { } @Override - public void setUniverseTransform(IBinder window, float alpha, float offx, float offy, - float dsdx, float dtdx, float dsdy, float dtdy) { - // pass for now. - } - - @Override public IBinder asBinder() { // pass for now. return null; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java new file mode 100644 index 0000000..0426907 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.layoutlib.bridge.android.support; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.View; + +import java.lang.reflect.Method; + +import static com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException; +import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod; +import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke; + +/** + * Utility class for working with the design support lib. + */ +public class DesignLibUtil { + + private static final String PKG_PREFIX = "android.support.design.widget."; + public static final String CN_COORDINATOR_LAYOUT = PKG_PREFIX + "CoordinatorLayout"; + public static final String CN_APPBAR_LAYOUT = PKG_PREFIX + "AppBarLayout"; + public static final String CN_COLLAPSING_TOOLBAR_LAYOUT = + PKG_PREFIX + "CollapsingToolbarLayout"; + public static final String CN_TOOLBAR = "android.support.v7.widget.Toolbar"; + public static final int SCROLL_AXIS_VERTICAL = 1 << 1; + + /** + * Tries to set the title of a view. This is used to set the title in a + * CollapsingToolbarLayout. + * <p/> + * Any exceptions thrown during the process are logged in {@link Bridge#getLog()} + */ + public static void setTitle(@NonNull View view, @Nullable String title) { + if (title == null) { + return; + } + try { + Method setTitle = getMethod(view.getClass(), "setTitle", CharSequence.class); + if (setTitle != null) { + invoke(setTitle, view, title); + } + } catch (ReflectionException e) { + Bridge.getLog().warning(LayoutLog.TAG_INFO, + "Error occurred while trying to set title.", e); + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java index e4c7288..5e5b3c9 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/RecyclerViewUtil.java @@ -16,8 +16,6 @@ package com.android.layoutlib.bridge.android.support; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.LayoutlibCallback; import com.android.ide.common.rendering.api.SessionParams; @@ -25,6 +23,8 @@ import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.RenderParamsFlags; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.view.View; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java index dd1f661..8b8cc38 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java @@ -16,8 +16,6 @@ package com.android.layoutlib.bridge.bars; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; @@ -26,6 +24,8 @@ import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.impl.ResourceHelper; import com.android.resources.ResourceType; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.ContextThemeWrapper; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java index 3d1a9b9..a19b689 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java @@ -16,7 +16,6 @@ package com.android.layoutlib.bridge.bars; -import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.ActionBarCallback; import com.android.ide.common.rendering.api.ActionBarCallback.HomeButtonStyle; import com.android.ide.common.rendering.api.RenderResources; @@ -24,6 +23,7 @@ import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; import com.android.layoutlib.bridge.android.BridgeContext; +import android.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java index bc1a41d..90a1c75 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java @@ -16,7 +16,6 @@ package com.android.layoutlib.bridge.bars; -import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.StyleResourceValue; @@ -32,6 +31,7 @@ import com.android.resources.ResourceType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.NonNull; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java index a1c9065..bb3d13f 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java @@ -16,8 +16,6 @@ package com.android.layoutlib.bridge.bars; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; @@ -27,6 +25,8 @@ import com.android.internal.view.menu.MenuItemImpl; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.impl.ResourceHelper; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; import android.util.DisplayMetrics; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java index 44c2cd8..daad602 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java @@ -16,8 +16,6 @@ package com.android.layoutlib.bridge.bars; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ActionBarCallback; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; @@ -31,6 +29,8 @@ import com.android.internal.widget.DecorToolbar; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.impl.ResourceHelper; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActionBar; import android.app.ActionBar.Tab; import android.app.ActionBar.TabListener; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java index 04aadff..dcf82a3 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java @@ -24,6 +24,7 @@ import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.pm.ApplicationInfo; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; @@ -32,6 +33,17 @@ public class NavigationBar extends CustomBar { /** Navigation bar background color attribute name. */ private static final String ATTR_COLOR = "navigationBarColor"; + // These correspond to @dimen/navigation_side_padding in the system ui code. + private static final int PADDING_WIDTH_DEFAULT = 36; + private static final int PADDING_WIDTH_SW360 = 40; + private static final int PADDING_WIDTH_SW400 = 50; + // These corresponds to @dimen/navigation_key_width in the system ui code. + private static final int WIDTH_DEFAULT = 36; + private static final int WIDTH_SW360 = 40; + private static final int WIDTH_SW600 = 48; + private static final String LAYOUT_XML = "/bars/navigation_bar.xml"; + private static final String LAYOUT_600DP_XML = "/bars/navigation_bar600dp.xml"; + /** * Constructor to be used when creating the {@link NavigationBar} as a regular control. @@ -45,13 +57,13 @@ public class NavigationBar extends CustomBar { ((BridgeContext) context).getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL, (context.getApplicationInfo().flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0, - context.getApplicationInfo().targetSdkVersion); + 0); } public NavigationBar(BridgeContext context, Density density, int orientation, boolean isRtl, boolean rtlEnabled, int simulatedPlatformVersion) throws XmlPullParserException { - super(context, orientation, "/bars/navigation_bar.xml", "navigation_bar.xml", - simulatedPlatformVersion); + super(context, orientation, getShortestWidth(context)>= 600 ? LAYOUT_600DP_XML : LAYOUT_XML, + "navigation_bar.xml", simulatedPlatformVersion); int color = getThemeAttrColor(ATTR_COLOR, true); setBackgroundColor(color == 0 ? 0xFF000000 : color); @@ -61,19 +73,76 @@ public class NavigationBar extends CustomBar { // We do know the order though. // 0 is a spacer. int back = 1; - int recent = 3; + int recent = 5; if (orientation == LinearLayout.VERTICAL || (isRtl && !rtlEnabled)) { // If RTL is enabled, then layoutlib mirrors the layout for us. - back = 3; + back = 5; recent = 1; } //noinspection SpellCheckingInspection - loadIcon(back, "ic_sysbar_back.png", density, isRtl); + loadIcon(back, "ic_sysbar_back.png", density, isRtl); //noinspection SpellCheckingInspection - loadIcon(2, "ic_sysbar_home.png", density, isRtl); + loadIcon(3, "ic_sysbar_home.png", density, isRtl); //noinspection SpellCheckingInspection loadIcon(recent, "ic_sysbar_recent.png", density, isRtl); + setupNavBar(context, orientation); + } + + private void setupNavBar(BridgeContext context, int orientation) { + float sw = getShortestWidth(context); + View leftPadding = getChildAt(0); + View rightPadding = getChildAt(6); + setSize(context, leftPadding, orientation, getSidePadding(sw)); + setSize(context, rightPadding, orientation, getSidePadding(sw)); + int navButtonWidth = getWidth(sw); + for (int i = 1; i < 6; i += 2) { + View navButton = getChildAt(i); + setSize(context, navButton, orientation, navButtonWidth); + } + if (sw >= 600) { + setSize(context, getChildAt(2), orientation, 128); + setSize(context, getChildAt(4), orientation, 128); + } + } + + private static void setSize(BridgeContext context, View view, int orientation, int size) { + size *= context.getMetrics().density; + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + if (orientation == HORIZONTAL) { + layoutParams.width = size; + } else { + layoutParams.height = size; + } + view.setLayoutParams(layoutParams); + } + + private static int getSidePadding(float sw) { + if (sw >= 400) { + return PADDING_WIDTH_SW400; + } + if (sw >= 360) { + return PADDING_WIDTH_SW360; + } + return PADDING_WIDTH_DEFAULT; + } + + private static int getWidth(float sw) { + if (sw >= 600) { + return WIDTH_SW600; + } + if (sw >= 360) { + return WIDTH_SW360; + } + return WIDTH_DEFAULT; + } + + private static float getShortestWidth(BridgeContext context) { + DisplayMetrics metrics = context.getMetrics(); + float sw = metrics.widthPixels < metrics.heightPixels ? + metrics.widthPixels : metrics.heightPixels; + sw /= metrics.density; + return sw; } @Override diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java index 261cc98..47258b6 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java @@ -19,6 +19,7 @@ package com.android.layoutlib.bridge.impl; import com.android.layoutlib.bridge.util.Debug; import com.android.layoutlib.bridge.util.SparseWeakArray; +import android.annotation.Nullable; import android.util.SparseArray; import java.lang.ref.WeakReference; @@ -48,7 +49,7 @@ import java.util.List; * int -> Delegate class link. * * Native methods usually always have the int as parameters. The first thing the delegate method - * will do is call {@link #getDelegate(int)} to get the Java object matching the int. + * will do is call {@link #getDelegate(long)} to get the Java object matching the int. * * Typical native init methods are returning a new int back to the Java class, so * {@link #addNewDelegate(Object)} does the same. @@ -57,7 +58,7 @@ import java.util.List; * the Java object needs to count as a reference (even though it only holds an int), we use the * following mechanism: * - * - {@link #addNewDelegate(Object)} and {@link #removeJavaReferenceFor(int)} adds and removes + * - {@link #addNewDelegate(Object)} and {@link #removeJavaReferenceFor(long)} adds and removes * the delegate to/from a list. This list hold the reference and prevents the GC from reclaiming * the delegate. * @@ -70,12 +71,13 @@ import java.util.List; * @param <T> the delegate class to manage */ public final class DelegateManager<T> { + @SuppressWarnings("FieldCanBeLocal") private final Class<T> mClass; private final SparseWeakArray<T> mDelegates = new SparseWeakArray<T>(); /** list used to store delegates when their main object holds a reference to them. * This is to ensure that the WeakReference in the SparseWeakArray doesn't get GC'ed * @see #addNewDelegate(Object) - * @see #removeJavaReferenceFor(int) + * @see #removeJavaReferenceFor(long) */ private final List<T> mJavaReferences = new ArrayList<T>(); private int mDelegateCounter = 0; @@ -94,6 +96,7 @@ public final class DelegateManager<T> { * @param native_object the native int. * @return the delegate or null if not found. */ + @Nullable public T getDelegate(long native_object) { if (native_object > 0) { T delegate = mDelegates.get(native_object); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java index 803849f..6e67f59 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java @@ -17,10 +17,12 @@ package com.android.layoutlib.bridge.impl; -import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.NonNull; +import android.annotation.Nullable; + import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; @@ -35,24 +37,36 @@ import java.io.InputStream; */ public class ParserFactory { + public final static boolean LOG_PARSER = false; + private final static String ENCODING = "UTF-8"; //$NON-NLS-1$ - public final static boolean LOG_PARSER = false; + // Used to get a new XmlPullParser from the client. + @Nullable + private static com.android.ide.common.rendering.api.ParserFactory sParserFactory; - public static XmlPullParser create(File f) + public static void setParserFactory( + @Nullable com.android.ide.common.rendering.api.ParserFactory parserFactory) { + sParserFactory = parserFactory; + } + + @NonNull + public static XmlPullParser create(@NonNull File f) throws XmlPullParserException, FileNotFoundException { InputStream stream = new FileInputStream(f); return create(stream, f.getName(), f.length()); } - public static XmlPullParser create(InputStream stream, String name) + @NonNull + public static XmlPullParser create(@NonNull InputStream stream, @Nullable String name) throws XmlPullParserException { return create(stream, name, -1); } - private static XmlPullParser create(InputStream stream, String name, long size) - throws XmlPullParserException { - KXmlParser parser = instantiateParser(name); + @NonNull + private static XmlPullParser create(@NonNull InputStream stream, @Nullable String name, + long size) throws XmlPullParserException { + XmlPullParser parser = instantiateParser(name); stream = readAndClose(stream, name, size); @@ -60,19 +74,20 @@ public class ParserFactory { return parser; } - private static KXmlParser instantiateParser(String name) throws XmlPullParserException { - KXmlParser parser; - if (name != null) { - parser = new CustomParser(name); - } else { - parser = new KXmlParser(); + @NonNull + public static XmlPullParser instantiateParser(@Nullable String name) + throws XmlPullParserException { + if (sParserFactory == null) { + throw new XmlPullParserException("ParserFactory not initialized."); } + XmlPullParser parser = sParserFactory.createParser(name); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); return parser; } - private static InputStream readAndClose(InputStream stream, String name, long size) - throws XmlPullParserException { + @NonNull + private static InputStream readAndClose(@NonNull InputStream stream, @Nullable String name, + long size) throws XmlPullParserException { // just a sanity check. It's doubtful we'll have such big files! if (size > Integer.MAX_VALUE) { throw new XmlPullParserException("File " + name + " is too big to be parsed"); @@ -121,22 +136,8 @@ public class ParserFactory { } finally { try { bufferedStream.close(); - } catch (IOException e) { + } catch (IOException ignored) { } } } - - private static class CustomParser extends KXmlParser { - private final String mName; - - CustomParser(String name) { - super(); - mName = name; - } - - @Override - public String toString() { - return mName; - } - } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java index c708316..de77d57 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java @@ -227,6 +227,9 @@ public abstract class RenderAction<T extends RenderParams> extends FrameworkReso * The counterpart is {@link #tearDown()}. */ private void setUp() { + // setup the ParserFactory + ParserFactory.setParserFactory(mParams.getLayoutlibCallback().getParserFactory()); + // make sure the Resources object references the context (and other objects) for this // scene mContext.initResources(); @@ -271,7 +274,7 @@ public abstract class RenderAction<T extends RenderParams> extends FrameworkReso mContext.getRenderResources().setFrameworkResourceIdProvider(null); mContext.getRenderResources().setLogger(null); } - + ParserFactory.setParserFactory(null); } public static BridgeContext getCurrentContext() { diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java index 9e26502..26f9000 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java @@ -38,6 +38,9 @@ import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.ArrayList; import java.util.Collections; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index f6e5ef1..d571d35 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -16,8 +16,6 @@ package com.android.layoutlib.bridge.impl; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.AdapterBinding; import com.android.ide.common.rendering.api.HardwareConfig; import com.android.ide.common.rendering.api.IAnimationListener; @@ -46,6 +44,7 @@ import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.RenderParamsFlags; +import com.android.layoutlib.bridge.android.support.DesignLibUtil; import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; import com.android.layoutlib.bridge.bars.AppCompatActionBar; import com.android.layoutlib.bridge.bars.BridgeActionBar; @@ -68,6 +67,8 @@ import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Fragment_Delegate; import android.graphics.Bitmap; import android.graphics.Bitmap_Delegate; @@ -148,7 +149,6 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { private int mTitleBarSize; private int mActionBarSize; - // information being returned through the API private BufferedImage mImage; private List<ViewInfo> mViewInfoList; @@ -424,6 +424,8 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // post-inflate process. For now this supports TabHost/TabWidget postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null); + setActiveToolbar(view, context, params); + // get the background drawable if (mWindowBackground != null) { Drawable d = ResourceHelper.getDrawable(mWindowBackground, context); @@ -544,6 +546,8 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // now do the layout. mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); + handleScrolling(mViewRoot); + if (params.isLayoutOnly()) { // delete the canvas and image to reset them on the next full rendering mImage = null; @@ -1350,6 +1354,99 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } /** + * If the root layout is a CoordinatorLayout with an AppBar: + * Set the title of the AppBar to the title of the activity context. + */ + private void setActiveToolbar(View view, BridgeContext context, SessionParams params) { + View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT); + if (coordinatorLayout == null) { + return; + } + View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT); + if (appBar == null) { + return; + } + ViewGroup collapsingToolbar = + (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT); + if (collapsingToolbar == null) { + return; + } + if (!hasToolbar(collapsingToolbar)) { + return; + } + RenderResources res = context.getRenderResources(); + String title = params.getAppLabel(); + ResourceValue titleValue = res.findResValue(title, false); + if (titleValue != null && titleValue.getValue() != null) { + title = titleValue.getValue(); + } + DesignLibUtil.setTitle(collapsingToolbar, title); + } + + private View findChildView(View view, String className) { + if (!(view instanceof ViewGroup)) { + return null; + } + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + if (isInstanceOf(group.getChildAt(i), className)) { + return group.getChildAt(i); + } + } + return null; + } + + private boolean hasToolbar(View collapsingToolbar) { + if (!(collapsingToolbar instanceof ViewGroup)) { + return false; + } + ViewGroup group = (ViewGroup) collapsingToolbar; + for (int i = 0; i < group.getChildCount(); i++) { + if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) { + return true; + } + } + return false; + } + + /** + * Set the vertical scroll position on all the components with the "scrollY" attribute. If the + * component supports nested scrolling attempt that first, then use the unconsumed scroll part + * to scroll the content in the component. + */ + private void handleScrolling(View view) { + BridgeContext context = getContext(); + int scrollPos = context.getScrollYPos(view); + if (scrollPos != 0) { + if (view.isNestedScrollingEnabled()) { + int[] consumed = new int[2]; + if (view.startNestedScroll(DesignLibUtil.SCROLL_AXIS_VERTICAL)) { + view.dispatchNestedPreScroll(0, scrollPos, consumed, null); + view.dispatchNestedScroll(consumed[0], consumed[1], 0, scrollPos, null); + view.stopNestedScroll(); + scrollPos -= consumed[1]; + } + } + if (scrollPos != 0) { + view.scrollBy(0, scrollPos); + } else { + view.scrollBy(0, scrollPos); + } + } else { + view.scrollBy(0, scrollPos); + } + + if (!(view instanceof ViewGroup)) { + return; + } + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + handleScrolling(child); + } + } + + /** * Check if the object is an instance of a class named {@code className}. This doesn't work * for interfaces. */ diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java index 677c744..ca77193 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java @@ -16,7 +16,7 @@ package com.android.layoutlib.bridge.impl; -import com.android.annotations.NonNull; +import com.android.SdkConstants; import com.android.ide.common.rendering.api.DensityBasedResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderResources; @@ -31,6 +31,7 @@ import com.android.resources.Density; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.NonNull; import android.content.res.ColorStateList; import android.content.res.Resources.Theme; import android.graphics.Bitmap; @@ -70,6 +71,10 @@ public final class ResourceHelper { public static int getColor(String value) { if (value != null) { if (!value.startsWith("#")) { + if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) { + throw new NumberFormatException(String.format( + "Attribute '%s' not found. Are you using the right theme?", value)); + } throw new NumberFormatException( String.format("Color value '%s' must start with #", value)); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java index 979aa33..08a8faf 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java @@ -16,10 +16,10 @@ package com.android.layoutlib.bridge.util; -import com.android.annotations.NonNull; import com.android.resources.ResourceType; import com.android.util.Pair; +import android.annotation.NonNull; import android.util.SparseArray; import java.util.HashMap; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java index 8e61edf..b324451 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/ReflectionUtils.java @@ -16,8 +16,8 @@ package com.android.layoutlib.bridge.util; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; +import android.annotation.NonNull; +import android.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/tools/layoutlib/bridge/src/libcore/icu/DateIntervalFormat_Delegate.java b/tools/layoutlib/bridge/src/libcore/icu/DateIntervalFormat_Delegate.java deleted file mode 100644 index d94c205..0000000 --- a/tools/layoutlib/bridge/src/libcore/icu/DateIntervalFormat_Delegate.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2013 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. - */ - -package libcore.icu; - -import java.text.FieldPosition; - -import com.android.ide.common.rendering.api.LayoutLog; -import com.android.layoutlib.bridge.Bridge; -import com.android.layoutlib.bridge.impl.DelegateManager; -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; -import com.ibm.icu.text.DateIntervalFormat; -import com.ibm.icu.util.DateInterval; -import com.ibm.icu.util.TimeZone; -import com.ibm.icu.util.ULocale; - -public class DateIntervalFormat_Delegate { - - // ---- delegate manager ---- - private static final DelegateManager<DateIntervalFormat_Delegate> sManager = - new DelegateManager<DateIntervalFormat_Delegate>(DateIntervalFormat_Delegate.class); - - // ---- delegate data ---- - private DateIntervalFormat mFormat; - - - // ---- native methods ---- - - @LayoutlibDelegate - /*package*/static String formatDateInterval(long address, long fromDate, long toDate) { - DateIntervalFormat_Delegate delegate = sManager.getDelegate((int)address); - if (delegate == null) { - Bridge.getLog().error(LayoutLog.TAG_BROKEN, - "Unable for find native DateIntervalFormat", null); - return null; - } - DateInterval interval = new DateInterval(fromDate, toDate); - StringBuffer sb = new StringBuffer(); - FieldPosition pos = new FieldPosition(0); - delegate.mFormat.format(interval, sb, pos); - return sb.toString(); - } - - @LayoutlibDelegate - /*package*/ static long createDateIntervalFormat(String skeleton, String localeName, - String tzName) { - TimeZone prevDefaultTz = TimeZone.getDefault(); - TimeZone.setDefault(TimeZone.getTimeZone(tzName)); - DateIntervalFormat_Delegate newDelegate = new DateIntervalFormat_Delegate(); - newDelegate.mFormat = - DateIntervalFormat.getInstance(skeleton, new ULocale(localeName)); - TimeZone.setDefault(prevDefaultTz); - return sManager.addNewDelegate(newDelegate); - } - - @LayoutlibDelegate - /*package*/ static void destroyDateIntervalFormat(long address) { - sManager.removeJavaReferenceFor((int)address); - } - -} diff --git a/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java b/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java index b8b5fed..9c58010 100644 --- a/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java +++ b/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java @@ -17,9 +17,11 @@ package libcore.icu; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; -import com.ibm.icu.text.DateTimePatternGenerator; -import com.ibm.icu.util.Currency; -import com.ibm.icu.util.ULocale; + +import android.icu.text.DateTimePatternGenerator; +import android.icu.util.Currency; +import android.icu.util.ULocale; +import android.icu.util.VersionInfo; import java.util.Locale; @@ -53,18 +55,19 @@ public class ICU_Delegate { } @LayoutlibDelegate + @SuppressWarnings("deprecation") /*package*/ static String getCldrVersion() { - return "22.1.1"; // TODO: check what the right value should be. + return VersionInfo.ICU_DATA_VERSION.toString(); } @LayoutlibDelegate /*package*/ static String getIcuVersion() { - return "unknown_layoutlib"; + return VersionInfo.ICU_VERSION.toString(); } @LayoutlibDelegate /*package*/ static String getUnicodeVersion() { - return "5.2"; + return VersionInfo.UNICODE_7_0.toString(); } @LayoutlibDelegate @@ -181,8 +184,8 @@ public class ICU_Delegate { /*package*/ static boolean initLocaleDataNative(String locale, LocaleData result) { // Used by Calendar. - result.firstDayOfWeek = Integer.valueOf(1); - result.minimalDaysInFirstWeek = Integer.valueOf(1); + result.firstDayOfWeek = 1; + result.minimalDaysInFirstWeek = 1; // Used by DateFormatSymbols. result.amPm = new String[] { "AM", "PM" }; @@ -252,4 +255,9 @@ public class ICU_Delegate { /*package*/ static String getDefaultLocale() { return ICU.getDefaultLocale(); } + + @LayoutlibDelegate + /*package*/ static String getTZDataVersion() { + return ICU.getTZDataVersion(); + } } diff --git a/tools/layoutlib/bridge/tests/Android.mk b/tools/layoutlib/bridge/tests/Android.mk index 11390c3..5eef24a 100644 --- a/tools/layoutlib/bridge/tests/Android.mk +++ b/tools/layoutlib/bridge/tests/Android.mk @@ -26,7 +26,6 @@ LOCAL_MODULE_TAGS := optional LOCAL_JAVA_LIBRARIES := layoutlib \ kxml2-2.3.0 \ - icu4j \ layoutlib_api-prebuilt \ tools-common-prebuilt \ sdk-common \ diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build.gradle b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build.gradle index 0f37fce..4561e1b 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build.gradle +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.1.3' + classpath 'com.android.tools.build:gradle:1.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -19,12 +19,12 @@ allprojects { apply plugin: 'com.android.application' android { - compileSdkVersion 21 + compileSdkVersion 22 buildToolsVersion '21.1.2' defaultConfig { applicationId 'com.android.layoutlib.test.myapplication' - minSdkVersion 19 - targetSdkVersion 21 + minSdkVersion 21 + targetSdkVersion 22 versionCode 1 versionName '1.0' } diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/ArraysCheckWidget.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/ArraysCheckWidget.class Binary files differnew file mode 100644 index 0000000..e0373cb --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/ArraysCheckWidget.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/MyActivity.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/MyActivity.class Binary files differindex 8af93eb..ec42017 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/MyActivity.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/MyActivity.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$array.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$array.class Binary files differnew file mode 100644 index 0000000..b87f193 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$array.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$attr.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$attr.class Binary files differindex 9bab801..e2968d4 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$attr.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$attr.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$dimen.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$dimen.class Binary files differindex 7ad8605..0e208f2 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$dimen.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$dimen.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$drawable.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$drawable.class Binary files differindex e9e0a33..2b77af3 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$drawable.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$drawable.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$id.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$id.class Binary files differindex 069f9f7..fd01b44 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$id.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$id.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$integer.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$integer.class Binary files differnew file mode 100644 index 0000000..91cf5b6 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$integer.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$layout.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$layout.class Binary files differindex 36e2688..e172b2d 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$layout.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$layout.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$menu.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$menu.class Binary files differindex ca438ad..aecbff6 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$menu.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$menu.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$string.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$string.class Binary files differindex a98abf5..fc3f236 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$string.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$string.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$style.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$style.class Binary files differindex 7d8cc84..83ad35b 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$style.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R$style.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R.class b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R.class Binary files differindex 7e6113b..d5b81c4 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R.class +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/build/intermediates/classes/debug/com/android/layoutlib/test/myapplication/R.class diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png Binary files differindex c9b76be..2b86bfb 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png Binary files differnew file mode 100644 index 0000000..9a13568 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/ArraysCheckWidget.java b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/ArraysCheckWidget.java new file mode 100644 index 0000000..41d75de --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/java/com/android/layoutlib/test/myapplication/ArraysCheckWidget.java @@ -0,0 +1,41 @@ +package com.android.layoutlib.test.myapplication; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * A widget to test obtaining arrays from resources. + */ +public class ArraysCheckWidget extends LinearLayout { + public ArraysCheckWidget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ArraysCheckWidget(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ArraysCheckWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + Resources resources = context.getResources(); + for (CharSequence chars : resources.getTextArray(R.array.array)) { + addTextView(context, chars); + } + for (int i : resources.getIntArray(R.array.int_array)) { + addTextView(context, String.valueOf(i)); + } + for (String string : resources.getStringArray(R.array.string_array)) { + addTextView(context, string); + } + } + + private void addTextView(Context context, CharSequence string) { + TextView textView = new TextView(context); + textView.setText(string); + textView.setTextSize(30); + addView(textView); + } +} diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/array_check.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/array_check.xml new file mode 100644 index 0000000..50646ab --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/array_check.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.android.layoutlib.test.myapplication.ArraysCheckWidget xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + +</com.android.layoutlib.test.myapplication.ArraysCheckWidget>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml new file mode 100644 index 0000000..f6e14d2 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <array name="array"> + <item>first</item> + <item>second</item> + </array> + <integer-array name="int_array"> + <item>1</item> + <item>0xaB</item> <!-- hex entry (decimal 171) --> + <item>010</item> <!-- octal entry --> + <item>@integer/ten</item> <!-- value reference --> + <item>?attr/myattr</item> <!-- theme attr reference --> + </integer-array> + <string-array name="string_array"> + <item>mystring</item> + <item>@string/hello_world</item> <!-- string ref in appNs --> + <!-- theme ref in android NS. value = @string/candidates_style = <u>candidates</u> --> + <item>?android:attr/candidatesTextStyleSpans</item> + <item>@android:string/unknownName</item> <!-- value = Unknown --> + </string-array> + + <!-- resources that the above array can refer to --> + <integer name="ten">10</integer> + <integer name="twelve">12</integer> +</resources>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/attrs.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/attrs.xml new file mode 100644 index 0000000..894e18c --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/attrs.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <attr name="myattr" format="integer" /> +</resources>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml index ff6c9d2..88c9cbc 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml @@ -1,7 +1,8 @@ <resources> <!-- Base application theme. --> - <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar"> + <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar"> + <item name="myattr">@integer/ten</item> <!-- Customize your theme here. --> </style> diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java index 8b362ec..d8937f4 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java @@ -192,12 +192,12 @@ public class TestDelegates extends TestCase { StringBuilder sb = new StringBuilder(method.getName() + "("); for (int j = 0; j < parameters.length; j++) { Class<?> theClass = parameters[j]; - sb.append(theClass.getName()); int dimensions = 0; while (theClass.isArray()) { dimensions++; theClass = theClass.getComponentType(); } + sb.append(theClass.getName()); for (int i = 0; i < dimensions; i++) { sb.append("[]"); } diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java index 92fcf90..77c997b 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java @@ -18,12 +18,26 @@ package com.android.layoutlib.bridge.android; import com.android.layoutlib.bridge.impl.ParserFactory; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.kxml2.io.KXmlParser; import org.w3c.dom.Node; import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; -import junit.framework.TestCase; +import android.annotation.NonNull; -public class BridgeXmlBlockParserTest extends TestCase { +import static org.junit.Assert.assertEquals; + +public class BridgeXmlBlockParserTest { + + @BeforeClass + public static void setUp() { + ParserFactory.setParserFactory(new ParserFactoryImpl()); + } + + @Test public void testXmlBlockParser() throws Exception { XmlPullParser parser = ParserFactory.create( @@ -65,7 +79,7 @@ public class BridgeXmlBlockParserTest extends TestCase { //------------ /** - * Quick'n'dirty debug helper that dumps an XML structure to stdout. + * Quick 'n' dirty debug helper that dumps an XML structure to stdout. */ @SuppressWarnings("unused") private void dump(Node node, String prefix) { @@ -104,7 +118,20 @@ public class BridgeXmlBlockParserTest extends TestCase { if (n != null) { dump(n, prefix); } + } + @AfterClass + public static void tearDown() { + ParserFactory.setParserFactory(null); } + private static class ParserFactoryImpl + extends com.android.ide.common.rendering.api.ParserFactory { + + @NonNull + @Override + public XmlPullParser createParser(String displayName) throws XmlPullParserException { + return new KXmlParser(); + } + } } diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java index d7e5486..16911bd 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java @@ -16,7 +16,7 @@ package com.android.layoutlib.bridge.intensive; -import com.android.annotations.NonNull; +import android.annotation.NonNull; import java.awt.AlphaComposite; import java.awt.Color; diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java index f2a039e..272a2b8 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java @@ -16,7 +16,6 @@ package com.android.layoutlib.bridge.intensive; -import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.Result; @@ -34,9 +33,13 @@ import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; import com.android.utils.ILogger; -import org.junit.Before; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import android.annotation.NonNull; +import android.annotation.Nullable; + import java.io.File; import java.io.FileFilter; import java.io.IOException; @@ -81,11 +84,11 @@ public class Main { /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/ private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res"; - private LayoutLog mLayoutLibLog; - private FrameworkResources mFrameworkRepo; - private ResourceRepository mProjectResources; - private ILogger mLogger; - private Bridge mBridge; + private static LayoutLog sLayoutLibLog; + private static FrameworkResources sFrameworkRepo; + private static ResourceRepository sProjectResources; + private static ILogger sLogger; + private static Bridge sBridge; static { // Test that System Properties are properly set. @@ -122,9 +125,14 @@ public class Main { if (platformDir != null) { return platformDir; } - // Test if workingDir is platform/frameworks/base/tools/layoutlib. That is, root should be - // workingDir/../../../../ (4 levels up) + + // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge. File currentDir = workingDir; + if (currentDir.getName().equalsIgnoreCase("bridge")) { + currentDir = currentDir.getParentFile(); + } + // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be + // workingDir/../../../../ (4 levels up) for (int i = 0; i < 4; i++) { if (currentDir != null) { currentDir = currentDir.getParentFile(); @@ -249,15 +257,15 @@ public class Main { /** * Initialize the bridge and the resource maps. */ - @Before - public void setUp() { + @BeforeClass + public static void setUp() { File data_dir = new File(PLATFORM_DIR, "data"); File res = new File(data_dir, "res"); - mFrameworkRepo = new FrameworkResources(new FolderWrapper(res)); - mFrameworkRepo.loadResources(); - mFrameworkRepo.loadPublicResources(getLogger()); + sFrameworkRepo = new FrameworkResources(new FolderWrapper(res)); + sFrameworkRepo.loadResources(); + sFrameworkRepo.loadPublicResources(getLogger()); - mProjectResources = + sProjectResources = new ResourceRepository(new FolderWrapper(TEST_RES_DIR + APP_TEST_RES), false) { @NonNull @Override @@ -265,13 +273,13 @@ public class Main { return new ResourceItem(name); } }; - mProjectResources.loadResources(); + sProjectResources.loadResources(); File fontLocation = new File(data_dir, "fonts"); File buildProp = new File(PLATFORM_DIR, "build.prop"); File attrs = new File(res, "values" + File.separator + "attrs.xml"); - mBridge = new Bridge(); - mBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation, + sBridge = new Bridge(); + sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation, ConfigGenerator.getEnumMap(attrs), getLayoutLog()); } @@ -288,6 +296,20 @@ public class Main { renderAndVerify("allwidgets.xml", "allwidgets.png"); } + @Test + public void testArrayCheck() throws ClassNotFoundException { + renderAndVerify("array_check.xml", "array_check.png"); + } + + @AfterClass + public static void tearDown() { + sLayoutLibLog = null; + sFrameworkRepo = null; + sProjectResources = null; + sLogger = null; + sBridge = null; + } + /** * Create a new rendering session and test that rendering given layout on nexus 5 * doesn't throw any exceptions and matches the provided image. @@ -302,7 +324,7 @@ public class Main { // TODO: Set up action bar handler properly to test menu rendering. // Create session params. SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, layoutLibCallback); - RenderSession session = mBridge.createSession(params); + RenderSession session = sBridge.createSession(params); if (!session.getResult().isSuccess()) { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); @@ -328,9 +350,9 @@ public class Main { ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback) { FolderConfiguration config = configGenerator.getFolderConfig(); ResourceResolver resourceResolver = - ResourceResolver.create(mProjectResources.getConfiguredResources(config), - mFrameworkRepo.getConfiguredResources(config), - "Theme.Material.Light.DarkActionBar", false); + ResourceResolver.create(sProjectResources.getConfiguredResources(config), + sFrameworkRepo.getConfiguredResources(config), + "AppTheme", true); return new SessionParams( layoutParser, @@ -344,9 +366,9 @@ public class Main { getLayoutLog()); } - private LayoutLog getLayoutLog() { - if (mLayoutLibLog == null) { - mLayoutLibLog = new LayoutLog() { + private static LayoutLog getLayoutLog() { + if (sLayoutLibLog == null) { + sLayoutLibLog = new LayoutLog() { @Override public void warning(String tag, String message, Object data) { System.out.println("Warning " + tag + ": " + message); @@ -354,13 +376,14 @@ public class Main { } @Override - public void fidelityWarning(String tag, String message, Throwable throwable, - Object data) { + public void fidelityWarning(@Nullable String tag, String message, + Throwable throwable, Object data) { + System.out.println("FidelityWarning " + tag + ": " + message); if (throwable != null) { throwable.printStackTrace(); } - failWithMsg(message); + failWithMsg(message == null ? "" : message); } @Override @@ -379,18 +402,18 @@ public class Main { } }; } - return mLayoutLibLog; + return sLayoutLibLog; } - private ILogger getLogger() { - if (mLogger == null) { - mLogger = new ILogger() { + private static ILogger getLogger() { + if (sLogger == null) { + sLogger = new ILogger() { @Override - public void error(Throwable t, String msgFormat, Object... args) { + public void error(Throwable t, @Nullable String msgFormat, Object... args) { if (t != null) { t.printStackTrace(); } - failWithMsg(msgFormat, args); + failWithMsg(msgFormat == null ? "" : msgFormat, args); } @Override @@ -409,7 +432,7 @@ public class Main { } }; } - return mLogger; + return sLogger; } private static void failWithMsg(@NonNull String msgFormat, Object... args) { diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java index 1191df6..8964c45 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java @@ -111,6 +111,21 @@ public class ConfigGenerator { .setSoftButtons(true) .setNavigation(Navigation.NONAV); + public static final ConfigGenerator NEXUS_5_LAND = new ConfigGenerator() + .setScreenHeight(1080) + .setScreenWidth(1920) + .setXdpi(445) + .setYdpi(445) + .setOrientation(ScreenOrientation.LANDSCAPE) + .setDensity(Density.XXHIGH) + .setRatio(ScreenRatio.NOTLONG) + .setSize(ScreenSize.NORMAL) + .setKeyboard(Keyboard.NOKEY) + .setTouchScreen(TouchScreen.FINGER) + .setKeyboardState(KeyboardState.SOFT) + .setSoftButtons(true) + .setNavigation(Navigation.NONAV); + private static final String TAG_ATTR = "attr"; private static final String TAG_ENUM = "enum"; private static final String TAG_FLAG = "flag"; diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/LayoutLibTestCallback.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/LayoutLibTestCallback.java index 5b648ef..6c16ed0 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/LayoutLibTestCallback.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/LayoutLibTestCallback.java @@ -21,6 +21,7 @@ import com.android.ide.common.rendering.api.ActionBarCallback; import com.android.ide.common.rendering.api.AdapterBinding; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.LayoutlibCallback; +import com.android.ide.common.rendering.api.ParserFactory; import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.IntArrayWrapper; @@ -28,6 +29,13 @@ import com.android.resources.ResourceType; import com.android.util.Pair; import com.android.utils.ILogger; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.NonNull; +import android.annotation.Nullable; + import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -155,4 +163,17 @@ public class LayoutLibTestCallback extends LayoutlibCallback { public boolean supports(int ideFeature) { return false; } + + @NonNull + @Override + public ParserFactory getParserFactory() { + return new ParserFactory() { + @NonNull + @Override + public XmlPullParser createParser(@Nullable String debugName) + throws XmlPullParserException { + return new KXmlParser(); + } + }; + } } diff --git a/tools/layoutlib/bridge/update_nav_icons.sh b/tools/layoutlib/bridge/update_nav_icons.sh new file mode 100755 index 0000000..7030d19 --- /dev/null +++ b/tools/layoutlib/bridge/update_nav_icons.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# copies the navigation bar icons from system ui code to layoutlib. +# to run, simply execute the script. (if not using bash, cd to the dir +# containing this script and then run by ./update_nav_icons.sh) + +# Try to get the location of this script. +if [ -n $BASH ]; then + # see http://stackoverflow.com/a/246128/1546000 + MY_LOCATION=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + cd $MY_LOCATION +else + # Let's assume script was run from the same dir. + MY_LOCATION=$(pwd) +fi + +# Check mac or linux to get sed argument to enable extended regex. +case $(uname -s) in + Darwin) + EXT_REGEX="-E" + ;; + *) + EXT_REGEX="-r" + ;; +esac + + +FB="frameworks/base" +# frameworks/base relative to current location +FB=$(echo $MY_LOCATION | sed $EXT_REGEX -e "s,.*$FB[^/]*/,," -e "s,[^/]+,..,g") +CURRENT_API=21 # update only if icons change from this api version. +DENSITIES="ldpi mdpi hdpi xhdpi xxhdpi" +ICONS="ic_sysbar_back.png ic_sysbar_home.png ic_sysbar_recent.png" +BARS="./resources/bars/" + +for icon in $ICONS +do + for density in $DENSITIES + do + destination="$BARS/v$CURRENT_API/$density/" + mkdir -p "$destination" # create if not present. + cp -v "$FB/packages/SystemUI/res/drawable-$density/$icon" "$destination" + done + + for density in $DENSITIES + do + destination="$BARS/v$CURRENT_API/ldrtl-$density/" + mkdir -p "$destination" + cp -v "$FB/packages/SystemUI/res/drawable-ldrtl-$density/$icon" "$destination" + done +done diff --git a/tools/layoutlib/create/create.iml b/tools/layoutlib/create/create.iml index b7e8eb3..9b18e73 100644 --- a/tools/layoutlib/create/create.iml +++ b/tools/layoutlib/create/create.iml @@ -11,8 +11,17 @@ </content> <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="asm-4.0" level="project" /> + <orderEntry type="module-library"> + <library name="asm-4.0"> + <CLASSES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/asm/asm-4.0.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/asm/src.zip!/" /> + </SOURCES> + </library> + </orderEntry> <orderEntry type="library" scope="TEST" name="JUnit4" level="application" /> </component> -</module> - +</module>
\ No newline at end of file diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java index aa51c46..c8b2b84 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -728,7 +728,7 @@ public class AsmAnalyzer { // Check if method needs to replaced by a call to a different method. - if (ReplaceMethodCallsAdapter.isReplacementNeeded(owner, name, desc)) { + if (ReplaceMethodCallsAdapter.isReplacementNeeded(owner, name, desc, mOwnerClass)) { mReplaceMethodCallClasses.add(mOwnerClass); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java index bd6f070..f6c2626 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -24,9 +24,11 @@ import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -72,6 +74,9 @@ public class AsmGenerator { /** FQCN Names of classes to refactor. All reference to old-FQCN will be updated to new-FQCN. * map old-FQCN => new-FQCN */ private final HashMap<String, String> mRefactorClasses; + /** Methods to inject. FQCN of class in which method should be injected => runnable that does + * the injection. */ + private final Map<String, ICreateInfo.InjectMethodRunnable> mInjectedMethodsMap; /** * Creates a new generator that can generate the output JAR with the stubbed classes. @@ -83,7 +88,23 @@ public class AsmGenerator { public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo) { mLog = log; mOsDestJar = osDestJar; - mInjectClasses = createInfo.getInjectedClasses(); + ArrayList<Class<?>> injectedClasses = + new ArrayList<Class<?>>(Arrays.asList(createInfo.getInjectedClasses())); + // Search for and add anonymous inner classes also. + ListIterator<Class<?>> iter = injectedClasses.listIterator(); + while (iter.hasNext()) { + Class<?> clazz = iter.next(); + try { + int i = 1; + while(i < 100) { + iter.add(Class.forName(clazz.getName() + "$" + i)); + i++; + } + } catch (ClassNotFoundException ignored) { + // Expected. + } + } + mInjectClasses = injectedClasses.toArray(new Class<?>[0]); mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods())); // Create the map/set of methods to change to delegates @@ -165,6 +186,8 @@ public class AsmGenerator { } returnTypes.add(binaryToInternalClassName(className)); } + + mInjectedMethodsMap = createInfo.getInjectedMethodsMap(); } /** @@ -285,13 +308,7 @@ public class AsmGenerator { * e.g. it returns something like "com/foo/OuterClass$InnerClass1$InnerClass2.class" */ private String classToEntryPath(Class<?> clazz) { - String name = ""; - Class<?> parent; - while ((parent = clazz.getEnclosingClass()) != null) { - name = "$" + clazz.getSimpleName() + name; - clazz = parent; - } - return classNameToEntryPath(clazz.getCanonicalName() + name); + return classNameToEntryPath(clazz.getName()); } /** @@ -337,7 +354,7 @@ public class AsmGenerator { ClassVisitor cv = cw; if (mReplaceMethodCallsClasses.contains(className)) { - cv = new ReplaceMethodCallsAdapter(cv); + cv = new ReplaceMethodCallsAdapter(cv, className); } cv = new RefactorClassAdapter(cv, mRefactorClasses); @@ -345,6 +362,10 @@ public class AsmGenerator { cv = new RenameClassAdapter(cv, className, newName); } + String binaryNewName = newName.replace('/', '.'); + if (mInjectedMethodsMap.keySet().contains(binaryNewName)) { + cv = new InjectMethodsAdapter(cv, mInjectedMethodsMap.get(binaryNewName)); + } cv = new TransformClassAdapter(mLog, mStubMethods, mDeleteReturns.get(className), newName, cv, stubNativesOnly); diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java index 8f50c5d..499bea4 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -26,7 +26,9 @@ import com.android.tools.layoutlib.java.System_Delegate; import com.android.tools.layoutlib.java.UnsafeByteSequence; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -105,6 +107,7 @@ public final class CreateInfo implements ICreateInfo { return JAVA_PKG_CLASSES; } + @Override public Set<String> getExcludedClasses() { String[] refactoredClasses = getJavaPkgClasses(); int count = refactoredClasses.length / 2 + EXCLUDED_CLASSES.length; @@ -115,6 +118,12 @@ public final class CreateInfo implements ICreateInfo { excludedClasses.addAll(Arrays.asList(EXCLUDED_CLASSES)); return excludedClasses; } + + @Override + public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { + return INJECTED_METHODS; + } + //----- /** @@ -127,6 +136,8 @@ public final class CreateInfo implements ICreateInfo { ICreateInfo.class, CreateInfo.class, LayoutlibDelegate.class, + InjectMethodRunnable.class, + InjectMethodRunnables.class, /* Java package classes */ AutoCloseable.class, Objects.class, @@ -156,12 +167,14 @@ public final class CreateInfo implements ICreateInfo { "android.os.HandlerThread#run", "android.preference.Preference#getView", "android.text.format.DateFormat#is24HourFormat", + "android.text.Hyphenator#getSystemHyphenatorLocation", "android.util.Xml#newPullParser", "android.view.Choreographer#getRefreshRate", "android.view.Display#updateDisplayInfoLocked", "android.view.Display#getWindowManager", "android.view.LayoutInflater#rInflate", "android.view.LayoutInflater#parseInclude", + "android.view.View#getWindowToken", "android.view.View#isInEditMode", "android.view.ViewRootImpl#isInTouchMode", "android.view.WindowManagerGlobal#getWindowManagerService", @@ -231,7 +244,6 @@ public final class CreateInfo implements ICreateInfo { "android.text.AndroidBidi", "android.text.StaticLayout", "android.view.Display", - "libcore.icu.DateIntervalFormat", "libcore.icu.ICU", }; @@ -254,7 +266,6 @@ public final class CreateInfo implements ICreateInfo { "android.view.SurfaceView", "android.view._Original_SurfaceView", "android.view.accessibility.AccessibilityManager", "android.view.accessibility._Original_AccessibilityManager", "android.webkit.WebView", "android.webkit._Original_WebView", - "com.android.internal.policy.PolicyManager", "com.android.internal.policy._Original_PolicyManager", }; /** @@ -287,5 +298,10 @@ public final class CreateInfo implements ICreateInfo { private final static String[] DELETE_RETURNS = new String[] { null }; // separator, for next class/methods list. -} + private final static Map<String, InjectMethodRunnable> INJECTED_METHODS = + new HashMap<String, InjectMethodRunnable>(1) {{ + put("android.content.Context", + InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); + }}; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java index 7690fcd..61b64a2 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java @@ -307,7 +307,9 @@ public class DependencyFinder { try { // exclude classes that are part of the default JRE (the one executing this program) - if (getClass().getClassLoader().loadClass(className) != null) { + // or in java package (we won't be able to load them anyway). + if (className.startsWith("java.") || + getClass().getClassLoader().loadClass(className) != null) { return; } } catch (ClassNotFoundException e) { diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java index e49a668..54b1fe6 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java @@ -16,6 +16,9 @@ package com.android.tools.layoutlib.create; +import org.objectweb.asm.ClassVisitor; + +import java.util.Map; import java.util.Set; /** @@ -27,33 +30,33 @@ public interface ICreateInfo { * Returns the list of class from layoutlib_create to inject in layoutlib. * The list can be empty but must not be null. */ - public abstract Class<?>[] getInjectedClasses(); + Class<?>[] getInjectedClasses(); /** * Returns the list of methods to rewrite as delegates. * The list can be empty but must not be null. */ - public abstract String[] getDelegateMethods(); + String[] getDelegateMethods(); /** * Returns the list of classes on which to delegate all native methods. * The list can be empty but must not be null. */ - public abstract String[] getDelegateClassNatives(); + String[] getDelegateClassNatives(); /** * Returns The list of methods to stub out. Each entry must be in the form * "package.package.OuterClass$InnerClass#MethodName". * The list can be empty but must not be null. */ - public abstract String[] getOverriddenMethods(); + String[] getOverriddenMethods(); /** * Returns the list of classes to rename, must be an even list: the binary FQCN * of class to replace followed by the new FQCN. * The list can be empty but must not be null. */ - public abstract String[] getRenamedClasses(); + String[] getRenamedClasses(); /** * Returns the list of classes for which the methods returning them should be deleted. @@ -62,7 +65,7 @@ public interface ICreateInfo { * the methods to delete. * The list can be empty but must not be null. */ - public abstract String[] getDeleteReturns(); + String[] getDeleteReturns(); /** * Returns the list of classes to refactor, must be an even list: the @@ -70,7 +73,24 @@ public interface ICreateInfo { * to the old class should be updated to the new class. * The list can be empty but must not be null. */ - public abstract String[] getJavaPkgClasses(); + String[] getJavaPkgClasses(); + + Set<String> getExcludedClasses(); + + /** + * Returns a map from binary FQCN className to {@link InjectMethodRunnable} which will be + * called to inject methods into a class. + * Can be empty but must not be null. + */ + Map<String, InjectMethodRunnable> getInjectedMethodsMap(); - public abstract Set<String> getExcludedClasses(); + abstract class InjectMethodRunnable { + /** + * @param cv Must be {@link ClassVisitor}. However, the param type is object so that when + * loading the class, ClassVisitor is not loaded. This is because when injecting + * CreateInfo in LayoutLib (see {@link #getInjectedClasses()}, we don't want to inject + * asm classes also, but still keep CreateInfo loadable. + */ + public abstract void generateMethods(Object cv); + } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java new file mode 100644 index 0000000..37fc096 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.create.ICreateInfo.InjectMethodRunnable; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +public class InjectMethodRunnables { + public static final ICreateInfo.InjectMethodRunnable CONTEXT_GET_FRAMEWORK_CLASS_LOADER + = new InjectMethodRunnable() { + @Override + public void generateMethods(Object classVisitor) { + assert classVisitor instanceof ClassVisitor; + ClassVisitor cv = (ClassVisitor) classVisitor; + // generated by compiling the class: + // class foo { public ClassLoader getFrameworkClassLoader() { return getClass().getClassLoader(); } } + // and then running ASMifier on it: + // java -classpath asm-debug-all-5.0.2.jar:. org.objectweb.asm.util.ASMifier foo + MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getFrameworkClassLoader", + "()Ljava/lang/ClassLoader;", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", + "()Ljava/lang/Class;"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", + "()Ljava/lang/ClassLoader;"); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + // generated code ends. + } + }; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java new file mode 100644 index 0000000..ea2b9c9 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.create.ICreateInfo.InjectMethodRunnable; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Injects methods into some classes. + */ +public class InjectMethodsAdapter extends ClassVisitor { + + private final ICreateInfo.InjectMethodRunnable mRunnable; + + public InjectMethodsAdapter(ClassVisitor cv, InjectMethodRunnable runnable) { + super(Opcodes.ASM4, cv); + mRunnable = runnable; + } + + @Override + public void visitEnd() { + mRunnable.generateMethods(this); + super.visitEnd(); + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java index fa570c8..2951edb 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -18,8 +18,6 @@ package com.android.tools.layoutlib.create; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -105,7 +103,7 @@ public class Main { "com.android.internal.widget.*", "android.text.**", "android.graphics.*", - "android.graphics.drawable.*", + "android.graphics.drawable.**", "android.content.*", "android.content.res.*", "android.preference.*", @@ -118,10 +116,14 @@ public class Main { "android.app.DatePickerDialog", // b.android.com/28318 "android.app.TimePickerDialog", // b.android.com/61515 "com.android.internal.view.menu.ActionMenu", + "android.icu.**", // needed by LayoutLib + "android.annotation.NonNull", // annotations + "android.annotation.Nullable", // annotations }, excludeClasses, new String[] { "com/android/i18n/phonenumbers/data/*", + "android/icu/impl/data/**" }); aa.analyze(); agen.generate(); diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java index 384d8ca..4369148 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java @@ -62,14 +62,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { // Case 1: java.lang.System.arraycopy() METHOD_REPLACERS.add(new MethodReplacer() { @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && "arraycopy".equals(name) && ARRAYCOPY_DESCRIPTORS.contains(desc); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.desc = "(Ljava/lang/Object;ILjava/lang/Object;II)V"; } }); @@ -81,14 +80,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { Type.getMethodDescriptor(STRING, Type.getType(Locale.class)); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && "()Ljava/lang/String;".equals(desc) && ("toLanguageTag".equals(name) || "getScript".equals(name)); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.opcode = Opcodes.INVOKESTATIC; mi.owner = ANDROID_LOCALE_CLASS; mi.desc = LOCALE_TO_STRING; @@ -103,7 +101,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { Type.getType(Locale.class), STRING); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LOCALE_CLASS.equals(owner) && ("adjustLanguageCode".equals(name) && desc.equals(STRING_TO_STRING) || "forLanguageTag".equals(name) && desc.equals(STRING_TO_LOCALE)); @@ -111,7 +109,6 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.owner = ANDROID_LOCALE_CLASS; } }); @@ -119,14 +116,13 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { // Case 4: java.lang.System.log?() METHOD_REPLACERS.add(new MethodReplacer() { @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return JAVA_LANG_SYSTEM.equals(owner) && name.length() == 4 && name.startsWith("log"); } @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); assert mi.desc.equals("(Ljava/lang/String;Ljava/lang/Throwable;)V") || mi.desc.equals("(Ljava/lang/String;)V"); mi.name = "log"; @@ -142,7 +138,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { private final String LINKED_HASH_MAP = Type.getInternalName(LinkedHashMap.class); @Override - public boolean isNeeded(String owner, String name, String desc) { + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { return LINKED_HASH_MAP.equals(owner) && "eldest".equals(name) && VOID_TO_MAP_ENTRY.equals(desc); @@ -150,26 +146,64 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void replace(MethodInformation mi) { - assert isNeeded(mi.owner, mi.name, mi.desc); mi.opcode = Opcodes.INVOKESTATIC; mi.owner = Type.getInternalName(LinkedHashMap_Delegate.class); mi.desc = Type.getMethodDescriptor( Type.getType(Map.Entry.class), Type.getType(LinkedHashMap.class)); } }); + + // Case 6: android.content.Context.getClassLoader() in LayoutInflater + METHOD_REPLACERS.add(new MethodReplacer() { + // When LayoutInflater asks for a class loader, we must return the class loader that + // cannot return app's custom views/classes. This is so that in case of any failure + // or exception when instantiating the views, the IDE can replace it with a mock view + // and have proper error handling. However, if a custom view asks for the class + // loader, we must return a class loader that can find app's custom views as well. + // Thus, we rewrite the call to get class loader in LayoutInflater to + // getFrameworkClassLoader and inject a new method in Context. This leaves the normal + // method: Context.getClassLoader() free to be used by the apps. + private final String VOID_TO_CLASS_LOADER = + Type.getMethodDescriptor(Type.getType(ClassLoader.class)); + + @Override + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { + return owner.equals("android/content/Context") && + sourceClass.equals("android/view/LayoutInflater") && + name.equals("getClassLoader") && + desc.equals(VOID_TO_CLASS_LOADER); + } + + @Override + public void replace(MethodInformation mi) { + mi.name = "getFrameworkClassLoader"; + } + }); } - public static boolean isReplacementNeeded(String owner, String name, String desc) { + /** + * If a method some.package.Class.Method(args) is called from some.other.Class, + * @param owner some/package/Class + * @param name Method + * @param desc (args)returnType + * @param sourceClass some/other/Class + * @return if the method invocation needs to be replaced by some other class. + */ + public static boolean isReplacementNeeded(String owner, String name, String desc, + String sourceClass) { for (MethodReplacer replacer : METHOD_REPLACERS) { - if (replacer.isNeeded(owner, name, desc)) { + if (replacer.isNeeded(owner, name, desc, sourceClass)) { return true; } } return false; } - public ReplaceMethodCallsAdapter(ClassVisitor cv) { + private final String mOriginalClassName; + + public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) { super(Opcodes.ASM4, cv); + mOriginalClassName = originalClassName; } @Override @@ -187,7 +221,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { for (MethodReplacer replacer : METHOD_REPLACERS) { - if (replacer.isNeeded(owner, name, desc)) { + if (replacer.isNeeded(owner, name, desc, mOriginalClassName)) { MethodInformation mi = new MethodInformation(opcode, owner, name, desc); replacer.replace(mi); opcode = mi.opcode; @@ -216,13 +250,12 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } private interface MethodReplacer { - public boolean isNeeded(String owner, String name, String desc); + boolean isNeeded(String owner, String name, String desc, String sourceClass); /** * Updates the MethodInformation with the new values of the method attributes - * opcode, owner, name and desc. - * */ - public void replace(MethodInformation mi); + void replace(MethodInformation mi); } } diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java index cf91386..2c21470 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -19,7 +19,9 @@ package com.android.tools.layoutlib.create; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.junit.After; @@ -32,13 +34,17 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -130,6 +136,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { + return new HashMap<String, InjectMethodRunnable>(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -200,6 +211,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { + return new HashMap<String, InjectMethodRunnable>(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -278,6 +294,11 @@ public class AsmGeneratorTest { // methods deleted from their return type. return new String[0]; } + + @Override + public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { + return new HashMap<String, InjectMethodRunnable>(0); + } }; AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); @@ -303,6 +324,118 @@ public class AsmGeneratorTest { filesFound.keySet().toArray()); } + @Test + public void testMethodInjection() throws IOException, LogAbortException, + ClassNotFoundException, IllegalAccessException, InstantiationException, + NoSuchMethodException, InvocationTargetException { + ICreateInfo ci = new ICreateInfo() { + @Override + public Class<?>[] getInjectedClasses() { + return new Class<?>[0]; + } + + @Override + public String[] getDelegateMethods() { + return new String[0]; + } + + @Override + public String[] getDelegateClassNatives() { + return new String[0]; + } + + @Override + public String[] getOverriddenMethods() { + // methods to force override + return new String[0]; + } + + @Override + public String[] getRenamedClasses() { + // classes to rename (so that we can replace them) + return new String[0]; + } + + @Override + public String[] getJavaPkgClasses() { + // classes to refactor (so that we can replace them) + return new String[0]; + } + + @Override + public Set<String> getExcludedClasses() { + return new HashSet<String>(0); + } + + @Override + public String[] getDeleteReturns() { + // methods deleted from their return type. + return new String[0]; + } + + @Override + public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { + HashMap<String, InjectMethodRunnable> map = + new HashMap<String, InjectMethodRunnable>(1); + map.put("mock_android.util.EmptyArray", + InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); + return map; + } + }; + + AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); + AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, + null, // derived from + new String[] { // include classes + "**" + }, + ci.getExcludedClasses(), + new String[] { /* include files */ + "mock_android/data/data*" + }); + aa.analyze(); + agen.generate(); + Map<String, ClassReader> output = new TreeMap<String, ClassReader>(); + Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + parseZip(mOsDestJar, output, filesFound); + final String modifiedClass = "mock_android.util.EmptyArray"; + final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class"); + ZipFile zipFile = new ZipFile(mOsDestJar); + ZipEntry entry = zipFile.getEntry(modifiedClassPath); + assertNotNull(entry); + final byte[] bytes; + final InputStream inputStream = zipFile.getInputStream(entry); + try { + bytes = getByteArray(inputStream); + } finally { + inputStream.close(); + } + ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + if (name.equals(modifiedClass)) { + return defineClass(null, bytes, 0, bytes.length); + } + throw new ClassNotFoundException(name + " not found."); + } + }; + Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass); + Object emptyArrayInstance = emptyArrayClass.newInstance(); + Method method = emptyArrayClass.getMethod("getFrameworkClassLoader"); + Object cl = method.invoke(emptyArrayInstance); + assertEquals(classLoader, cl); + } + + private static byte[] getByteArray(InputStream stream) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = stream.read(buffer, 0, buffer.length)) > -1) { + bos.write(buffer, 0, read); + } + return bos.toByteArray(); + } + private void parseZip(String jarPath, Map<String, ClassReader> classes, Map<String, InputStream> filesFound) throws IOException { diff --git a/tools/layoutlib/rename_font/build_font.py b/tools/layoutlib/rename_font/build_font.py index c747d92..9713a4c 100755 --- a/tools/layoutlib/rename_font/build_font.py +++ b/tools/layoutlib/rename_font/build_font.py @@ -209,15 +209,18 @@ def ends_in_regular(string): def get_version(string): - # The string must begin with 'Version n.nn ' - # to extract n.nn, we return the second entry in the split strings. string = string.strip() - if not string.startswith('Version '): - raise InvalidFontException('mal-formed font version') - return sanitize(string.split()[1]) + # The spec says that the version string should start with "Version ". But not + # all fonts do. So, we return the complete string if it doesn't start with + # the prefix, else we return the rest of the string after sanitizing it. + prefix = 'Version ' + if string.startswith(prefix): + string = string[len(prefix):] + return sanitize(string) def sanitize(string): + """ Remove non-standard chars. """ return re.sub(r'[^\w-]+', '', string) if __name__ == '__main__': diff --git a/tools/layoutlib/rename_font/build_font_single.py b/tools/layoutlib/rename_font/build_font_single.py index 5f7dad9..4245cdc 100755 --- a/tools/layoutlib/rename_font/build_font_single.py +++ b/tools/layoutlib/rename_font/build_font_single.py @@ -193,15 +193,18 @@ def ends_in_regular(string): def get_version(string): - # The string must begin with 'Version n.nn ' - # to extract n.nn, we return the second entry in the split strings. string = string.strip() - if not string.startswith('Version '): - raise InvalidFontException('mal-formed font version') - return sanitize(string.split()[1]) + # The spec says that the version string should start with "Version ". But not + # all fonts do. So, we return the complete string if it doesn't start with + # the prefix, else we return the rest of the string after sanitizing it. + prefix = 'Version ' + if string.startswith(prefix): + string = string[len(prefix):] + return sanitize(string) def sanitize(string): + """ Remove non-standard chars. """ return re.sub(r'[^\w-]+', '', string) if __name__ == '__main__': diff --git a/tools/obbtool/Android.mk b/tools/obbtool/Android.mk index 78d7253..1b1f63e 100644 --- a/tools/obbtool/Android.mk +++ b/tools/obbtool/Android.mk @@ -13,7 +13,7 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES := \ Main.cpp -LOCAL_CFLAGS := -Wall -Werror +LOCAL_CFLAGS := -Wall -Werror -Wno-mismatched-tags #LOCAL_C_INCLUDES += @@ -36,7 +36,7 @@ include $(CLEAR_VARS) LOCAL_MODULE := pbkdf2gen LOCAL_MODULE_TAGS := optional -LOCAL_CFLAGS := -Wall -Werror +LOCAL_CFLAGS := -Wall -Werror -Wno-mismatched-tags LOCAL_SRC_FILES := pbkdf2gen.cpp LOCAL_LDLIBS += -ldl LOCAL_STATIC_LIBRARIES := libcrypto_static diff --git a/tools/orientationplot/README.txt b/tools/orientationplot/README.txt index d53f65e..958207d 100644 --- a/tools/orientationplot/README.txt +++ b/tools/orientationplot/README.txt @@ -9,6 +9,8 @@ PREREQUISITES 2. numpy 3. matplotlib +eg. sudo apt-get install python-numpy python-matplotlib + USAGE ----- diff --git a/tools/orientationplot/orientationplot.py b/tools/orientationplot/orientationplot.py index 6fc3922..77ed074 100755 --- a/tools/orientationplot/orientationplot.py +++ b/tools/orientationplot/orientationplot.py @@ -440,7 +440,7 @@ class Plotter: # Notice print "Window Orientation Listener plotting tool" print "-----------------------------------------\n" -print "Please turn on the Window Orientation Listener logging in Development Settings." +print "Please turn on the Window Orientation Listener logging. See README.txt." # Start adb. print "Starting adb logcat.\n" diff --git a/tools/split-select/Android.mk b/tools/split-select/Android.mk index 013e570..d9ddf08 100644 --- a/tools/split-select/Android.mk +++ b/tools/split-select/Android.mk @@ -52,7 +52,8 @@ hostStaticLibs := \ libutils \ libcutils \ libexpat \ - libziparchive-host + libziparchive-host \ + libbase cFlags := -Wall -Werror diff --git a/tools/split-select/Grouper.cpp b/tools/split-select/Grouper.cpp index 22685cd..55e52fc 100644 --- a/tools/split-select/Grouper.cpp +++ b/tools/split-select/Grouper.cpp @@ -34,7 +34,6 @@ groupByMutualExclusivity(const Vector<SplitDescription>& splits) { // Find mutually exclusive splits and group them. KeyedVector<SplitDescription, SortedVector<SplitDescription> > densityGroups; KeyedVector<SplitDescription, SortedVector<SplitDescription> > abiGroups; - KeyedVector<SplitDescription, SortedVector<SplitDescription> > localeGroups; const size_t splitCount = splits.size(); for (size_t i = 0; i < splitCount; i++) { const SplitDescription& split = splits[i]; @@ -47,10 +46,6 @@ groupByMutualExclusivity(const Vector<SplitDescription>& splits) { SplitDescription key(split); key.abi = abi::Variant_none; appendValue(abiGroups, key, split); - } else if (split.config.locale != 0) { - SplitDescription key(split); - key.config.clearLocale(); - appendValue(localeGroups, key, split); } else { groups.add(); groups.editTop().add(split); @@ -67,10 +62,6 @@ groupByMutualExclusivity(const Vector<SplitDescription>& splits) { groups.add(abiGroups[i]); } - const size_t localeCount = localeGroups.size(); - for (size_t i = 0; i < localeCount; i++) { - groups.add(localeGroups[i]); - } return groups; } diff --git a/tools/split-select/Grouper_test.cpp b/tools/split-select/Grouper_test.cpp index a5f9c5a..7294a86 100644 --- a/tools/split-select/Grouper_test.cpp +++ b/tools/split-select/Grouper_test.cpp @@ -37,6 +37,8 @@ protected: addSplit(splits, "en-rUS-sw300dp-xhdpi"); addSplit(splits, "large"); addSplit(splits, "pl-rPL"); + addSplit(splits, "fr-rCA"); + addSplit(splits, "fr"); addSplit(splits, "xlarge"); addSplit(splits, "en-rUS-sw600dp-xhdpi"); addSplit(splits, "en-rUS-sw300dp-hdpi"); @@ -64,7 +66,7 @@ protected: }; TEST_F(GrouperTest, shouldHaveCorrectNumberOfGroups) { - EXPECT_EQ(12u, mGroups.size()); + EXPECT_EQ(15u, mGroups.size()); } TEST_F(GrouperTest, shouldGroupDensities) { @@ -79,7 +81,10 @@ TEST_F(GrouperTest, shouldGroupAbi) { } TEST_F(GrouperTest, shouldGroupLocale) { - expectHasGroupWithSplits("pl-rPL", "de-rDE"); + expectHasGroupWithSplits("pl-rPL"); + expectHasGroupWithSplits("de-rDE"); + expectHasGroupWithSplits("fr"); + expectHasGroupWithSplits("fr-rCA"); } TEST_F(GrouperTest, shouldGroupEachSplitIntoItsOwnGroup) { |