diff options
Diffstat (limited to 'tools')
56 files changed, 2525 insertions, 992 deletions
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 48cfc4453d8f..2ecf25b751b6 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -15,6 +15,7 @@ // toolSources = [ + "cmd/Command.cpp", "cmd/Compile.cpp", "cmd/Convert.cpp", "cmd/Diff.cpp", @@ -111,6 +112,7 @@ cc_library_host_static { "link/XmlReferenceLinker.cpp", "optimize/MultiApkGenerator.cpp", "optimize/ResourceDeduper.cpp", + "optimize/ResourceFilter.cpp", "optimize/VersionCollapser.cpp", "process/SymbolTable.cpp", "split/TableSplitter.cpp", @@ -123,7 +125,6 @@ cc_library_host_static { "ConfigDescription.cpp", "Debug.cpp", "DominatorTree.cpp", - "Flags.cpp", "java/AnnotationProcessor.cpp", "java/ClassDefinition.cpp", "java/JavaClassGenerator.cpp", diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h index d6f599520d71..75123537116f 100644 --- a/tools/aapt2/AppInfo.h +++ b/tools/aapt2/AppInfo.h @@ -31,9 +31,12 @@ struct AppInfo { // The app's minimum SDK version, if it is defined. Maybe<int> min_sdk_version; - // The app's version code, if it is defined. + // The app's version code (the lower 32 bits of the long version code), if it is defined. Maybe<uint32_t> version_code; + // The app's version code major (the upper 32 bits of the long version code), if it is defined. + Maybe<uint32_t> version_code_major; + // The app's revision code, if it is defined. Maybe<uint32_t> revision_code; diff --git a/tools/aapt2/Flags.h b/tools/aapt2/Flags.h deleted file mode 100644 index 3b3ae710dc7b..000000000000 --- a/tools/aapt2/Flags.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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_FLAGS_H -#define AAPT_FLAGS_H - -#include <functional> -#include <ostream> -#include <string> -#include <unordered_set> -#include <vector> - -#include "androidfw/StringPiece.h" - -#include "util/Maybe.h" - -namespace aapt { - -class Flags { - public: - Flags& RequiredFlag(const android::StringPiece& name, const android::StringPiece& description, - std::string* value); - Flags& RequiredFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::vector<std::string>* value); - Flags& OptionalFlag(const android::StringPiece& name, const android::StringPiece& description, - Maybe<std::string>* value); - Flags& OptionalFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::vector<std::string>* value); - Flags& OptionalFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::unordered_set<std::string>* value); - Flags& OptionalSwitch(const android::StringPiece& name, const android::StringPiece& description, - bool* value); - - void Usage(const android::StringPiece& command, std::ostream* out); - - bool Parse(const android::StringPiece& command, const std::vector<android::StringPiece>& args, - std::ostream* outError); - - const std::vector<std::string>& GetArgs(); - - private: - struct Flag { - std::string name; - std::string description; - std::function<bool(const android::StringPiece& value)> action; - bool required; - size_t num_args; - - bool parsed; - }; - - std::vector<Flag> flags_; - std::vector<std::string> args_; -}; - -} // namespace aapt - -#endif // AAPT_FLAGS_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 808b29cfd844..23903c9e05f3 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -29,6 +29,13 @@ #include "androidfw/StringPiece.h" #include "Diagnostics.h" +#include "cmd/Command.h" +#include "cmd/Compile.h" +#include "cmd/Convert.h" +#include "cmd/Diff.h" +#include "cmd/Dump.h" +#include "cmd/Link.h" +#include "cmd/Optimize.h" #include "util/Files.h" #include "util/Util.h" @@ -43,114 +50,121 @@ static const char* sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. static const char* sMinorVersion = "19"; -static void PrintVersion() { - std::cerr << StringPrintf("Android Asset Packaging Tool (aapt) %s:%s", sMajorVersion, - sMinorVersion) - << std::endl; -} - -static void PrintUsage() { - std::cerr << "\nusage: aapt2 [compile|link|dump|diff|optimize|convert|version] ..." << std::endl; -} +/** Prints the version information of AAPT2. */ +class VersionCommand : public Command { + public: + explicit VersionCommand() : Command("version") { + SetDescription("Prints the version of aapt."); + } -extern int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics); -extern int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics); -extern int Dump(const std::vector<StringPiece>& args); -extern int Diff(const std::vector<StringPiece>& args); -extern int Optimize(const std::vector<StringPiece>& args); -extern int Convert(const std::vector<StringPiece>& args); - -static int ExecuteCommand(const StringPiece& command, const std::vector<StringPiece>& args, - IDiagnostics* diagnostics) { - if (command == "compile" || command == "c") { - return Compile(args, diagnostics); - } else if (command == "link" || command == "l") { - return Link(args, diagnostics); - } else if (command == "dump" || command == "d") { - return Dump(args); - } else if (command == "diff") { - return Diff(args); - } else if (command == "optimize") { - return Optimize(args); - } else if (command == "convert") { - return Convert(args); - } else if (command == "version") { - PrintVersion(); + int Action(const std::vector<std::string>& /* args */) override { + std::cerr << StringPrintf("Android Asset Packaging Tool (aapt) %s:%s", sMajorVersion, + sMinorVersion) + << std::endl; return 0; } - diagnostics->Error(DiagMessage() << "unknown command '" << command << "'"); - return -1; -} +}; + +/** The main entry point of AAPT. */ +class MainCommand : public Command { + public: + explicit MainCommand(IDiagnostics* diagnostics) : Command("aapt2"), diagnostics_(diagnostics) { + AddOptionalSubcommand(util::make_unique<CompileCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<LinkCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<DumpCommand>()); + AddOptionalSubcommand(util::make_unique<DiffCommand>()); + AddOptionalSubcommand(util::make_unique<OptimizeCommand>()); + AddOptionalSubcommand(util::make_unique<ConvertCommand>()); + AddOptionalSubcommand(util::make_unique<VersionCommand>()); + } -static void RunDaemon(IDiagnostics* diagnostics) { - std::cout << "Ready" << std::endl; - - // Run in daemon mode. The first line of input is the command. This can be 'quit' which ends - // the daemon mode. Each subsequent line is a single parameter to the command. The end of a - // invocation is signaled by providing an empty line. At any point, an EOF signal or the - // command 'quit' will end the daemon mode. - while (true) { - std::vector<std::string> raw_args; - for (std::string line; std::getline(std::cin, line) && !line.empty();) { - raw_args.push_back(line); + int Action(const std::vector<std::string>& args) override { + if (args.size() == 0) { + diagnostics_->Error(DiagMessage() << "no subcommand specified"); + } else { + diagnostics_->Error(DiagMessage() << "unknown subcommand '" << args[0] << "'"); } - if (!std::cin) { - break; - } + Usage(&std::cerr); + return -1; + } - // An empty command does nothing. - if (raw_args.empty()) { - continue; - } + private: + IDiagnostics* diagnostics_; +}; - if (raw_args[0] == "quit") { - break; - } +/* + * Run in daemon mode. The first line of input is the command. This can be 'quit' which ends + * the daemon mode. Each subsequent line is a single parameter to the command. The end of a + * invocation is signaled by providing an empty line. At any point, an EOF signal or the + * command 'quit' will end the daemon mode. + */ +class DaemonCommand : public Command { + public: + explicit DaemonCommand(IDiagnostics* diagnostics) : Command("daemon", "m"), + diagnostics_(diagnostics) { + SetDescription("Runs aapt in daemon mode. Each subsequent line is a single parameter to the\n" + "command. The end of an invocation is signaled by providing an empty line."); + } - std::vector<StringPiece> args; - args.insert(args.end(), ++raw_args.begin(), raw_args.end()); - int ret = ExecuteCommand(raw_args[0], args, diagnostics); - if (ret != 0) { - std::cerr << "Error" << std::endl; + int Action(const std::vector<std::string>& /* args */) override { + std::cout << "Ready" << std::endl; + + while (true) { + std::vector<std::string> raw_args; + for (std::string line; std::getline(std::cin, line) && !line.empty();) { + raw_args.push_back(line); + } + + if (!std::cin) { + break; + } + + // An empty command does nothing. + if (raw_args.empty()) { + continue; + } + + // End the dameon + if (raw_args[0] == "quit") { + break; + } + + std::vector<StringPiece> args; + args.insert(args.end(), raw_args.begin(), raw_args.end()); + if (MainCommand(diagnostics_).Execute(args, &std::cerr) != 0) { + std::cerr << "Error" << std::endl; + } + std::cerr << "Done" << std::endl; } - std::cerr << "Done" << std::endl; + std::cout << "Exiting daemon" << std::endl; + + return 0; } - std::cout << "Exiting daemon" << std::endl; -} + + private: + IDiagnostics* diagnostics_; +}; } // namespace aapt int MainImpl(int argc, char** argv) { - if (argc < 2) { - std::cerr << "no command specified\n"; - aapt::PrintUsage(); + if (argc < 1) { return -1; } - argv += 1; - argc -= 1; - - aapt::StdErrDiagnostics diagnostics; - // Collect the arguments starting after the program name and command name. std::vector<StringPiece> args; for (int i = 1; i < argc; i++) { args.push_back(argv[i]); } - const StringPiece command(argv[0]); - if (command != "daemon" && command != "m") { - // Single execution. - const int result = aapt::ExecuteCommand(command, args, &diagnostics); - if (result < 0) { - aapt::PrintUsage(); - } - return result; - } + // Add the daemon subcommand here so it cannot be called while executing the daemon + aapt::StdErrDiagnostics diagnostics; + auto main_command = new aapt::MainCommand(&diagnostics); + main_command->AddOptionalSubcommand(aapt::util::make_unique<aapt::DaemonCommand>(&diagnostics)); - aapt::RunDaemon(&diagnostics); - return 0; + return main_command->Execute(args, &std::cerr); } int main(int argc, char** argv) { diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 7f48544c0ae4..8719a233d774 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -208,6 +208,15 @@ class SegmentNode : public Node { } }; +// A chunk of text in the XML string within a CDATA tags. +class CdataSegmentNode : public SegmentNode { + public: + + void Build(StringBuilder* builder) const override { + builder->AppendText(data, /* preserve_spaces */ true); + } +}; + // A tag that will be encoded into the final flattened string. Tags like <b> or <i>. class SpanNode : public Node { public: @@ -244,6 +253,7 @@ bool ResourceParser::FlattenXmlSubtree( std::vector<Node*> node_stack; node_stack.push_back(&root); + bool cdata_block = false; bool saw_span_node = false; SegmentNode* first_segment = nullptr; SegmentNode* last_segment = nullptr; @@ -253,11 +263,15 @@ bool ResourceParser::FlattenXmlSubtree( const xml::XmlPullParser::Event event = parser->event(); // First take care of any SegmentNodes that should be created. - if (event == xml::XmlPullParser::Event::kStartElement || - event == xml::XmlPullParser::Event::kEndElement) { + if (event == xml::XmlPullParser::Event::kStartElement + || event == xml::XmlPullParser::Event::kEndElement + || event == xml::XmlPullParser::Event::kCdataStart + || event == xml::XmlPullParser::Event::kCdataEnd) { if (!current_text.empty()) { - std::unique_ptr<SegmentNode> segment_node = util::make_unique<SegmentNode>(); + std::unique_ptr<SegmentNode> segment_node = (cdata_block) + ? util::make_unique<CdataSegmentNode>() : util::make_unique<SegmentNode>(); segment_node->data = std::move(current_text); + last_segment = node_stack.back()->AddChild(std::move(segment_node)); if (first_segment == nullptr) { first_segment = last_segment; @@ -333,6 +347,16 @@ bool ResourceParser::FlattenXmlSubtree( } } break; + case xml::XmlPullParser::Event::kCdataStart: { + cdata_block = true; + break; + } + + case xml::XmlPullParser::Event::kCdataEnd: { + cdata_block = false; + break; + } + default: // ignore. break; @@ -447,6 +471,9 @@ bool ResourceParser::ParseResources(xml::XmlPullParser* parser) { parsed_resource.config = config_; parsed_resource.source = source_.WithLine(parser->line_number()); parsed_resource.comment = std::move(comment); + if (options_.visibility) { + parsed_resource.visibility_level = options_.visibility.value(); + } // Extract the product name if it exists. if (Maybe<StringPiece> maybe_product = xml::FindNonEmptyAttribute(parser, "product")) { @@ -774,7 +801,8 @@ std::unique_ptr<Item> ResourceParser::ParseXml(xml::XmlPullParser* parser, if (allow_raw_value) { // We can't parse this so return a RawString if we are allowed. return util::make_unique<RawString>( - table_->string_pool.MakeRef(raw_value, StringPool::Context(config_))); + table_->string_pool.MakeRef(util::TrimWhitespace(raw_value), + StringPool::Context(config_))); } return {}; } @@ -836,6 +864,12 @@ bool ResourceParser::ParseString(xml::XmlPullParser* parser, } bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<public> tag not allowed with --visibility flag"); + return false; + } + if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config << "' for <public> tag"); @@ -878,6 +912,12 @@ bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out } bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<public-group> tag not allowed with --visibility flag"); + return false; + } + if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config @@ -999,6 +1039,11 @@ bool ResourceParser::ParseSymbolImpl(xml::XmlPullParser* parser, } bool ResourceParser::ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<java-symbol> and <symbol> tags not allowed with --visibility flag"); + return false; + } if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config << "' for <" @@ -1070,6 +1115,9 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource child_resource.name.entry = maybe_name.value().to_string(); child_resource.source = item_source; child_resource.overlayable = true; + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } out_resource->child_resources.push_back(std::move(child_resource)); xml::XmlPullParser::SkipCurrentElement(parser); @@ -1212,6 +1260,9 @@ bool ResourceParser::ParseAttrImpl(xml::XmlPullParser* parser, child_resource.name = symbol.symbol.name.value(); child_resource.source = item_source; child_resource.value = util::make_unique<Id>(); + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } out_resource->child_resources.push_back(std::move(child_resource)); symbol.symbol.SetComment(std::move(comment)); @@ -1589,6 +1640,9 @@ bool ResourceParser::ParseDeclareStyleable(xml::XmlPullParser* parser, child_resource.name = child_ref.name.value(); child_resource.source = item_source; child_resource.comment = std::move(comment); + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } if (!ParseAttrImpl(parser, &child_resource, true)) { error = true; diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index fb9dbd0cd0fd..68130c2512d3 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -45,6 +45,10 @@ struct ResourceParserOptions { * warnings. */ bool error_on_positional_arguments = true; + + // If visibility was forced, we need to use it when creating a new resource and also error if we + // try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. + Maybe<Visibility::Level> visibility; }; /* diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 41b4041efb7a..ee496d501113 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -497,6 +497,24 @@ TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedItems) { EXPECT_THAT(style->entries[0].key.name, Eq(make_value(test::ParseNameOrDie("android:attr/bar")))); } +TEST_F(ResourceParserTest, ParseStyleWithRawStringItem) { + std::string input = R"( + <style name="foo"> + <item name="bar"> + com.helloworld.AppClass + </item> + </style>)"; + ASSERT_TRUE(TestParse(input)); + + Style* style = test::GetValue<Style>(&table_, "style/foo"); + ASSERT_THAT(style, NotNull()); + EXPECT_THAT(style->entries[0].value, NotNull()); + RawString* value = ValueCast<RawString>(style->entries[0].value.get()); + EXPECT_THAT(value, NotNull()); + EXPECT_THAT(*value->value, StrEq(R"(com.helloworld.AppClass)")); +} + + TEST_F(ResourceParserTest, ParseStyleWithInferredParent) { ASSERT_TRUE(TestParse(R"(<style name="foo.bar"/>)")); @@ -971,4 +989,40 @@ TEST_F(ResourceParserTest, ParseIdItem) { ASSERT_FALSE(TestParse(input)); } +TEST_F(ResourceParserTest, ParseCData) { + std::string input = R"( + <string name="foo"><![CDATA[some text and ' apostrophe]]></string>)"; + + ASSERT_TRUE(TestParse(input)); + String* output = test::GetValue<String>(&table_, "string/foo"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq("some text and ' apostrophe")); + + // Double quotes should not change the state of whitespace processing + input = R"(<string name="foo2">Hello<![CDATA[ "</string>' ]]> World</string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo2"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string("Hello \"</string>' World").data())); + + // Cdata blocks should not have their whitespace trimmed + input = R"(<string name="foo3"> <![CDATA[ text ]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo3"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string(" text ").data())); + + input = R"(<string name="foo4"> <![CDATA[]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo4"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string("").data())); + + input = R"(<string name="foo5"> <![CDATA[ ]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo5"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string(" ").data())); +} + } // namespace aapt diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index 560077cc322c..c48765b7b947 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -797,16 +797,20 @@ StringBuilder::StringBuilder(bool preserve_spaces) : preserve_spaces_(preserve_spaces), quote_(preserve_spaces) { } -StringBuilder& StringBuilder::AppendText(const std::string& text) { +StringBuilder& StringBuilder::AppendText(const std::string& text, bool preserve_spaces) { if (!error_.empty()) { return *this; } + // Enable preserving spaces if it is enabled for this append or the StringBuilder was constructed + // to preserve spaces + preserve_spaces = (preserve_spaces) ? preserve_spaces : preserve_spaces_; + const size_t previous_len = xml_string_.text.size(); Utf8Iterator iter(text); while (iter.HasNext()) { char32_t codepoint = iter.Next(); - if (!quote_ && iswspace(codepoint)) { + if (!preserve_spaces && !quote_ && iswspace(codepoint)) { if (!last_codepoint_was_space_) { // Emit a space if it's the first. xml_string_.text += ' '; @@ -827,7 +831,6 @@ StringBuilder& StringBuilder::AppendText(const std::string& text) { case U't': xml_string_.text += '\t'; break; - case U'n': xml_string_.text += '\n'; break; @@ -855,12 +858,12 @@ StringBuilder& StringBuilder::AppendText(const std::string& text) { break; } } - } else if (!preserve_spaces_ && codepoint == U'"') { + } else if (!preserve_spaces && codepoint == U'"') { // Only toggle the quote state when we are not preserving spaces. quote_ = !quote_; - } else if (!quote_ && codepoint == U'\'') { - // This should be escaped. + } else if (!preserve_spaces && !quote_ && codepoint == U'\'') { + // This should be escaped when we are not preserving spaces error_ = StringPrintf("unescaped apostrophe in string\n\"%s\"", text.c_str()); return *this; diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h index 7af2fe06b908..410ef28ce78a 100644 --- a/tools/aapt2/ResourceUtils.h +++ b/tools/aapt2/ResourceUtils.h @@ -267,8 +267,10 @@ class StringBuilder { // single quotations can be used without escaping them. explicit StringBuilder(bool preserve_spaces = false); - // Appends a chunk of text. - StringBuilder& AppendText(const std::string& text); + // Appends a chunk of text. If preserve_spaces is true, whitespace removal is not performed, and + // single quotations can be used without escaping them for this append. Otherwise, the + // StringBuilder will behave as it was constructed. + StringBuilder& AppendText(const std::string& text, bool preserve_spaces = false); // Starts a Span (tag) with the given name. The name is expected to be of the form: // "tag_name;attr1=value;attr2=value;" diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp index 11f3fa3bc6cd..5ce464074335 100644 --- a/tools/aapt2/ResourceUtils_test.cpp +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -254,6 +254,29 @@ TEST(ResourceUtilsTest, StringBuilderUnicodeCodes) { TEST(ResourceUtilsTest, StringBuilderPreserveSpaces) { EXPECT_THAT(ResourceUtils::StringBuilder(true /*preserve_spaces*/).AppendText("\"").to_string(), Eq("\"")); + + // Single quotes should be able to be used without escaping them when preserving spaces and the + // spaces should not be trimmed + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys ") + .AppendText(" 'this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow ") + .to_string(), + Eq(" hey guys 'this is so cool' wow ")); + + // Reading a double quote while preserving spaces should not change the quote state + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys ") + .AppendText(" \"this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow ") + .to_string(), + Eq(" hey guys \"this is so cool' wow ")); + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys\" ") + .AppendText(" \"this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow \" ") + .to_string(), + Eq(" hey guys \"this is so cool' wow ")); } } // namespace aapt diff --git a/tools/aapt2/StringPool.cpp b/tools/aapt2/StringPool.cpp index b37e1fbd9693..8eabd3225d87 100644 --- a/tools/aapt2/StringPool.cpp +++ b/tools/aapt2/StringPool.cpp @@ -367,7 +367,7 @@ const std::string kStringTooLarge = "STRING_TOO_LARGE"; static bool EncodeString(const std::string& str, const bool utf8, BigBuffer* out, IDiagnostics* diag) { if (utf8) { - const std::string& encoded = str; + const std::string& encoded = util::Utf8ToModifiedUtf8(str); const ssize_t utf16_length = utf8_to_utf16_length( reinterpret_cast<const uint8_t*>(encoded.data()), encoded.size()); CHECK(utf16_length >= 0); diff --git a/tools/aapt2/StringPool_test.cpp b/tools/aapt2/StringPool_test.cpp index 4b3afe2bb962..0778564ee079 100644 --- a/tools/aapt2/StringPool_test.cpp +++ b/tools/aapt2/StringPool_test.cpp @@ -303,6 +303,25 @@ TEST(StringPoolTest, Flatten) { } } +TEST(StringPoolTest, FlattenModifiedUTF8) { + using namespace android; // For NO_ERROR on Windows. + StdErrDiagnostics diag; + StringPool pool; + StringPool::Ref ref_a = pool.MakeRef("\xF0\x90\x90\x80"); // 𐐀 (U+10400) + StringPool::Ref ref_b = pool.MakeRef("foo \xF0\x90\x90\xB7 bar"); // 𐐷 (U+10437) + StringPool::Ref ref_c = pool.MakeRef("\xF0\x90\x90\x80\xF0\x90\x90\xB7"); + + BigBuffer buffer(1024); + StringPool::FlattenUtf8(&buffer, pool, &diag); + std::unique_ptr<uint8_t[]> data = util::Copy(buffer); + + // Check that the 4 byte utf-8 codepoint is encoded using 2 3 byte surrogate pairs + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + EXPECT_THAT(util::GetString(test, 0), Eq("\xED\xA0\x81\xED\xB0\x80")); + EXPECT_THAT(util::GetString(test, 1), Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar")); + EXPECT_THAT(util::GetString(test, 2), Eq("\xED\xA0\x81\xED\xB0\x80\xED\xA0\x81\xED\xB0\xB7")); +} TEST(StringPoolTest, MaxEncodingLength) { StdErrDiagnostics diag; diff --git a/tools/aapt2/Flags.cpp b/tools/aapt2/cmd/Command.cpp index 84977ab424cc..09411b95bcfe 100644 --- a/tools/aapt2/Flags.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "Flags.h" +#include "Command.h" #include <iomanip> #include <iostream> @@ -29,87 +29,113 @@ using android::StringPiece; namespace aapt { -Flags& Flags::RequiredFlag(const StringPiece& name, - const StringPiece& description, std::string* value) { +void Command::AddRequiredFlag(const StringPiece& name, + const StringPiece& description, std::string* value) { auto func = [value](const StringPiece& arg) -> bool { *value = arg.to_string(); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, true, 1, false}); - return *this; } -Flags& Flags::RequiredFlagList(const StringPiece& name, - const StringPiece& description, - std::vector<std::string>* value) { +void Command::AddRequiredFlagList(const StringPiece& name, + const StringPiece& description, + std::vector<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->push_back(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, true, 1, false}); - return *this; } -Flags& Flags::OptionalFlag(const StringPiece& name, - const StringPiece& description, - Maybe<std::string>* value) { +void Command::AddOptionalFlag(const StringPiece& name, + const StringPiece& description, + Maybe<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { *value = arg.to_string(); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalFlagList(const StringPiece& name, - const StringPiece& description, - std::vector<std::string>* value) { +void Command::AddOptionalFlagList(const StringPiece& name, + const StringPiece& description, + std::vector<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->push_back(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalFlagList(const StringPiece& name, - const StringPiece& description, - std::unordered_set<std::string>* value) { +void Command::AddOptionalFlagList(const StringPiece& name, + const StringPiece& description, + std::unordered_set<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->insert(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalSwitch(const StringPiece& name, - const StringPiece& description, bool* value) { +void Command::AddOptionalSwitch(const StringPiece& name, + const StringPiece& description, bool* value) { auto func = [value](const StringPiece& arg) -> bool { *value = true; return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 0, false}); - return *this; } -void Flags::Usage(const StringPiece& command, std::ostream* out) { +void Command::AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand) { + subcommand->fullname_ = name_ + " " + subcommand->name_; + subcommands_.push_back(std::move(subcommand)); +} + +void Command::SetDescription(const android::StringPiece& description) { + description_ = description.to_string(); +} + +void Command::Usage(std::ostream* out) { constexpr size_t kWidth = 50; - *out << command << " [options]"; + *out << fullname_; + + if (!subcommands_.empty()) { + *out << " [subcommand]"; + } + + *out << " [options]"; for (const Flag& flag : flags_) { if (flag.required) { *out << " " << flag.name << " arg"; } } - *out << " files...\n\nOptions:\n"; + *out << " files...\n"; + + if (!subcommands_.empty()) { + *out << "\nSubcommands:\n"; + for (auto& subcommand : subcommands_) { + std::string argline = subcommand->name_; + + // Split the description by newlines and write out the argument (which is + // empty after the first line) followed by the description line. This will make sure + // that multiline descriptions are still right justified and aligned. + for (StringPiece line : util::Tokenize(subcommand->description_, '\n')) { + *out << " " << std::setw(kWidth) << std::left << argline << line << "\n"; + argline = " "; + } + } + } + + *out << "\nOptions:\n"; for (const Flag& flag : flags_) { std::string argline = flag.name; @@ -118,10 +144,8 @@ void Flags::Usage(const StringPiece& command, std::ostream* out) { } // Split the description by newlines and write out the argument (which is - // empty after - // the first line) followed by the description line. This will make sure - // that multiline - // descriptions are still right justified and aligned. + // empty after the first line) followed by the description line. This will make sure + // that multiline descriptions are still right justified and aligned. for (StringPiece line : util::Tokenize(flag.description, '\n')) { *out << " " << std::setw(kWidth) << std::left << argline << line << "\n"; argline = " "; @@ -132,19 +156,29 @@ void Flags::Usage(const StringPiece& command, std::ostream* out) { out->flush(); } -bool Flags::Parse(const StringPiece& command, - const std::vector<StringPiece>& args, - std::ostream* out_error) { +int Command::Execute(const std::vector<android::StringPiece>& args, std::ostream* out_error) { + std::vector<std::string> file_args; + for (size_t i = 0; i < args.size(); i++) { StringPiece arg = args[i]; if (*(arg.data()) != '-') { - args_.push_back(arg.to_string()); + // Continue parsing as the sub command if the first argument matches one of the subcommands + if (i == 0) { + for (auto& subcommand : subcommands_) { + if (arg == subcommand->name_ || arg==subcommand->short_name_) { + return subcommand->Execute( + std::vector<android::StringPiece>(args.begin() + 1, args.end()), out_error); + } + } + } + + file_args.push_back(arg.to_string()); continue; } if (arg == "-h" || arg == "--help") { - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } bool match = false; @@ -154,7 +188,7 @@ bool Flags::Parse(const StringPiece& command, i++; if (i >= args.size()) { *out_error << flag.name << " missing argument.\n\n"; - Usage(command, out_error); + Usage(out_error); return false; } flag.action(args[i]); @@ -169,21 +203,20 @@ bool Flags::Parse(const StringPiece& command, if (!match) { *out_error << "unknown option '" << arg << "'.\n\n"; - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } } for (const Flag& flag : flags_) { if (flag.required && !flag.parsed) { *out_error << "missing required flag " << flag.name << "\n\n"; - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } } - return true; -} -const std::vector<std::string>& Flags::GetArgs() { return args_; } + return Action(file_args); +} } // namespace aapt diff --git a/tools/aapt2/cmd/Command.h b/tools/aapt2/cmd/Command.h new file mode 100644 index 000000000000..71dc6fe48da1 --- /dev/null +++ b/tools/aapt2/cmd/Command.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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_COMMAND_H +#define AAPT_COMMAND_H + +#include <functional> +#include <ostream> +#include <string> +#include <unordered_set> +#include <vector> + +#include "androidfw/StringPiece.h" + +#include "util/Maybe.h" + +namespace aapt { + +class Command { + public: + explicit Command(const android::StringPiece& name) : name_(name.to_string()), + fullname_(name.to_string()) { } + explicit Command(const android::StringPiece& name, const android::StringPiece& short_name) + : name_(name.to_string()), short_name_(short_name.to_string()), fullname_(name.to_string()) {} + virtual ~Command() = default; + + void AddRequiredFlag(const android::StringPiece& name, const android::StringPiece& description, + std::string* value); + void AddRequiredFlagList(const android::StringPiece& name, const android::StringPiece& + description, std::vector<std::string>* value); + void AddOptionalFlag(const android::StringPiece& name, const android::StringPiece& description, + Maybe<std::string>* value); + void AddOptionalFlagList(const android::StringPiece& name, + const android::StringPiece& description, std::vector<std::string>* value); + void AddOptionalFlagList(const android::StringPiece& name, + const android::StringPiece& description, std::unordered_set<std::string>* value); + void AddOptionalSwitch(const android::StringPiece& name, const android::StringPiece& description, + bool* value); + void AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand); + + void SetDescription(const android::StringPiece& name); + + /** Prints the help menu of the command. */ + void Usage(std::ostream* out); + + /** + * Parses the command line arguments, sets the flag variable values, and runs the action of + * the command. If the arguments fail to parse to the command and its subcommands, then the action + * will not be run and the usage will be printed instead. + **/ + int Execute(const std::vector<android::StringPiece>& args, std::ostream* outError); + + /** The action to preform when the command is executed. */ + virtual int Action(const std::vector<std::string>& args) = 0; + + private: + struct Flag { + std::string name; + std::string description; + std::function<bool(const android::StringPiece& value)> action; + bool required; + size_t num_args; + + bool parsed; + }; + + std::string description_; + std::string name_; + std::string short_name_; + std::string fullname_; + std::vector<Flag> flags_; + std::vector<std::unique_ptr<Command>> subcommands_; +}; + +} // namespace aapt + +#endif // AAPT_COMMAND_H diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index ab8a4b77a89d..36b5578d1deb 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -14,8 +14,9 @@ * limitations under the License. */ -#include <dirent.h> +#include "Compile.h" +#include <dirent.h> #include <string> #include "android-base/errors.h" @@ -27,9 +28,9 @@ #include "ConfigDescription.h" #include "Diagnostics.h" -#include "Flags.h" #include "ResourceParser.h" #include "ResourceTable.h" +#include "cmd/Util.h" #include "compile/IdAssigner.h" #include "compile/InlineXmlFormatParser.h" #include "compile/Png.h" @@ -120,16 +121,6 @@ static Maybe<ResourcePathData> ExtractResourcePathData(const std::string& path, extension.to_string(), config_str.to_string(), config}; } -struct CompileOptions { - std::string output_path; - Maybe<std::string> res_dir; - Maybe<std::string> generate_text_symbols_path; - bool pseudolocalize = false; - bool no_png_crunch = false; - bool legacy_mode = false; - bool verbose = false; -}; - static std::string BuildIntermediateContainerFilename(const ResourcePathData& data) { std::stringstream name; name << data.resource_dir; @@ -226,6 +217,10 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // If the filename includes donottranslate, then the default translatable is false. parser_options.translatable = path_data.name.find("donottranslate") == std::string::npos; + // If visibility was forced, we need to use it when creating a new resource and also error if + // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. + parser_options.visibility = options.visibility; + ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config, parser_options); if (!res_parser.Parse(&xml_parser)) { @@ -319,8 +314,16 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, if (!entry->values.empty()) { auto styleable = static_cast<const Styleable*>(entry->values.front()->value.get()); for (const auto& attr : styleable->entries) { + // The visibility of the children under the styleable does not matter as they are + // nested under their parent and use its visibility. r_txt_printer.Print("default int styleable "); r_txt_printer.Print(entry->name); + // If the package name is present, also include it in the mangled name (e.g. + // "android") + if (!attr.name.value().package.empty()) { + r_txt_printer.Print("_"); + r_txt_printer.Print(MakePackageSafeName(attr.name.value().package)); + } r_txt_printer.Print("_"); r_txt_printer.Println(attr.name.value().entry); } @@ -694,56 +697,47 @@ class CompileContext : public IAaptContext { bool verbose_ = false; }; -// Entry point for compilation phase. Parses arguments and dispatches to the correct steps. -int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { - CompileContext context(diagnostics); - CompileOptions options; - - bool verbose = false; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path", &options.output_path) - .OptionalFlag("--dir", "Directory to scan for resources", &options.res_dir) - .OptionalFlag("--output-text-symbols", - "Generates a text file containing the resource symbols in the\n" - "specified file", - &options.generate_text_symbols_path) - .OptionalSwitch("--pseudo-localize", - "Generate resources for pseudo-locales " - "(en-XA and ar-XB)", - &options.pseudolocalize) - .OptionalSwitch("--no-crunch", "Disables PNG processing", &options.no_png_crunch) - .OptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", - &options.legacy_mode) - .OptionalSwitch("-v", "Enables verbose logging", &verbose); - if (!flags.Parse("aapt2 compile", args, &std::cerr)) { - return 1; +int CompileCommand::Action(const std::vector<std::string>& args) { + CompileContext context(diagnostic_); + context.SetVerbose(options_.verbose); + + if (visibility_) { + if (visibility_.value() == "public") { + options_.visibility = Visibility::Level::kPublic; + } else if (visibility_.value() == "private") { + options_.visibility = Visibility::Level::kPrivate; + } else if (visibility_.value() == "default") { + options_.visibility = Visibility::Level::kUndefined; + } else { + context.GetDiagnostics()->Error( + DiagMessage() << "Unrecognized visibility level passes to --visibility: '" + << visibility_.value() << "'. Accepted levels: public, private, default"); + return 1; + } } - context.SetVerbose(verbose); - std::unique_ptr<IArchiveWriter> archive_writer; std::vector<ResourcePathData> input_data; - if (options.res_dir) { - if (!flags.GetArgs().empty()) { + if (options_.res_dir) { + if (!args.empty()) { // Can't have both files and a resource directory. context.GetDiagnostics()->Error(DiagMessage() << "files given but --dir specified"); - flags.Usage("aapt2 compile", &std::cerr); + Usage(&std::cerr); return 1; } - if (!LoadInputFilesFromDir(&context, options, &input_data)) { + if (!LoadInputFilesFromDir(&context, options_, &input_data)) { return 1; } - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options.output_path); + archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } else { - input_data.reserve(flags.GetArgs().size()); + input_data.reserve(args.size()); // Collect data from the path for each input file. - for (const std::string& arg : flags.GetArgs()) { + for (const std::string& arg : args) { std::string error_str; if (Maybe<ResourcePathData> path_data = ExtractResourcePathData(arg, &error_str)) { input_data.push_back(std::move(path_data.value())); @@ -753,7 +747,7 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { } } - archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options.output_path); + archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path); } if (!archive_writer) { @@ -762,7 +756,7 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { bool error = false; for (ResourcePathData& path_data : input_data) { - if (options.verbose) { + if (options_.verbose) { context.GetDiagnostics()->Note(DiagMessage(path_data.source) << "processing"); } @@ -783,21 +777,21 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { if (*type != ResourceType::kRaw) { if (path_data.extension == "xml") { compile_func = &CompileXml; - } else if ((!options.no_png_crunch && path_data.extension == "png") + } else if ((!options_.no_png_crunch && path_data.extension == "png") || path_data.extension == "9.png") { compile_func = &CompilePng; } } } else { context.GetDiagnostics()->Error(DiagMessage() - << "invalid file path '" << path_data.source << "'"); + << "invalid file path '" << path_data.source << "'"); error = true; continue; } // Treat periods as a reserved character that should not be present in a file name // Legacy support for AAPT which did not reserve periods - if (compile_func != &CompileFile && !options.legacy_mode + if (compile_func != &CompileFile && !options_.legacy_mode && std::count(path_data.name.begin(), path_data.name.end(), '.') != 0) { error = true; context.GetDiagnostics()->Error(DiagMessage() << "resource file '" << path_data.source.path @@ -808,7 +802,7 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { // Compile the file. const std::string out_path = BuildIntermediateContainerFilename(path_data); - error |= !compile_func(&context, options, path_data, archive_writer.get(), out_path); + error |= !compile_func(&context, options_, path_data, archive_writer.get(), out_path); } return error ? 1 : 0; } diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index d95cf1c22732..41519520fda1 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -1,13 +1,69 @@ +/* + * Copyright (C) 2018 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 AAPT2_COMPILE_H #define AAPT2_COMPILE_H #include "androidfw/StringPiece.h" +#include "Command.h" #include "Diagnostics.h" +#include "ResourceTable.h" namespace aapt { - int Compile(const std::vector<android::StringPiece>& args, IDiagnostics* diagnostics); +struct CompileOptions { + std::string output_path; + Maybe<std::string> res_dir; + Maybe<std::string> generate_text_symbols_path; + Maybe<Visibility::Level> visibility; + bool pseudolocalize = false; + bool no_png_crunch = false; + bool legacy_mode = false; + bool verbose = false; +}; + +class CompileCommand : public Command { + public: + explicit CompileCommand(IDiagnostics* diagnostic) : Command("compile", "c"), + diagnostic_(diagnostic) { + SetDescription("Compiles resources to be linked into an apk."); + AddRequiredFlag("-o", "Output path", &options_.output_path); + AddOptionalFlag("--dir", "Directory to scan for resources", &options_.res_dir); + AddOptionalFlag("--output-text-symbols", + "Generates a text file containing the resource symbols in the\n" + "specified file", &options_.generate_text_symbols_path); + AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " + "(en-XA and ar-XB)", &options_.pseudolocalize); + AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch); + AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", + &options_.legacy_mode); + AddOptionalSwitch("-v", "Enables verbose logging", &options_.verbose); + AddOptionalFlag("--visibility", + "Sets the visibility of the compiled resources to the specified\n" + "level. Accepted levels: public, private, default", &visibility_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diagnostic_; + CompileOptions options_; + Maybe<std::string> visibility_; +}; }// namespace aapt diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp index 212f2cf26e0d..d21addf4a081 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -23,7 +23,8 @@ namespace aapt { -int TestCompile(std::string path, std::string outDir, bool legacy, StdErrDiagnostics& diag) { +int TestCompile(const std::string& path, const std::string& outDir, bool legacy, + StdErrDiagnostics& diag) { std::vector<android::StringPiece> args; args.push_back(path); args.push_back("-o"); @@ -32,7 +33,7 @@ int TestCompile(std::string path, std::string outDir, bool legacy, StdErrDiagnos if (legacy) { args.push_back("--legacy"); } - return aapt::Compile(args, &diag); + return CompileCommand(&diag).Execute(args, &std::cerr); } TEST(CompilerTest, MultiplePeriods) { diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index eb307fb1ddce..d57eaa1ba145 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -14,12 +14,13 @@ * limitations under the License. */ +#include "Convert.h" + #include <vector> #include "android-base/macros.h" #include "androidfw/StringPiece.h" -#include "Flags.h" #include "LoadedApk.h" #include "ValueVisitor.h" #include "cmd/Util.h" @@ -321,37 +322,18 @@ class Context : public IAaptContext { StdErrDiagnostics diag_; }; -int Convert(const vector<StringPiece>& args) { - - static const char* kOutputFormatProto = "proto"; - static const char* kOutputFormatBinary = "binary"; +const char* ConvertCommand::kOutputFormatProto = "proto"; +const char* ConvertCommand::kOutputFormatBinary = "binary"; - Context context; - std::string output_path; - Maybe<std::string> output_format; - TableFlattenerOptions options; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path", &output_path) - .OptionalFlag("--output-format", StringPrintf("Format of the output. Accepted values are " - "'%s' and '%s'. When not set, defaults to '%s'.", kOutputFormatProto, - kOutputFormatBinary, kOutputFormatBinary), &output_format) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.use_sparse_entries) - .OptionalSwitch("-v", "Enables verbose logging", &context.verbose_); - if (!flags.Parse("aapt2 convert", args, &std::cerr)) { - return 1; - } - - if (flags.GetArgs().size() != 1) { +int ConvertCommand::Action(const std::vector<std::string>& args) { + if (args.size() != 1) { std::cerr << "must supply a single proto APK\n"; - flags.Usage("aapt2 convert", &std::cerr); + Usage(&std::cerr); return 1; } - const StringPiece& path = flags.GetArgs()[0]; + Context context; + const StringPiece& path = args[0]; unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics()); if (apk == nullptr) { context.GetDiagnostics()->Error(DiagMessage(path) << "failed to load APK"); @@ -367,24 +349,24 @@ int Convert(const vector<StringPiece>& args) { context.package_ = app_info.value().package; unique_ptr<IArchiveWriter> writer = - CreateZipFileArchiveWriter(context.GetDiagnostics(), output_path); + CreateZipFileArchiveWriter(context.GetDiagnostics(), output_path_); if (writer == nullptr) { return 1; } unique_ptr<IApkSerializer> serializer; - if (!output_format || output_format.value() == kOutputFormatBinary) { - serializer.reset(new BinaryApkSerializer(&context, apk->GetSource(), options)); - } else if (output_format.value() == kOutputFormatProto) { + if (!output_format_ || output_format_.value() == ConvertCommand::kOutputFormatBinary) { + + serializer.reset(new BinaryApkSerializer(&context, apk->GetSource(), options_)); + } else if (output_format_.value() == ConvertCommand::kOutputFormatProto) { serializer.reset(new ProtoApkSerializer(&context, apk->GetSource())); } else { context.GetDiagnostics()->Error(DiagMessage(path) - << "Invalid value for flag --output-format: " - << output_format.value()); + << "Invalid value for flag --output-format: " + << output_format_.value()); return 1; } - return ConvertApk(&context, std::move(apk), serializer.get(), writer.get()) ? 0 : 1; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h new file mode 100644 index 000000000000..fcec23d16f78 --- /dev/null +++ b/tools/aapt2/cmd/Convert.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 AAPT2_CONVERT_H +#define AAPT2_CONVERT_H + +#include "Command.h" +#include "format/binary/TableFlattener.h" + +namespace aapt { + +class ConvertCommand : public Command { + public: + explicit ConvertCommand() : Command("convert") { + SetDescription("Converts an apk between binary and proto formats."); + AddRequiredFlag("-o", "Output path", &output_path_); + AddOptionalFlag("--output-format", android::base::StringPrintf("Format of the output. " + "Accepted values are '%s' and '%s'. When not set, defaults to '%s'.", + kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); + AddOptionalSwitch("--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.", + &options_.use_sparse_entries); + AddOptionalSwitch("-v", "Enables verbose logging", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + const static char* kOutputFormatProto; + const static char* kOutputFormatBinary; + + TableFlattenerOptions options_; + std::string output_path_; + Maybe<std::string> output_format_; + bool verbose_ = false; +}; + +}// namespace aapt + +#endif //AAPT2_CONVERT_H diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index 12113ed8a48a..262f4fc4e394 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -14,9 +14,10 @@ * limitations under the License. */ +#include "Diff.h" + #include "android-base/macros.h" -#include "Flags.h" #include "LoadedApk.h" #include "ValueVisitor.h" #include "process/IResourceTableConsumer.h" @@ -344,23 +345,18 @@ static void ZeroOutAppReferences(ResourceTable* table) { VisitAllValuesInTable(table, &visitor); } -int Diff(const std::vector<StringPiece>& args) { +int DiffCommand::Action(const std::vector<std::string>& args) { DiffContext context; - Flags flags; - if (!flags.Parse("aapt2 diff", args, &std::cerr)) { - return 1; - } - - if (flags.GetArgs().size() != 2u) { + if (args.size() != 2u) { std::cerr << "must have two apks as arguments.\n\n"; - flags.Usage("aapt2 diff", &std::cerr); + Usage(&std::cerr); return 1; } IDiagnostics* diag = context.GetDiagnostics(); - std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(flags.GetArgs()[0], diag); - std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(flags.GetArgs()[1], diag); + std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(args[0], diag); + std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(args[1], diag); if (!apk_a || !apk_b) { return 1; } diff --git a/tools/aapt2/cmd/Diff.h b/tools/aapt2/cmd/Diff.h new file mode 100644 index 000000000000..c38888863be9 --- /dev/null +++ b/tools/aapt2/cmd/Diff.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 AAPT2_DIFF_H +#define AAPT2_DIFF_H + +#include "Command.h" + +namespace aapt { + +class DiffCommand : public Command { + public: + explicit DiffCommand() : Command("diff") { + SetDescription("Prints the differences in resources of two apks."); + } + + int Action(const std::vector<std::string>& args) override; +}; + +}// namespace aapt + +#endif //AAPT2_DIFF_H diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index 8e7e5e59bc31..8b1f67204c22 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "Dump.h" + #include <cinttypes> #include <vector> @@ -22,7 +24,6 @@ #include "Debug.h" #include "Diagnostics.h" -#include "Flags.h" #include "format/Container.h" #include "format/binary/BinaryResourceParser.h" #include "format/proto/ProtoDeserialize.h" @@ -38,13 +39,6 @@ using ::android::base::StringPrintf; namespace aapt { -struct DumpOptions { - DebugPrintTableOptions print_options; - - // The path to a file within an APK to dump. - Maybe<std::string> file_to_dump_path; -}; - static const char* ResourceFileTypeToString(const ResourceFile::Type& type) { switch (type) { case ResourceFile::Type::kPng: @@ -305,29 +299,13 @@ class DumpContext : public IAaptContext { } // namespace -// Entry point for dump command. -int Dump(const std::vector<StringPiece>& args) { - bool verbose = false; - bool no_values = false; - DumpOptions options; - Flags flags = Flags() - .OptionalSwitch("--no-values", - "Suppresses output of values when displaying resource tables.", - &no_values) - .OptionalFlag("--file", "Dumps the specified file from the APK passed as arg.", - &options.file_to_dump_path) - .OptionalSwitch("-v", "increase verbosity of output", &verbose); - if (!flags.Parse("aapt2 dump", args, &std::cerr)) { - return 1; - } - +int DumpCommand::Action(const std::vector<std::string>& args) { DumpContext context; - context.SetVerbose(verbose); - - options.print_options.show_sources = true; - options.print_options.show_values = !no_values; - for (const std::string& arg : flags.GetArgs()) { - if (!TryDumpFile(&context, arg, options)) { + context.SetVerbose(verbose_); + options_.print_options.show_sources = true; + options_.print_options.show_values = !no_values_; + for (const std::string& arg : args) { + if (!TryDumpFile(&context, arg, options_)) { return 1; } } diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h new file mode 100644 index 000000000000..4893c8b76041 --- /dev/null +++ b/tools/aapt2/cmd/Dump.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 AAPT2_DUMP_H +#define AAPT2_DUMP_H + +#include "Command.h" +#include "Debug.h" + +namespace aapt { + +struct DumpOptions { + DebugPrintTableOptions print_options; + + // The path to a file within an APK to dump. + Maybe<std::string> file_to_dump_path; +}; + +class DumpCommand : public Command { + public: + DumpCommand() : Command("dump", "d") { + SetDescription("Prints resource and manifest information."); + AddOptionalSwitch("--no-values", "Suppresses output of values when displaying resource tables.", + &no_values_); + AddOptionalFlag("--file", "Dumps the specified file from the APK passed as arg.", + &options_.file_to_dump_path); + AddOptionalSwitch("-v", "increase verbosity of output", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + DumpOptions options_; + + bool verbose_ = false; + bool no_values_ = false; +}; + +}// namespace aapt + +#endif //AAPT2_DUMP_H diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index db42e7cb3e02..c94b8473ea87 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -14,9 +14,12 @@ * limitations under the License. */ +#include "Link.h" + #include <sys/stat.h> #include <cinttypes> +#include <algorithm> #include <queue> #include <unordered_map> #include <vector> @@ -28,7 +31,6 @@ #include "AppInfo.h" #include "Debug.h" -#include "Flags.h" #include "LoadedApk.h" #include "Locale.h" #include "NameMangler.h" @@ -73,70 +75,6 @@ using ::android::base::StringPrintf; namespace aapt { -enum class OutputFormat { - kApk, - kProto, -}; - -struct LinkOptions { - std::string output_path; - std::string manifest_path; - std::vector<std::string> include_paths; - std::vector<std::string> overlay_files; - std::vector<std::string> assets_dirs; - bool output_to_directory = false; - bool auto_add_overlay = false; - OutputFormat output_format = OutputFormat::kApk; - - // Java/Proguard options. - Maybe<std::string> generate_java_class_path; - Maybe<std::string> custom_java_package; - std::set<std::string> extra_java_packages; - Maybe<std::string> generate_text_symbols_path; - Maybe<std::string> generate_proguard_rules_path; - Maybe<std::string> generate_main_dex_proguard_rules_path; - bool generate_conditional_proguard_rules = false; - bool generate_non_final_ids = false; - std::vector<std::string> javadoc_annotations; - Maybe<std::string> private_symbols; - - // Optimizations/features. - bool no_auto_version = false; - bool no_version_vectors = false; - bool no_version_transitions = false; - bool no_resource_deduping = false; - bool no_xml_namespaces = false; - bool do_not_compress_anything = false; - std::unordered_set<std::string> extensions_to_not_compress; - - // Static lib options. - bool no_static_lib_packages = false; - - // AndroidManifest.xml massaging options. - ManifestFixerOptions manifest_fixer_options; - - // Products to use/filter on. - std::unordered_set<std::string> products; - - // Flattening options. - TableFlattenerOptions table_flattener_options; - - // Split APK options. - TableSplitterOptions table_splitter_options; - std::vector<SplitConstraints> split_constraints; - std::vector<std::string> split_paths; - - // Stable ID options. - std::unordered_map<ResourceName, ResourceId> stable_id_map; - Maybe<std::string> resource_id_map_path; - - // When 'true', allow reserved package IDs to be used for applications. Pre-O, the platform - // treats negative resource IDs [those with a package ID of 0x80 or higher] as invalid. - // In order to work around this limitation, we allow the use of traditionally reserved - // resource IDs [those between 0x02 and 0x7E]. - bool allow_reserved_package_id = false; -}; - class LinkContext : public IAaptContext { public: LinkContext(IDiagnostics* diagnostics) @@ -486,7 +424,7 @@ std::vector<std::unique_ptr<xml::XmlResource>> ResourceFileFlattener::LinkAndVer return {}; } - if (options_.update_proguard_spec && !proguard::CollectProguardRules(doc, keep_set_)) { + if (options_.update_proguard_spec && !proguard::CollectProguardRules(context_, doc, keep_set_)) { return {}; } @@ -771,9 +709,9 @@ static int32_t FindFrameworkAssetManagerCookie(const android::AssetManager& asse return table.getTableCookie(idx); } -class LinkCommand { +class Linker { public: - LinkCommand(LinkContext* context, const LinkOptions& options) + Linker(LinkContext* context, const LinkOptions& options) : options_(options), context_(context), final_table_(), @@ -961,6 +899,18 @@ class LinkCommand { app_info.version_code = maybe_code.value(); } + if (xml::Attribute* version_code_major_attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor")) { + Maybe<uint32_t> maybe_code = ResourceUtils::ParseInt(version_code_major_attr->value); + if (!maybe_code) { + diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number)) + << "invalid android:versionCodeMajor '" + << version_code_major_attr->value << "'"); + return {}; + } + app_info.version_code_major = maybe_code.value(); + } + if (xml::Attribute* revision_code_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "revisionCode")) { Maybe<uint32_t> maybe_code = ResourceUtils::ParseInt(revision_code_attr->value); @@ -1702,6 +1652,7 @@ class LinkCommand { TableMergerOptions table_merger_options; table_merger_options.auto_add_overlay = options_.auto_add_overlay; + table_merger_options.strict_visibility = options_.strict_visibility; table_merger_ = util::make_unique<TableMerger>(context_, &final_table_, table_merger_options); if (context_->IsVerbose()) { @@ -2025,188 +1976,12 @@ class LinkCommand { Maybe<std::string> included_feature_base_; }; -int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { - LinkContext context(diagnostics); - LinkOptions options; - std::vector<std::string> overlay_arg_list; - std::vector<std::string> extra_java_packages; - Maybe<std::string> package_id; - std::vector<std::string> configs; - Maybe<std::string> preferred_density; - Maybe<std::string> product_list; - bool legacy_x_flag = false; - bool require_localization = false; - bool verbose = false; - bool shared_lib = false; - bool static_lib = false; - bool proto_format = false; - Maybe<std::string> stable_id_file_path; - std::vector<std::string> split_args; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path.", &options.output_path) - .RequiredFlag("--manifest", "Path to the Android manifest to build.", - &options.manifest_path) - .OptionalFlagList("-I", "Adds an Android APK to link against.", &options.include_paths) - .OptionalFlagList("-A", - "An assets directory to include in the APK. These are unprocessed.", - &options.assets_dirs) - .OptionalFlagList("-R", - "Compilation unit to link, using `overlay` semantics.\n" - "The last conflicting resource given takes precedence.", - &overlay_arg_list) - .OptionalFlag("--package-id", - "Specify the package ID to use for this app. Must be greater or equal to\n" - "0x7f and can't be used with --static-lib or --shared-lib.", - &package_id) - .OptionalFlag("--java", "Directory in which to generate R.java.", - &options.generate_java_class_path) - .OptionalFlag("--proguard", "Output file for generated Proguard rules.", - &options.generate_proguard_rules_path) - .OptionalFlag("--proguard-main-dex", - "Output file for generated Proguard rules for the main dex.", - &options.generate_main_dex_proguard_rules_path) - .OptionalSwitch("--proguard-conditional-keep-rules", - "Generate conditional Proguard keep rules.", - &options.generate_conditional_proguard_rules) - .OptionalSwitch("--no-auto-version", - "Disables automatic style and layout SDK versioning.", - &options.no_auto_version) - .OptionalSwitch("--no-version-vectors", - "Disables automatic versioning of vector drawables. Use this only\n" - "when building with vector drawable support library.", - &options.no_version_vectors) - .OptionalSwitch("--no-version-transitions", - "Disables automatic versioning of transition resources. Use this only\n" - "when building with transition support library.", - &options.no_version_transitions) - .OptionalSwitch("--no-resource-deduping", - "Disables automatic deduping of resources with\n" - "identical values across compatible configurations.", - &options.no_resource_deduping) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.table_flattener_options.use_sparse_entries) - .OptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.", - &legacy_x_flag) - .OptionalSwitch("-z", "Require localization of strings marked 'suggested'.", - &require_localization) - .OptionalFlagList("-c", - "Comma separated list of configurations to include. The default\n" - "is all configurations.", - &configs) - .OptionalFlag("--preferred-density", - "Selects the closest matching density and strips out all others.", - &preferred_density) - .OptionalFlag("--product", "Comma separated list of product names to keep", &product_list) - .OptionalSwitch("--output-to-dir", - "Outputs the APK contents to a directory specified by -o.", - &options.output_to_directory) - .OptionalSwitch("--no-xml-namespaces", - "Removes XML namespace prefix and URI information from\n" - "AndroidManifest.xml and XML binaries in res/*.", - &options.no_xml_namespaces) - .OptionalFlag("--min-sdk-version", - "Default minimum SDK version to use for AndroidManifest.xml.", - &options.manifest_fixer_options.min_sdk_version_default) - .OptionalFlag("--target-sdk-version", - "Default target SDK version to use for AndroidManifest.xml.", - &options.manifest_fixer_options.target_sdk_version_default) - .OptionalFlag("--version-code", - "Version code (integer) to inject into the AndroidManifest.xml if none is\n" - "present.", - &options.manifest_fixer_options.version_code_default) - .OptionalFlag("--version-name", - "Version name to inject into the AndroidManifest.xml if none is present.", - &options.manifest_fixer_options.version_name_default) - .OptionalSwitch("--replace-version", - "If --version-code and/or --version-name are specified, these\n" - "values will replace any value already in the manifest. By\n" - "default, nothing is changed if the manifest already defines\n" - "these attributes.", - &options.manifest_fixer_options.replace_version) - .OptionalFlag("--compile-sdk-version-code", - "Version code (integer) to inject into the AndroidManifest.xml if none is\n" - "present.", - &options.manifest_fixer_options.compile_sdk_version) - .OptionalFlag("--compile-sdk-version-name", - "Version name to inject into the AndroidManifest.xml if none is present.", - &options.manifest_fixer_options.compile_sdk_version_codename) - .OptionalSwitch("--shared-lib", "Generates a shared Android runtime library.", - &shared_lib) - .OptionalSwitch("--static-lib", "Generate a static Android library.", &static_lib) - .OptionalSwitch("--proto-format", - "Generates compiled resources in Protobuf format.\n" - "Suitable as input to the bundle tool for generating an App Bundle.", - &proto_format) - .OptionalSwitch("--no-static-lib-packages", - "Merge all library resources under the app's package.", - &options.no_static_lib_packages) - .OptionalSwitch("--non-final-ids", - "Generates R.java without the final modifier. This is implied when\n" - "--static-lib is specified.", - &options.generate_non_final_ids) - .OptionalFlag("--stable-ids", "File containing a list of name to ID mapping.", - &stable_id_file_path) - .OptionalFlag("--emit-ids", - "Emit a file at the given path with a list of name to ID mappings,\n" - "suitable for use with --stable-ids.", - &options.resource_id_map_path) - .OptionalFlag("--private-symbols", - "Package name to use when generating R.java for private symbols.\n" - "If not specified, public and private symbols will use the application's\n" - "package name.", - &options.private_symbols) - .OptionalFlag("--custom-package", "Custom Java package under which to generate R.java.", - &options.custom_java_package) - .OptionalFlagList("--extra-packages", - "Generate the same R.java but with different package names.", - &extra_java_packages) - .OptionalFlagList("--add-javadoc-annotation", - "Adds a JavaDoc annotation to all generated Java classes.", - &options.javadoc_annotations) - .OptionalFlag("--output-text-symbols", - "Generates a text file containing the resource symbols of the R class in\n" - "the specified folder.", - &options.generate_text_symbols_path) - .OptionalSwitch("--allow-reserved-package-id", - "Allows the use of a reserved package ID. This should on be used for\n" - "packages with a pre-O min-sdk\n", - &options.allow_reserved_package_id) - .OptionalSwitch("--auto-add-overlay", - "Allows the addition of new resources in overlays without\n" - "<add-resource> tags.", - &options.auto_add_overlay) - .OptionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml.", - &options.manifest_fixer_options.rename_manifest_package) - .OptionalFlag("--rename-instrumentation-target-package", - "Changes the name of the target package for instrumentation. Most useful\n" - "when used in conjunction with --rename-manifest-package.", - &options.manifest_fixer_options.rename_instrumentation_target_package) - .OptionalFlagList("-0", "File extensions not to compress.", - &options.extensions_to_not_compress) - .OptionalSwitch("--warn-manifest-validation", - "Treat manifest validation errors as warnings.", - &options.manifest_fixer_options.warn_validation) - .OptionalFlagList("--split", - "Split resources matching a set of configs out to a Split APK.\n" - "Syntax: path/to/output.apk:<config>[,<config>[...]].\n" - "On Windows, use a semicolon ';' separator instead.", - &split_args) - .OptionalSwitch("-v", "Enables verbose logging.", &verbose) - .OptionalSwitch("--debug-mode", - "Inserts android:debuggable=\"true\" in to the application node of the\n" - "manifest, making the application debuggable even on production devices.", - &options.manifest_fixer_options.debug_mode); - - if (!flags.Parse("aapt2 link", args, &std::cerr)) { - return 1; - } +int LinkCommand::Action(const std::vector<std::string>& args) { + LinkContext context(diag_); // Expand all argument-files passed into the command line. These start with '@'. std::vector<std::string> arg_list; - for (const std::string& arg : flags.GetArgs()) { + for (const std::string& arg : args) { if (util::StartsWith(arg, "@")) { const std::string path = arg.substr(1, arg.size() - 1); std::string error; @@ -2220,27 +1995,27 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { } // Expand all argument-files passed to -R. - for (const std::string& arg : overlay_arg_list) { + for (const std::string& arg : overlay_arg_list_) { if (util::StartsWith(arg, "@")) { const std::string path = arg.substr(1, arg.size() - 1); std::string error; - if (!file::AppendArgsFromFile(path, &options.overlay_files, &error)) { + if (!file::AppendArgsFromFile(path, &options_.overlay_files, &error)) { context.GetDiagnostics()->Error(DiagMessage(path) << error); return 1; } } else { - options.overlay_files.push_back(arg); + options_.overlay_files.push_back(arg); } } - if (verbose) { - context.SetVerbose(verbose); + if (verbose_) { + context.SetVerbose(verbose_); } - if (int{shared_lib} + int{static_lib} + int{proto_format} > 1) { + if (int{shared_lib_} + int{static_lib_} + int{proto_format_} > 1) { context.GetDiagnostics()->Error( DiagMessage() - << "only one of --shared-lib, --static-lib, or --proto_format can be defined"); + << "only one of --shared-lib, --static-lib, or --proto_format can be defined"); return 1; } @@ -2248,26 +2023,26 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { context.SetPackageType(PackageType::kApp); context.SetPackageId(kAppPackageId); - if (shared_lib) { + if (shared_lib_) { context.SetPackageType(PackageType::kSharedLib); context.SetPackageId(0x00); - } else if (static_lib) { + } else if (static_lib_) { context.SetPackageType(PackageType::kStaticLib); - options.output_format = OutputFormat::kProto; - } else if (proto_format) { - options.output_format = OutputFormat::kProto; + options_.output_format = OutputFormat::kProto; + } else if (proto_format_) { + options_.output_format = OutputFormat::kProto; } - if (package_id) { + if (package_id_) { if (context.GetPackageType() != PackageType::kApp) { context.GetDiagnostics()->Error( DiagMessage() << "can't specify --package-id when not building a regular app"); return 1; } - const Maybe<uint32_t> maybe_package_id_int = ResourceUtils::ParseInt(package_id.value()); + const Maybe<uint32_t> maybe_package_id_int = ResourceUtils::ParseInt(package_id_.value()); if (!maybe_package_id_int) { - context.GetDiagnostics()->Error(DiagMessage() << "package ID '" << package_id.value() + context.GetDiagnostics()->Error(DiagMessage() << "package ID '" << package_id_.value() << "' is not a valid integer"); return 1; } @@ -2275,7 +2050,7 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { const uint32_t package_id_int = maybe_package_id_int.value(); if (package_id_int > std::numeric_limits<uint8_t>::max() || package_id_int == kFrameworkPackageId - || (!options.allow_reserved_package_id && package_id_int < kAppPackageId)) { + || (!options_.allow_reserved_package_id && package_id_int < kAppPackageId)) { context.GetDiagnostics()->Error( DiagMessage() << StringPrintf( "invalid package ID 0x%02x. Must be in the range 0x7f-0xff.", package_id_int)); @@ -2285,71 +2060,71 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { } // Populate the set of extra packages for which to generate R.java. - for (std::string& extra_package : extra_java_packages) { + for (std::string& extra_package : extra_java_packages_) { // A given package can actually be a colon separated list of packages. for (StringPiece package : util::Split(extra_package, ':')) { - options.extra_java_packages.insert(package.to_string()); + options_.extra_java_packages.insert(package.to_string()); } } - if (product_list) { - for (StringPiece product : util::Tokenize(product_list.value(), ',')) { + if (product_list_) { + for (StringPiece product : util::Tokenize(product_list_.value(), ',')) { if (product != "" && product != "default") { - options.products.insert(product.to_string()); + options_.products.insert(product.to_string()); } } } std::unique_ptr<IConfigFilter> filter; - if (!configs.empty()) { - filter = ParseConfigFilterParameters(configs, context.GetDiagnostics()); + if (!configs_.empty()) { + filter = ParseConfigFilterParameters(configs_, context.GetDiagnostics()); if (filter == nullptr) { return 1; } - options.table_splitter_options.config_filter = filter.get(); + options_.table_splitter_options.config_filter = filter.get(); } - if (preferred_density) { + if (preferred_density_) { Maybe<uint16_t> density = - ParseTargetDensityParameter(preferred_density.value(), context.GetDiagnostics()); + ParseTargetDensityParameter(preferred_density_.value(), context.GetDiagnostics()); if (!density) { return 1; } - options.table_splitter_options.preferred_densities.push_back(density.value()); + options_.table_splitter_options.preferred_densities.push_back(density.value()); } // Parse the split parameters. - for (const std::string& split_arg : split_args) { - options.split_paths.push_back({}); - options.split_constraints.push_back({}); - if (!ParseSplitParameter(split_arg, context.GetDiagnostics(), &options.split_paths.back(), - &options.split_constraints.back())) { + for (const std::string& split_arg : split_args_) { + options_.split_paths.push_back({}); + options_.split_constraints.push_back({}); + if (!ParseSplitParameter(split_arg, context.GetDiagnostics(), &options_.split_paths.back(), + &options_.split_constraints.back())) { return 1; } } - if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path) { - if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path.value(), - &options.stable_id_map)) { + if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path_) { + if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path_.value(), + &options_.stable_id_map)) { return 1; } } // Populate some default no-compress extensions that are already compressed. - options.extensions_to_not_compress.insert( + options_.extensions_to_not_compress.insert( {".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg", - ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", - ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", - ".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"}); + ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", + ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", + ".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"}); // Turn off auto versioning for static-libs. if (context.GetPackageType() == PackageType::kStaticLib) { - options.no_auto_version = true; - options.no_version_vectors = true; - options.no_version_transitions = true; + options_.no_auto_version = true; + options_.no_version_vectors = true; + options_.no_version_transitions = true; } - LinkCommand cmd(&context, options); + Linker cmd(&context, options_); return cmd.Run(arg_list); } diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h new file mode 100644 index 000000000000..e58a93ec416c --- /dev/null +++ b/tools/aapt2/cmd/Link.h @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2018 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 AAPT2_LINK_H +#define AAPT2_LINK_H + +#include "Command.h" +#include "Diagnostics.h" +#include "Resource.h" +#include "split/TableSplitter.h" +#include "format/binary/TableFlattener.h" +#include "link/ManifestFixer.h" + +namespace aapt { + +enum class OutputFormat { + kApk, + kProto, +}; + +struct LinkOptions { + std::string output_path; + std::string manifest_path; + std::vector<std::string> include_paths; + std::vector<std::string> overlay_files; + std::vector<std::string> assets_dirs; + bool output_to_directory = false; + bool auto_add_overlay = false; + OutputFormat output_format = OutputFormat::kApk; + + // Java/Proguard options. + Maybe<std::string> generate_java_class_path; + Maybe<std::string> custom_java_package; + std::set<std::string> extra_java_packages; + Maybe<std::string> generate_text_symbols_path; + Maybe<std::string> generate_proguard_rules_path; + Maybe<std::string> generate_main_dex_proguard_rules_path; + bool generate_conditional_proguard_rules = false; + bool generate_non_final_ids = false; + std::vector<std::string> javadoc_annotations; + Maybe<std::string> private_symbols; + + // Optimizations/features. + bool no_auto_version = false; + bool no_version_vectors = false; + bool no_version_transitions = false; + bool no_resource_deduping = false; + bool no_xml_namespaces = false; + bool do_not_compress_anything = false; + std::unordered_set<std::string> extensions_to_not_compress; + + // Static lib options. + bool no_static_lib_packages = false; + + // AndroidManifest.xml massaging options. + ManifestFixerOptions manifest_fixer_options; + + // Products to use/filter on. + std::unordered_set<std::string> products; + + // Flattening options. + TableFlattenerOptions table_flattener_options; + + // Split APK options. + TableSplitterOptions table_splitter_options; + std::vector<SplitConstraints> split_constraints; + std::vector<std::string> split_paths; + + // Stable ID options. + std::unordered_map<ResourceName, ResourceId> stable_id_map; + Maybe<std::string> resource_id_map_path; + + // When 'true', allow reserved package IDs to be used for applications. Pre-O, the platform + // treats negative resource IDs [those with a package ID of 0x80 or higher] as invalid. + // In order to work around this limitation, we allow the use of traditionally reserved + // resource IDs [those between 0x02 and 0x7E]. + bool allow_reserved_package_id = false; + + // Whether we should fail on definitions of a resource with conflicting visibility. + bool strict_visibility = false; +}; + +class LinkCommand : public Command { + public: + explicit LinkCommand(IDiagnostics* diag) : Command("link", "l"), + diag_(diag) { + SetDescription("Links resources into an apk."); + AddRequiredFlag("-o", "Output path.", &options_.output_path); + AddRequiredFlag("--manifest", "Path to the Android manifest to build.", + &options_.manifest_path); + AddOptionalFlagList("-I", "Adds an Android APK to link against.", &options_.include_paths); + AddOptionalFlagList("-A", "An assets directory to include in the APK. These are unprocessed.", + &options_.assets_dirs); + AddOptionalFlagList("-R", "Compilation unit to link, using `overlay` semantics.\n" + "The last conflicting resource given takes precedence.", &overlay_arg_list_); + AddOptionalFlag("--package-id", + "Specify the package ID to use for this app. Must be greater or equal to\n" + "0x7f and can't be used with --static-lib or --shared-lib.", &package_id_); + AddOptionalFlag("--java", "Directory in which to generate R.java.", + &options_.generate_java_class_path); + AddOptionalFlag("--proguard", "Output file for generated Proguard rules.", + &options_.generate_proguard_rules_path); + AddOptionalFlag("--proguard-main-dex", + "Output file for generated Proguard rules for the main dex.", + &options_.generate_main_dex_proguard_rules_path); + AddOptionalSwitch("--proguard-conditional-keep-rules", + "Generate conditional Proguard keep rules.", + &options_.generate_conditional_proguard_rules); + AddOptionalSwitch("--no-auto-version", "Disables automatic style and layout SDK versioning.", + &options_.no_auto_version); + AddOptionalSwitch("--no-version-vectors", + "Disables automatic versioning of vector drawables. Use this only\n" + "when building with vector drawable support library.", + &options_.no_version_vectors); + AddOptionalSwitch("--no-version-transitions", + "Disables automatic versioning of transition resources. Use this only\n" + "when building with transition support library.", + &options_.no_version_transitions); + AddOptionalSwitch("--no-resource-deduping", "Disables automatic deduping of resources with\n" + "identical values across compatible configurations.", + &options_.no_resource_deduping); + AddOptionalSwitch("--enable-sparse-encoding", + "This decreases APK size at the cost of resource retrieval performance.", + &options_.table_flattener_options.use_sparse_entries); + AddOptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.", + &legacy_x_flag_); + AddOptionalSwitch("-z", "Require localization of strings marked 'suggested'.", + &require_localization_); + AddOptionalFlagList("-c", + "Comma separated list of configurations to include. The default\n" + "is all configurations.", &configs_); + AddOptionalFlag("--preferred-density", + "Selects the closest matching density and strips out all others.", + &preferred_density_); + AddOptionalFlag("--product", "Comma separated list of product names to keep", &product_list_); + AddOptionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified by -o.", + &options_.output_to_directory); + AddOptionalSwitch("--no-xml-namespaces", "Removes XML namespace prefix and URI information\n" + "from AndroidManifest.xml and XML binaries in res/*.", + &options_.no_xml_namespaces); + AddOptionalFlag("--min-sdk-version", + "Default minimum SDK version to use for AndroidManifest.xml.", + &options_.manifest_fixer_options.min_sdk_version_default); + AddOptionalFlag("--target-sdk-version", + "Default target SDK version to use for AndroidManifest.xml.", + &options_.manifest_fixer_options.target_sdk_version_default); + AddOptionalFlag("--version-code", + "Version code (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", &options_.manifest_fixer_options.version_code_default); + AddOptionalFlag("--version-code-major", + "Version code major (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", &options_.manifest_fixer_options.version_code_major_default); + AddOptionalFlag("--version-name", + "Version name to inject into the AndroidManifest.xml if none is present.", + &options_.manifest_fixer_options.version_name_default); + AddOptionalSwitch("--replace-version", + "If --version-code and/or --version-name are specified, these\n" + "values will replace any value already in the manifest. By\n" + "default, nothing is changed if the manifest already defines\n" + "these attributes.", + &options_.manifest_fixer_options.replace_version); + AddOptionalFlag("--compile-sdk-version-code", + "Version code (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", + &options_.manifest_fixer_options.compile_sdk_version); + AddOptionalFlag("--compile-sdk-version-name", + "Version name to inject into the AndroidManifest.xml if none is present.", + &options_.manifest_fixer_options.compile_sdk_version_codename); + AddOptionalSwitch("--shared-lib", "Generates a shared Android runtime library.", + &shared_lib_); + AddOptionalSwitch("--static-lib", "Generate a static Android library.", &static_lib_); + AddOptionalSwitch("--proto-format", + "Generates compiled resources in Protobuf format.\n" + "Suitable as input to the bundle tool for generating an App Bundle.", + &proto_format_); + AddOptionalSwitch("--no-static-lib-packages", + "Merge all library resources under the app's package.", + &options_.no_static_lib_packages); + AddOptionalSwitch("--non-final-ids", + "Generates R.java without the final modifier. This is implied when\n" + "--static-lib is specified.", + &options_.generate_non_final_ids); + AddOptionalFlag("--stable-ids", "File containing a list of name to ID mapping.", + &stable_id_file_path_); + AddOptionalFlag("--emit-ids", + "Emit a file at the given path with a list of name to ID mappings,\n" + "suitable for use with --stable-ids.", + &options_.resource_id_map_path); + AddOptionalFlag("--private-symbols", + "Package name to use when generating R.java for private symbols.\n" + "If not specified, public and private symbols will use the application's\n" + "package name.", + &options_.private_symbols); + AddOptionalFlag("--custom-package", "Custom Java package under which to generate R.java.", + &options_.custom_java_package); + AddOptionalFlagList("--extra-packages", + "Generate the same R.java but with different package names.", + &extra_java_packages_); + AddOptionalFlagList("--add-javadoc-annotation", + "Adds a JavaDoc annotation to all generated Java classes.", + &options_.javadoc_annotations); + AddOptionalFlag("--output-text-symbols", + "Generates a text file containing the resource symbols of the R class in\n" + "the specified folder.", + &options_.generate_text_symbols_path); + AddOptionalSwitch("--allow-reserved-package-id", + "Allows the use of a reserved package ID. This should on be used for\n" + "packages with a pre-O min-sdk\n", + &options_.allow_reserved_package_id); + AddOptionalSwitch("--auto-add-overlay", + "Allows the addition of new resources in overlays without\n" + "<add-resource> tags.", + &options_.auto_add_overlay); + AddOptionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml.", + &options_.manifest_fixer_options.rename_manifest_package); + AddOptionalFlag("--rename-instrumentation-target-package", + "Changes the name of the target package for instrumentation. Most useful\n" + "when used in conjunction with --rename-manifest-package.", + &options_.manifest_fixer_options.rename_instrumentation_target_package); + AddOptionalFlagList("-0", "File extensions not to compress.", + &options_.extensions_to_not_compress); + AddOptionalSwitch("--no-compress", "Do not compress any resources.", + &options_.do_not_compress_anything); + AddOptionalSwitch("--warn-manifest-validation", + "Treat manifest validation errors as warnings.", + &options_.manifest_fixer_options.warn_validation); + AddOptionalFlagList("--split", + "Split resources matching a set of configs out to a Split APK.\n" + "Syntax: path/to/output.apk:<config>[,<config>[...]].\n" + "On Windows, use a semicolon ';' separator instead.", + &split_args_); + AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); + AddOptionalSwitch("--debug-mode", + "Inserts android:debuggable=\"true\" in to the application node of the\n" + "manifest, making the application debuggable even on production devices.", + &options_.manifest_fixer_options.debug_mode); + AddOptionalSwitch("--strict-visibility", + "Do not allow overlays with different visibility levels.", + &options_.strict_visibility); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; + LinkOptions options_; + + std::vector<std::string> overlay_arg_list_; + std::vector<std::string> extra_java_packages_; + Maybe<std::string> package_id_; + std::vector<std::string> configs_; + Maybe<std::string> preferred_density_; + Maybe<std::string> product_list_; + bool legacy_x_flag_ = false; + bool require_localization_ = false; + bool verbose_ = false; + bool shared_lib_ = false; + bool static_lib_ = false; + bool proto_format_ = false; + Maybe<std::string> stable_id_file_path_; + std::vector<std::string> split_args_; +}; + +}// namespace aapt + +#endif //AAPT2_LINK_H
\ No newline at end of file diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 9c76119f9504..47288ec092de 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "Optimize.h" + #include <memory> #include <vector> @@ -24,7 +26,6 @@ #include "androidfw/StringPiece.h" #include "Diagnostics.h" -#include "Flags.h" #include "LoadedApk.h" #include "ResourceUtils.h" #include "SdkConstants.h" @@ -38,6 +39,7 @@ #include "io/Util.h" #include "optimize/MultiApkGenerator.h" #include "optimize/ResourceDeduper.h" +#include "optimize/ResourceFilter.h" #include "optimize/VersionCollapser.h" #include "split/TableSplitter.h" #include "util/Files.h" @@ -53,33 +55,6 @@ using ::android::base::StringPrintf; namespace aapt { -struct OptimizeOptions { - // Path to the output APK. - Maybe<std::string> output_path; - // Path to the output APK directory for splits. - Maybe<std::string> output_dir; - - // Details of the app extracted from the AndroidManifest.xml - AppInfo app_info; - - // Split APK options. - TableSplitterOptions table_splitter_options; - - // List of output split paths. These are in the same order as `split_constraints`. - std::vector<std::string> split_paths; - - // List of SplitConstraints governing what resources go into each split. Ordered by `split_paths`. - std::vector<SplitConstraints> split_constraints; - - TableFlattenerOptions table_flattener_options; - - Maybe<std::vector<OutputArtifact>> apk_artifacts; - - // Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts - // are kept and will be written as output. - std::unordered_set<std::string> kept_artifacts; -}; - class OptimizeContext : public IAaptContext { public: OptimizeContext() = default; @@ -137,9 +112,9 @@ class OptimizeContext : public IAaptContext { int sdk_version_ = 0; }; -class OptimizeCommand { +class Optimizer { public: - OptimizeCommand(OptimizeContext* context, const OptimizeOptions& options) + Optimizer(OptimizeContext* context, const OptimizeOptions& options) : options_(options), context_(context) { } @@ -147,6 +122,13 @@ class OptimizeCommand { if (context_->IsVerbose()) { context_->GetDiagnostics()->Note(DiagMessage() << "Optimizing APK..."); } + if (!options_.resources_blacklist.empty()) { + ResourceFilter filter(options_.resources_blacklist); + if (!filter.Consume(context_, apk->GetResourceTable())) { + context_->GetDiagnostics()->Error(DiagMessage() << "failed filtering resources"); + return 1; + } + } VersionCollapser collapser; if (!collapser.Consume(context_, apk->GetResourceTable())) { @@ -284,16 +266,62 @@ class OptimizeCommand { OptimizeContext* context_; }; -bool ExtractWhitelistFromConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { +bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { std::string contents; if (!ReadFileToString(path, &contents, true)) { context->GetDiagnostics()->Error(DiagMessage() << "failed to parse whitelist from config file: " << path); return false; } - for (const StringPiece& resource_name : util::Tokenize(contents, ',')) { - options->table_flattener_options.whitelisted_resources.insert(resource_name.to_string()); + for (StringPiece resource_name : util::Tokenize(contents, ',')) { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.to_string()); + } + return true; +} + +bool ExtractConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { + std::string content; + if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { + context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist"); + return false; + } + + size_t line_no = 0; + for (StringPiece line : util::Tokenize(content, '\n')) { + line_no++; + line = util::TrimWhitespace(line); + if (line.empty()) { + continue; + } + + auto split_line = util::Split(line, '#'); + if (split_line.size() < 2) { + context->GetDiagnostics()->Error(DiagMessage(line) << "No # found in line"); + return false; + } + StringPiece resource_string = split_line[0]; + StringPiece directives = split_line[1]; + ResourceNameRef resource_name; + if (!ResourceUtils::ParseResourceName(resource_string, &resource_name)) { + context->GetDiagnostics()->Error(DiagMessage(line) << "Malformed resource name"); + return false; + } + if (!resource_name.package.empty()) { + context->GetDiagnostics()->Error(DiagMessage(line) + << "Package set for resource. Only use type/name"); + return false; + } + for (StringPiece directive : util::Tokenize(directives, ',')) { + if (directive == "remove") { + options->resources_blacklist.insert(resource_name.ToResourceName()); + } else if (directive == "no_obfuscate") { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.entry.to_string()); + } + } } return true; } @@ -317,76 +345,24 @@ bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk, return true; } -int Optimize(const std::vector<StringPiece>& args) { - OptimizeContext context; - OptimizeOptions options; - Maybe<std::string> config_path; - Maybe<std::string> whitelist_path; - Maybe<std::string> target_densities; - std::vector<std::string> configs; - std::vector<std::string> split_args; - std::unordered_set<std::string> kept_artifacts; - bool verbose = false; - bool print_only = false; - Flags flags = - Flags() - .OptionalFlag("-o", "Path to the output APK.", &options.output_path) - .OptionalFlag("-d", "Path to the output directory (for splits).", &options.output_dir) - .OptionalFlag("-x", "Path to XML configuration file.", &config_path) - .OptionalSwitch("-p", "Print the multi APK artifacts and exit.", &print_only) - .OptionalFlag( - "--target-densities", - "Comma separated list of the screen densities that the APK will be optimized for.\n" - "All the resources that would be unused on devices of the given densities will be \n" - "removed from the APK.", - &target_densities) - .OptionalFlag("--whitelist-config-path", - "Path to the whitelist.cfg file containing whitelisted resources \n" - "whose names should not be altered in final resource tables.", - &whitelist_path) - .OptionalFlagList("-c", - "Comma separated list of configurations to include. The default\n" - "is all configurations.", - &configs) - .OptionalFlagList("--split", - "Split resources matching a set of configs out to a " - "Split APK.\nSyntax: path/to/output.apk;<config>[,<config>[...]].\n" - "On Windows, use a semicolon ';' separator instead.", - &split_args) - .OptionalFlagList("--keep-artifacts", - "Comma separated list of artifacts to keep. If none are specified,\n" - "all artifacts will be kept.", - &kept_artifacts) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.table_flattener_options.use_sparse_entries) - .OptionalSwitch("--enable-resource-obfuscation", - "Enables obfuscation of key string pool to single value", - &options.table_flattener_options.collapse_key_stringpool) - .OptionalSwitch("-v", "Enables verbose logging", &verbose); - - if (!flags.Parse("aapt2 optimize", args, &std::cerr)) { - return 1; - } - - if (flags.GetArgs().size() != 1u) { +int OptimizeCommand::Action(const std::vector<std::string>& args) { + if (args.size() != 1u) { std::cerr << "must have one APK as argument.\n\n"; - flags.Usage("aapt2 optimize", &std::cerr); + Usage(&std::cerr); return 1; } - const std::string& apk_path = flags.GetArgs()[0]; - - context.SetVerbose(verbose); + const std::string& apk_path = args[0]; + OptimizeContext context; + context.SetVerbose(verbose_); IDiagnostics* diag = context.GetDiagnostics(); - if (config_path) { - std::string& path = config_path.value(); + if (config_path_) { + std::string& path = config_path_.value(); Maybe<ConfigurationParser> for_path = ConfigurationParser::ForPath(path); if (for_path) { - options.apk_artifacts = for_path.value().WithDiagnostics(diag).Parse(apk_path); - if (!options.apk_artifacts) { + options_.apk_artifacts = for_path.value().WithDiagnostics(diag).Parse(apk_path); + if (!options_.apk_artifacts) { diag->Error(DiagMessage() << "Failed to parse the output artifact list"); return 1; } @@ -396,28 +372,28 @@ int Optimize(const std::vector<StringPiece>& args) { return 1; } - if (print_only) { - for (const OutputArtifact& artifact : options.apk_artifacts.value()) { + if (print_only_) { + for (const OutputArtifact& artifact : options_.apk_artifacts.value()) { std::cout << artifact.name << std::endl; } return 0; } - if (!kept_artifacts.empty()) { - for (const std::string& artifact_str : kept_artifacts) { + if (!kept_artifacts_.empty()) { + for (const std::string& artifact_str : kept_artifacts_) { for (const StringPiece& artifact : util::Tokenize(artifact_str, ',')) { - options.kept_artifacts.insert(artifact.to_string()); + options_.kept_artifacts.insert(artifact.to_string()); } } } // Since we know that we are going to process the APK (not just print targets), make sure we // have somewhere to write them to. - if (!options.output_dir) { + if (!options_.output_dir) { diag->Error(DiagMessage() << "Output directory is required when using a configuration file"); return 1; } - } else if (print_only) { + } else if (print_only_) { diag->Error(DiagMessage() << "Asked to print artifacts without providing a configurations"); return 1; } @@ -427,50 +403,57 @@ int Optimize(const std::vector<StringPiece>& args) { return 1; } - if (target_densities) { + if (target_densities_) { // Parse the target screen densities. - for (const StringPiece& config_str : util::Tokenize(target_densities.value(), ',')) { + for (const StringPiece& config_str : util::Tokenize(target_densities_.value(), ',')) { Maybe<uint16_t> target_density = ParseTargetDensityParameter(config_str, diag); if (!target_density) { return 1; } - options.table_splitter_options.preferred_densities.push_back(target_density.value()); + options_.table_splitter_options.preferred_densities.push_back(target_density.value()); } } std::unique_ptr<IConfigFilter> filter; - if (!configs.empty()) { - filter = ParseConfigFilterParameters(configs, diag); + if (!configs_.empty()) { + filter = ParseConfigFilterParameters(configs_, diag); if (filter == nullptr) { return 1; } - options.table_splitter_options.config_filter = filter.get(); + options_.table_splitter_options.config_filter = filter.get(); } // Parse the split parameters. - for (const std::string& split_arg : split_args) { - options.split_paths.emplace_back(); - options.split_constraints.emplace_back(); - if (!ParseSplitParameter(split_arg, diag, &options.split_paths.back(), - &options.split_constraints.back())) { + for (const std::string& split_arg : split_args_) { + options_.split_paths.emplace_back(); + options_.split_constraints.emplace_back(); + if (!ParseSplitParameter(split_arg, diag, &options_.split_paths.back(), + &options_.split_constraints.back())) { return 1; } } - if (options.table_flattener_options.collapse_key_stringpool) { - if (whitelist_path) { - std::string& path = whitelist_path.value(); - if (!ExtractWhitelistFromConfig(path, &context, &options)) { + if (options_.table_flattener_options.collapse_key_stringpool) { + if (whitelist_path_) { + std::string& path = whitelist_path_.value(); + if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options_)) { return 1; } } } - if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) { + if (resources_config_path_) { + std::string& path = resources_config_path_.value(); + if (!ExtractConfig(path, &context, &options_)) { + return 1; + } + } + + if (!ExtractAppDataFromManifest(&context, apk.get(), &options_)) { return 1; } - OptimizeCommand cmd(&context, options); + Optimizer cmd(&context, options_); return cmd.Run(std::move(apk)); } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h new file mode 100644 index 000000000000..43bc216382fa --- /dev/null +++ b/tools/aapt2/cmd/Optimize.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 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 AAPT2_OPTIMIZE_H +#define AAPT2_OPTIMIZE_H + +#include "AppInfo.h" +#include "Command.h" +#include "configuration/ConfigurationParser.h" +#include "format/binary/TableFlattener.h" +#include "split/TableSplitter.h" + +namespace aapt { + +struct OptimizeOptions { + friend class OptimizeCommand; + + // Path to the output APK. + Maybe<std::string> output_path; + // Path to the output APK directory for splits. + Maybe<std::string> output_dir; + + // Details of the app extracted from the AndroidManifest.xml + AppInfo app_info; + + // Blacklist of unused resources that should be removed from the apk. + std::unordered_set<ResourceName> resources_blacklist; + + // Split APK options. + TableSplitterOptions table_splitter_options; + + // List of output split paths. These are in the same order as `split_constraints`. + std::vector<std::string> split_paths; + + // List of SplitConstraints governing what resources go into each split. Ordered by `split_paths`. + std::vector<SplitConstraints> split_constraints; + + TableFlattenerOptions table_flattener_options; + + Maybe<std::vector<aapt::configuration::OutputArtifact>> apk_artifacts; + + // Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts + // are kept and will be written as output. + std::unordered_set<std::string> kept_artifacts; +}; + +class OptimizeCommand : public Command { + public: + explicit OptimizeCommand() : Command("optimize") { + SetDescription("Preforms resource optimizations on an apk."); + AddOptionalFlag("-o", "Path to the output APK.", &options_.output_path); + AddOptionalFlag("-d", "Path to the output directory (for splits).", &options_.output_dir); + AddOptionalFlag("-x", "Path to XML configuration file.", &config_path_); + AddOptionalSwitch("-p", "Print the multi APK artifacts and exit.", &print_only_); + AddOptionalFlag( + "--target-densities", + "Comma separated list of the screen densities that the APK will be optimized for.\n" + "All the resources that would be unused on devices of the given densities will be \n" + "removed from the APK.", + &target_densities_); + AddOptionalFlag("--whitelist-path", + "Path to the whitelist.cfg file containing whitelisted resources \n" + "whose names should not be altered in final resource tables.", + &whitelist_path_); + AddOptionalFlag("--resources-config-path", + "Path to the resources.cfg file containing the list of resources and \n" + "directives to each resource. \n" + "Format: type/resource_name#[directive][,directive]", + &resources_config_path_); + AddOptionalFlagList("-c", + "Comma separated list of configurations to include. The default\n" + "is all configurations.", + &configs_); + AddOptionalFlagList("--split", + "Split resources matching a set of configs out to a " + "Split APK.\nSyntax: path/to/output.apk;<config>[,<config>[...]].\n" + "On Windows, use a semicolon ';' separator instead.", + &split_args_); + AddOptionalFlagList("--keep-artifacts", + "Comma separated list of artifacts to keep. If none are specified,\n" + "all artifacts will be kept.", + &kept_artifacts_); + AddOptionalSwitch("--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.", + &options_.table_flattener_options.use_sparse_entries); + AddOptionalSwitch("--enable-resource-obfuscation", + "Enables obfuscation of key string pool to single value", + &options_.table_flattener_options.collapse_key_stringpool); + AddOptionalSwitch("-v", "Enables verbose logging", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + OptimizeOptions options_; + + Maybe<std::string> config_path_; + Maybe<std::string> whitelist_path_; + Maybe<std::string> resources_config_path_; + Maybe<std::string> target_densities_; + std::vector<std::string> configs_; + std::vector<std::string> split_args_; + std::unordered_set<std::string> kept_artifacts_; + bool print_only_ = false; + bool verbose_ = false; +}; + +}// namespace aapt + +#endif //AAPT2_OPTIMIZE_H
\ No newline at end of file diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index 8b3a6701b409..c6c82b04ff2e 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -29,6 +29,7 @@ #include "util/Util.h" using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { @@ -145,7 +146,7 @@ static xml::NamespaceDecl CreateAndroidNamespaceDecl() { // // See frameworks/base/core/java/android/content/pm/PackageParser.java which // checks this at runtime. -static std::string MakePackageSafeName(const std::string &name) { +std::string MakePackageSafeName(const std::string &name) { std::string result(name); bool first = true; for (char &c : result) { @@ -168,6 +169,7 @@ static std::string MakePackageSafeName(const std::string &name) { std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, const SplitConstraints& constraints) { const ResourceId kVersionCode(0x0101021b); + const ResourceId kVersionCodeMajor(0x01010576); const ResourceId kRevisionCode(0x010104d5); const ResourceId kHasCode(0x0101000c); @@ -184,6 +186,14 @@ std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, util::make_unique<BinaryPrimitive>(android::Res_value::TYPE_INT_DEC, version_code)}); } + if (app_info.version_code_major) { + const uint32_t version_code_major = app_info.version_code_major.value(); + manifest_el->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, "versionCodeMajor", std::to_string(version_code_major), + CreateAttributeWithId(kVersionCodeMajor), + util::make_unique<BinaryPrimitive>(android::Res_value::TYPE_INT_DEC, version_code_major)}); + } + if (app_info.revision_code) { const uint32_t revision_code = app_info.revision_code.value(); manifest_el->attributes.push_back(xml::Attribute{ @@ -355,6 +365,17 @@ Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, app_info.version_code = maybe_code.value(); } + if (const xml::Attribute* version_code_major_attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor")) { + Maybe<uint32_t> maybe_code = ExtractCompiledInt(*version_code_major_attr, &error_msg); + if (!maybe_code) { + diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number)) + << "invalid android:versionCodeMajor: " << error_msg); + return {}; + } + app_info.version_code_major = maybe_code.value(); + } + if (const xml::Attribute* revision_code_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "revisionCode")) { Maybe<uint32_t> maybe_code = ExtractCompiledInt(*revision_code_attr, &error_msg); @@ -391,4 +412,21 @@ Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, return app_info; } +void SetLongVersionCode(xml::Element* manifest, uint64_t version) { + // Write the low bits of the version code to android:versionCode + auto version_code = manifest->FindOrCreateAttribute(xml::kSchemaAndroid, "versionCode"); + version_code->value = StringPrintf("0x%08x", (uint32_t) (version & 0xffffffff)); + version_code->compiled_value = ResourceUtils::TryParseInt(version_code->value); + + auto version_high = (uint32_t) (version >> 32); + if (version_high != 0) { + // Write the high bits of the version code to android:versionCodeMajor + auto version_major = manifest->FindOrCreateAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + version_major->value = StringPrintf("0x%08x", version_high); + version_major->compiled_value = ResourceUtils::TryParseInt(version_major->value); + } else { + manifest->RemoveAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + } +} + } // namespace aapt diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h index 7611c1526104..cf1443e30e1f 100644 --- a/tools/aapt2/cmd/Util.h +++ b/tools/aapt2/cmd/Util.h @@ -60,6 +60,18 @@ std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, IDiagnostics* diag); +// Returns a copy of 'name' which conforms to the regex '[a-zA-Z]+[a-zA-Z0-9_]*' by +// replacing nonconforming characters with underscores. +// +// See frameworks/base/core/java/android/content/pm/PackageParser.java which +// checks this at runtime. +std::string MakePackageSafeName(const std::string &name); + +// Sets the versionCode and versionCodeMajor attributes to the version code. Attempts to encode the +// version code using the versionCode attribute only, and encodes using both versionCode and +// versionCodeMajor if the version code requires more than 32 bits. +void SetLongVersionCode(xml::Element* manifest, uint64_t version_code); + } // namespace aapt #endif /* AAPT_SPLIT_UTIL_H */ diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 0c527f6a90dc..b9fb5b2868a7 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -18,6 +18,7 @@ #include "AppInfo.h" #include "split/TableSplitter.h" +#include "test/Builders.h" #include "test/Test.h" namespace aapt { @@ -36,4 +37,51 @@ TEST(UtilTest, SplitNamesAreSanitized) { EXPECT_EQ(root->FindAttribute("", "targetConfig")->value, "b+sr+Latn,en-rUS-land"); } +TEST (UtilTest, LongVersionCodeDefined) { + auto doc = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.aapt.test" android:versionCode="0x1" android:versionCodeMajor="0x1"> + </manifest>)"); + SetLongVersionCode(doc->root.get(), 42); + + auto version_code = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_NE(version_code, nullptr); + EXPECT_EQ(version_code->value, "0x0000002a"); + + ASSERT_NE(version_code->compiled_value, nullptr); + auto compiled_version_code = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + ASSERT_NE(compiled_version_code, nullptr); + EXPECT_EQ(compiled_version_code->value.data, 42U); + + auto version_code_major = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + EXPECT_EQ(version_code_major, nullptr); +} + +TEST (UtilTest, LongVersionCodeUndefined) { + auto doc = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.aapt.test"> + </manifest>)"); + SetLongVersionCode(doc->root.get(), 420000000000); + + auto version_code = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_NE(version_code, nullptr); + EXPECT_EQ(version_code->value, "0xc9f36800"); + + ASSERT_NE(version_code->compiled_value, nullptr); + auto compiled_version_code = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + ASSERT_NE(compiled_version_code, nullptr); + EXPECT_EQ(compiled_version_code->value.data, 0xc9f36800); + + auto version_code_major = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_NE(version_code_major, nullptr); + EXPECT_EQ(version_code_major->value, "0x00000061"); + + ASSERT_NE(version_code_major->compiled_value, nullptr); + auto compiled_version_code_major = ValueCast<BinaryPrimitive>( + version_code_major->compiled_value.get()); + ASSERT_NE(compiled_version_code_major, nullptr); + EXPECT_EQ(compiled_version_code_major->value.data, 0x61); +} + } // namespace aapt diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 6b07b1e96261..d1a70a75a44e 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -256,9 +256,20 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res styleable_attr.field_name = TransformNestedAttr(attr.name.value(), array_field_name, package_name_to_generate); + Reference ref = attr; + if (attr.name.value().package.empty()) { + + // If the resource does not have a package name, set the package to the unmangled package name + // of the styleable declaration because attributes without package names would have been + // declared in the same package as the styleable. + ref.name = ResourceName(package_name_to_generate, ref.name.value().type, + ref.name.value().entry); + } + // Look up the symbol so that we can write out in the comments what are possible legal values // for this attribute. - const SymbolTable::Symbol* symbol = context_->GetExternalSymbols()->FindByReference(attr); + const SymbolTable::Symbol* symbol = context_->GetExternalSymbols()->FindByReference(ref); + if (symbol && symbol->attribute) { // Copy the symbol data structure because the returned instance can be destroyed. styleable_attr.symbol = *symbol; @@ -303,7 +314,7 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res const ResourceName& attr_name = entry.attr_ref->name.value(); styleable_comment << "<tr><td><code>{@link #" << entry.field_name << " " << (!attr_name.package.empty() ? attr_name.package - : context_->GetCompilationPackage()) + : package_name_to_generate) << ":" << attr_name.entry << "}</code></td>"; // Only use the comment up until the first '.'. This is to stay compatible with @@ -347,7 +358,9 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res } // Add the Styleable array to the Styleable class. - out_class_def->AddMember(std::move(array_def)); + if (out_class_def != nullptr) { + out_class_def->AddMember(std::move(array_def)); + } // Now we emit the indices into the array. for (size_t i = 0; i < attr_count; i++) { @@ -372,7 +385,7 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res StringPiece package_name = attr_name.package; if (package_name.empty()) { - package_name = context_->GetCompilationPackage(); + package_name = package_name_to_generate; } std::unique_ptr<IntMember> index_member = @@ -578,7 +591,6 @@ bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate, if (out_r_txt != nullptr) { r_txt_printer = util::make_unique<Printer>(out_r_txt); } - // Generate an onResourcesLoaded() callback if requested. if (out != nullptr && options_.rewrite_callback_options) { rewrite_method = diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index e449546f9399..fa208be120ed 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -107,6 +107,55 @@ TEST(JavaClassGeneratorTest, CorrectPackageNameIsUsed) { EXPECT_THAT(output, Not(HasSubstr("com_foo$two"))); } +TEST(JavaClassGeneratorTest, StyleableAttributesWithDifferentPackageName) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .SetPackageId("app", 0x7f) + .AddValue("app:attr/foo", ResourceId(0x7f010000), + test::AttributeBuilder().Build()) + .AddValue("app:attr/bar", ResourceId(0x7f010001), + test::AttributeBuilder().Build()) + .AddValue("android:attr/baz", ResourceId(0x01010000), + test::AttributeBuilder().Build()) + .AddValue("app:styleable/MyStyleable", ResourceId(0x7f030000), + test::StyleableBuilder() + .AddItem("app:attr/foo", ResourceId(0x7f010000)) + .AddItem("attr/bar", ResourceId(0x7f010001)) + .AddItem("android:attr/baz", ResourceId(0x01010000)) + .Build()) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(table.get())) + .SetNameManglerPolicy(NameManglerPolicy{"custom"}) + .SetCompilationPackage("custom") + .Build(); + JavaClassGenerator generator(context.get(), table.get(), {}); + + std::string output; + StringOutputStream out(&output); + EXPECT_TRUE(generator.Generate("app", &out)); + out.Flush(); + + EXPECT_THAT(output, Not(HasSubstr("public static final int baz=0x01010000;"))); + EXPECT_THAT(output, HasSubstr("public static final int foo=0x7f010000;")); + EXPECT_THAT(output, HasSubstr("public static final int bar=0x7f010001;")); + + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_android_baz=0;")); + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_foo=1;")); + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_bar=2;")); + + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_android_baz android:baz")); + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_foo app:foo")); + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_bar app:bar")); + + EXPECT_THAT(output, HasSubstr("@link android.R.attr#baz")); + EXPECT_THAT(output, HasSubstr("@link app.R.attr#foo")); + EXPECT_THAT(output, HasSubstr("@link app.R.attr#bar")); +} + TEST(JavaClassGeneratorTest, AttrPrivateIsWrittenAsAttr) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() @@ -438,4 +487,22 @@ TEST(JavaClassGeneratorTest, GenerateOnResourcesLoadedCallbackForSharedLibrary) EXPECT_THAT(output, HasSubstr("com.boo.R.onResourcesLoaded")); } +TEST(JavaClassGeneratorTest, OnlyGenerateRText) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddValue("android:attr/foo", ResourceId(0x01010000), util::make_unique<Attribute>()) + .AddValue("android:styleable/hey.dude", ResourceId(0x01020000), + test::StyleableBuilder() + .AddItem("android:attr/foo", ResourceId(0x01010000)) + .Build()) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder().SetPackageId(0x01).SetCompilationPackage("android").Build(); + JavaClassGenerator generator(context.get(), table.get(), {}); + + ASSERT_TRUE(generator.Generate("android", nullptr)); +} + } // namespace aapt diff --git a/tools/aapt2/java/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp index ffcef8966654..d40795accf79 100644 --- a/tools/aapt2/java/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -39,7 +39,11 @@ class BaseVisitor : public xml::Visitor { public: using xml::Visitor::Visit; - BaseVisitor(const ResourceFile& file, KeepSet* keep_set) : file_(file), keep_set_(keep_set) { + BaseVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set, "...") { + } + + BaseVisitor(const ResourceFile& file, KeepSet* keep_set, const std::string& ctor_signature) + : file_(file), keep_set_(keep_set), ctor_signature_(ctor_signature) { } void Visit(xml::Element* node) override { @@ -50,11 +54,11 @@ class BaseVisitor : public xml::Visitor { // This is a custom view, let's figure out the class name from this. std::string package = maybe_package.value().package + "." + node->name; if (util::IsJavaClassName(package)) { - AddClass(node->line_number, package); + AddClass(node->line_number, package, ctor_signature_); } } } else if (util::IsJavaClassName(node->name)) { - AddClass(node->line_number, node->name); + AddClass(node->line_number, node->name, ctor_signature_); } for (const auto& child : node->children) { @@ -74,13 +78,18 @@ class BaseVisitor : public xml::Visitor { protected: ResourceFile file_; KeepSet* keep_set_; + std::string ctor_signature_; - virtual void AddClass(size_t line_number, const std::string& class_name) { - keep_set_->AddConditionalClass({file_.name, file_.source.WithLine(line_number)}, class_name); + virtual void AddClass(size_t line_number, const std::string& class_name, + const std::string& ctor_signature) { + keep_set_->AddConditionalClass({file_.name, file_.source.WithLine(line_number)}, + {class_name, ctor_signature}); } - void AddMethod(size_t line_number, const std::string& method_name) { - keep_set_->AddMethod({file_.name, file_.source.WithLine(line_number)}, method_name); + void AddMethod(size_t line_number, const std::string& method_name, + const std::string& method_signature) { + keep_set_->AddMethod({file_.name, file_.source.WithLine(line_number)}, + {method_name, method_signature}); } void AddReference(size_t line_number, Reference* ref) { @@ -100,32 +109,39 @@ class BaseVisitor : public xml::Visitor { class LayoutVisitor : public BaseVisitor { public: - LayoutVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set) { + LayoutVisitor(const ResourceFile& file, KeepSet* keep_set) + : BaseVisitor(file, keep_set, "android.content.Context, android.util.AttributeSet") { } void Visit(xml::Element* node) override { - bool check_class = false; - bool check_name = false; + bool is_view = false; + bool is_fragment = false; if (node->namespace_uri.empty()) { if (node->name == "view") { - check_class = true; + is_view = true; } else if (node->name == "fragment") { - check_class = check_name = true; + is_fragment = true; } } else if (node->namespace_uri == xml::kSchemaAndroid) { - check_name = node->name == "fragment"; + is_fragment = node->name == "fragment"; } for (const auto& attr : node->attributes) { - if (check_class && attr.namespace_uri.empty() && attr.name == "class" && - util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); - } else if (check_name && attr.namespace_uri == xml::kSchemaAndroid && - attr.name == "name" && util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); - } else if (attr.namespace_uri == xml::kSchemaAndroid && - attr.name == "onClick") { - AddMethod(node->line_number, attr.value); + if (attr.namespace_uri.empty() && attr.name == "class") { + if (util::IsJavaClassName(attr.value)) { + if (is_view) { + AddClass(node->line_number, attr.value, + "android.content.Context, android.util.AttributeSet"); + } else if (is_fragment) { + AddClass(node->line_number, attr.value, ""); + } + } + } else if (attr.namespace_uri == xml::kSchemaAndroid && attr.name == "name") { + if (is_fragment && util::IsJavaClassName(attr.value)) { + AddClass(node->line_number, attr.value, ""); + } + } else if (attr.namespace_uri == xml::kSchemaAndroid && attr.name == "onClick") { + AddMethod(node->line_number, attr.value, "android.view.View"); } } @@ -147,9 +163,9 @@ class MenuVisitor : public BaseVisitor { if (attr.namespace_uri == xml::kSchemaAndroid) { if ((attr.name == "actionViewClass" || attr.name == "actionProviderClass") && util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); + AddClass(node->line_number, attr.value, "android.content.Context"); } else if (attr.name == "onClick") { - AddMethod(node->line_number, attr.value); + AddMethod(node->line_number, attr.value, "android.view.MenuItem"); } } } @@ -178,7 +194,7 @@ class XmlResourceVisitor : public BaseVisitor { xml::Attribute* attr = node->FindAttribute(xml::kSchemaAndroid, "fragment"); if (attr && util::IsJavaClassName(attr->value)) { - AddClass(node->line_number, attr->value); + AddClass(node->line_number, attr->value, ""); } } @@ -189,6 +205,29 @@ class XmlResourceVisitor : public BaseVisitor { DISALLOW_COPY_AND_ASSIGN(XmlResourceVisitor); }; +class NavigationVisitor : public BaseVisitor { + public: + NavigationVisitor(const ResourceFile& file, KeepSet* keep_set, const std::string& package) + : BaseVisitor(file, keep_set), package_(package) { + } + + void Visit(xml::Element* node) override { + const auto& attr = node->FindAttribute(xml::kSchemaAndroid, "name"); + if (attr != nullptr && !attr->value.empty()) { + std::string name = (attr->value[0] == '.') ? package_ + attr->value : attr->value; + if (util::IsJavaClassName(name)) { + AddClass(node->line_number, name, "..."); + } + } + + BaseVisitor::Visit(node); + } + + private: + DISALLOW_COPY_AND_ASSIGN(NavigationVisitor); + const std::string package_; +}; + class TransitionVisitor : public BaseVisitor { public: TransitionVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set) { @@ -200,7 +239,8 @@ class TransitionVisitor : public BaseVisitor { if (check_class) { xml::Attribute* attr = node->FindAttribute({}, "class"); if (attr && util::IsJavaClassName(attr->value)) { - AddClass(node->line_number, attr->value); + AddClass(node->line_number, attr->value, + "android.content.Context, android.util.AttributeSet"); } } @@ -231,7 +271,14 @@ class ManifestVisitor : public BaseVisitor { if (attr) { Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); if (result) { - AddClass(node->line_number, result.value()); + AddClass(node->line_number, result.value(), ""); + } + } + attr = node->FindAttribute(xml::kSchemaAndroid, "appComponentFactory"); + if (attr) { + Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); + if (result) { + AddClass(node->line_number, result.value(), ""); } } if (main_dex_only_) { @@ -262,7 +309,7 @@ class ManifestVisitor : public BaseVisitor { if (get_name) { Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); if (result) { - AddClass(node->line_number, result.value()); + AddClass(node->line_number, result.value(), ""); } } } @@ -270,7 +317,8 @@ class ManifestVisitor : public BaseVisitor { BaseVisitor::Visit(node); } - virtual void AddClass(size_t line_number, const std::string& class_name) override { + virtual void AddClass(size_t line_number, const std::string& class_name, + const std::string& ctor_signature) override { keep_set_->AddManifestClass({file_.name, file_.source.WithLine(line_number)}, class_name); } @@ -291,7 +339,7 @@ bool CollectProguardRulesForManifest(xml::XmlResource* res, KeepSet* keep_set, b return false; } -bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set) { +bool CollectProguardRules(IAaptContext* context_, xml::XmlResource* res, KeepSet* keep_set) { if (!res->root) { return false; } @@ -309,6 +357,12 @@ bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set) { break; } + case ResourceType::kNavigation: { + NavigationVisitor visitor(res->file, keep_set, context_->GetCompilationPackage()); + res->root->Accept(&visitor); + break; + } + case ResourceType::kTransition: { TransitionVisitor visitor(res->file, keep_set); res->root->Accept(&visitor); @@ -336,7 +390,7 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first).Println(" { <init>(); }"); } for (const auto& entry : keep_set.conditional_class_set_) { @@ -352,13 +406,15 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { printer.Print("-if class **.R$layout { int ") .Print(JavaClassGenerator::TransformToFieldName(location.name.entry)) .Println("; }"); - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first.name).Print(" { <init>(") + .Print(entry.first.signature).Println("); }"); } } else { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first.name).Print(" { <init>(") + .Print(entry.first.signature).Println("); }"); } printer.Println(); } @@ -367,7 +423,8 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keepclassmembers class * { *** ").Print(entry.first).Println("(...); }"); + printer.Print("-keepclassmembers class * { *** ").Print(entry.first.name) + .Print("(").Print(entry.first.signature).Println("); }"); printer.Println(); } } diff --git a/tools/aapt2/java/ProguardRules.h b/tools/aapt2/java/ProguardRules.h index 46827ee7cf93..01dad0b08aea 100644 --- a/tools/aapt2/java/ProguardRules.h +++ b/tools/aapt2/java/ProguardRules.h @@ -40,6 +40,11 @@ struct UsageLocation { Source source; }; +struct NameAndSignature { + std::string name; + std::string signature; +}; + class KeepSet { public: KeepSet() = default; @@ -51,12 +56,13 @@ class KeepSet { manifest_class_set_[class_name].insert(file); } - inline void AddConditionalClass(const UsageLocation& file, const std::string& class_name) { - conditional_class_set_[class_name].insert(file); + inline void AddConditionalClass(const UsageLocation& file, + const NameAndSignature& class_and_signature) { + conditional_class_set_[class_and_signature].insert(file); } - inline void AddMethod(const UsageLocation& file, const std::string& method_name) { - method_set_[method_name].insert(file); + inline void AddMethod(const UsageLocation& file, const NameAndSignature& name_and_signature) { + method_set_[name_and_signature].insert(file); } inline void AddReference(const UsageLocation& file, const ResourceName& resource_name) { @@ -71,15 +77,15 @@ class KeepSet { bool conditional_keep_rules_ = false; std::map<std::string, std::set<UsageLocation>> manifest_class_set_; - std::map<std::string, std::set<UsageLocation>> method_set_; - std::map<std::string, std::set<UsageLocation>> conditional_class_set_; + std::map<NameAndSignature, std::set<UsageLocation>> method_set_; + std::map<NameAndSignature, std::set<UsageLocation>> conditional_class_set_; std::map<ResourceName, std::set<UsageLocation>> reference_set_; }; bool CollectProguardRulesForManifest(xml::XmlResource* res, KeepSet* keep_set, bool main_dex_only = false); -bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set); +bool CollectProguardRules(IAaptContext* context, xml::XmlResource* res, KeepSet* keep_set); bool CollectResourceReferences(IAaptContext* context, ResourceTable* table, KeepSet* keep_set); @@ -100,6 +106,20 @@ inline int operator<(const UsageLocation& lhs, const UsageLocation& rhs) { return lhs.name.compare(rhs.name); } +// +// NameAndSignature implementation. +// + +inline bool operator<(const NameAndSignature& lhs, const NameAndSignature& rhs) { + if (lhs.name < rhs.name) { + return true; + } + if (lhs.name == rhs.name) { + return lhs.signature < rhs.signature; + } + return false; +} + } // namespace proguard } // namespace aapt diff --git a/tools/aapt2/java/ProguardRules_test.cpp b/tools/aapt2/java/ProguardRules_test.cpp index 37d1a5fbaeb8..83c72d89bb62 100644 --- a/tools/aapt2/java/ProguardRules_test.cpp +++ b/tools/aapt2/java/ProguardRules_test.cpp @@ -34,6 +34,37 @@ std::string GetKeepSetString(const proguard::KeepSet& set) { return out; } +TEST(ProguardRulesTest, ManifestRuleDefaultConstructorOnly) { + std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application + android:appComponentFactory="com.foo.BarAppComponentFactory" + android:backupAgent="com.foo.BarBackupAgent" + android:name="com.foo.BarApplication" + > + <activity android:name="com.foo.BarActivity"/> + <service android:name="com.foo.BarService"/> + <receiver android:name="com.foo.BarReceiver"/> + <provider android:name="com.foo.BarProvider"/> + </application> + <instrumentation android:name="com.foo.BarInstrumentation"/> + </manifest>)"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRulesForManifest(manifest.get(), &set, false)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarAppComponentFactory { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarBackupAgent { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarApplication { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarActivity { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarService { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarReceiver { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarProvider { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarInstrumentation { <init>(); }")); +} + TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"( @@ -42,11 +73,11 @@ TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { @@ -56,11 +87,11 @@ TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { @@ -72,12 +103,40 @@ TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(); }")); +} + +TEST(ProguardRulesTest, NavigationFragmentNameAndClassRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("com.base").Build(); + std::unique_ptr<xml::XmlResource> navigation = test::BuildXmlDom(R"( + <navigation + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <custom android:id="@id/foo" + android:name="com.package.Foo"/> + <fragment android:id="@id/bar" + android:name="com.package.Bar"> + <nested android:id="@id/nested" + android:name=".Nested"/> + </fragment> + </navigation> + )"); + + navigation->file.name = test::ParseNameOrDie("navigation/graph.xml"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), navigation.get(), &set)); + + std::string actual = GetKeepSetString(set); + EXPECT_THAT(actual, HasSubstr("-keep class com.package.Foo { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.package.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.base.Nested { <init>(...); }")); } TEST(ProguardRulesTest, CustomViewRulesAreEmitted) { @@ -89,11 +148,12 @@ TEST(ProguardRulesTest, CustomViewRulesAreEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) { @@ -125,16 +185,16 @@ TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) { ASSERT_TRUE(xml_linker.Consume(context.get(), foo_layout.get())); proguard::KeepSet set = proguard::KeepSet(true); - ASSERT_TRUE(proguard::CollectProguardRules(bar_layout.get(), &set)); - ASSERT_TRUE(proguard::CollectProguardRules(foo_layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), bar_layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), foo_layout.get(), &set)); std::string actual = GetKeepSetString(set); EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); EXPECT_THAT(actual, HasSubstr("int foo")); EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); } TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) { @@ -147,15 +207,15 @@ TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) { proguard::KeepSet set = proguard::KeepSet(true); set.AddReference({test::ParseNameOrDie("layout/bar"), {}}, layout->file.name); - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); EXPECT_THAT(actual, HasSubstr("int foo")); EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); } TEST(ProguardRulesTest, NonLayoutReferencesAreUnconditional) { @@ -168,12 +228,13 @@ TEST(ProguardRulesTest, NonLayoutReferencesAreUnconditional) { proguard::KeepSet set = proguard::KeepSet(true); set.AddReference({test::ParseNameOrDie("style/MyStyle"), {}}, layout->file.name); - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); EXPECT_THAT(actual, Not(HasSubstr("-if"))); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { @@ -184,11 +245,12 @@ TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("bar_method")); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** bar_method(android.view.View); }")); } TEST(ProguardRulesTest, MenuRulesAreEmitted) { @@ -203,14 +265,49 @@ TEST(ProguardRulesTest, MenuRulesAreEmitted) { menu->file.name = test::ParseNameOrDie("menu/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(menu.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), menu.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("on_click")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** on_click(android.view.MenuItem); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(android.content.Context); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(android.content.Context); }")); EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat"))); } +TEST(ProguardRulesTest, TransitionPathMotionRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> transition = test::BuildXmlDom(R"( + <changeBounds> + <pathMotion class="com.foo.Bar"/> + </changeBounds>)"); + transition->file.name = test::ParseNameOrDie("transition/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transition.get(), &set)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); +} + +TEST(ProguardRulesTest, TransitionRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> transitionSet = test::BuildXmlDom(R"( + <transitionSet> + <transition class="com.foo.Bar"/> + </transitionSet>)"); + transitionSet->file.name = test::ParseNameOrDie("transition/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transitionSet.get(), &set)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); +} + } // namespace aapt diff --git a/tools/aapt2/jni/aapt2_jni.cpp b/tools/aapt2/jni/aapt2_jni.cpp index ad5ad4c336e5..ba9646f9aeb4 100644 --- a/tools/aapt2/jni/aapt2_jni.cpp +++ b/tools/aapt2/jni/aapt2_jni.cpp @@ -25,15 +25,12 @@ #include "ScopedUtfChars.h" #include "Diagnostics.h" +#include "cmd/Compile.h" +#include "cmd/Link.h" #include "util/Util.h" using android::StringPiece; -namespace aapt { -extern int Compile(const std::vector<StringPiece>& args, IDiagnostics* iDiagnostics); -extern int Link(const std::vector<StringPiece>& args, IDiagnostics* iDiagnostics); -} - /* * Converts a java List<String> into C++ vector<ScopedUtfChars>. */ @@ -126,7 +123,7 @@ JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeCompile( list_to_utfchars(env, arguments_obj); std::vector<StringPiece> compile_args = extract_pieces(compile_args_jni); JniDiagnostics diagnostics(env, diagnostics_obj); - return aapt::Compile(compile_args, &diagnostics); + return aapt::CompileCommand(&diagnostics).Execute(compile_args, &std::cerr); } JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeLink(JNIEnv* env, @@ -137,7 +134,7 @@ JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeLink(JNIEnv* list_to_utfchars(env, arguments_obj); std::vector<StringPiece> link_args = extract_pieces(link_args_jni); JniDiagnostics diagnostics(env, diagnostics_obj); - return aapt::Link(link_args, &diagnostics); + return aapt::LinkCommand(&diagnostics).Execute(link_args, &std::cerr); } JNIEXPORT void JNICALL Java_com_android_tools_aapt2_Aapt2Jni_ping( diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index ee4e70288994..55a32c8b512c 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -282,6 +282,17 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, } } + if (options_.version_code_major_default) { + if (options_.replace_version) { + el->RemoveAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + } + if (el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor") == nullptr) { + el->attributes.push_back( + xml::Attribute{xml::kSchemaAndroid, "versionCodeMajor", + options_.version_code_major_default.value()}); + } + } + if (el->FindAttribute("", "platformBuildVersionCode") == nullptr) { auto versionCode = el->FindAttribute(xml::kSchemaAndroid, "versionCode"); if (versionCode != nullptr) { diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h index 98d06fd776d7..3ef57d0d0e42 100644 --- a/tools/aapt2/link/ManifestFixer.h +++ b/tools/aapt2/link/ManifestFixer.h @@ -52,6 +52,10 @@ struct ManifestFixerOptions { // replace_version is set. Maybe<std::string> version_code_default; + // The version code to set if 'android:versionCodeMajor' is not defined in <manifest> or if + // replace_version is set. + Maybe<std::string> version_code_major_default; + // The version of the framework being compiled against to set for 'android:compileSdkVersion' in // the <manifest> tag. Maybe<std::string> compile_sdk_version; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 5bc004d62ff2..adea6273bc8b 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -329,6 +329,7 @@ TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { ManifestFixerOptions options; options.version_name_default = std::string("Beta"); options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" @@ -347,136 +348,199 @@ TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); ASSERT_THAT(attr, NotNull()); EXPECT_THAT(attr->value, StrEq("0x10000000")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } TEST_F(ManifestFixerTest, DontUseDefaultVersionNameAndCode) { -ManifestFixerOptions options; -options.version_name_default = std::string("Beta"); -options.version_code_default = std::string("0x10000000"); + ManifestFixerOptions options; + options.version_name_default = std::string("Beta"); + options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); + + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); +} -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( +TEST_F(ManifestFixerTest, ReplaceVersionNameAndCode) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_name_default = std::string("Beta"); + options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android" - android:versionCode="0x20000000" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Beta")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x10000000")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } -TEST_F(ManifestFixerTest, ReplaceVersionNameAndCode) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_name_default = std::string("Beta"); -options.version_code_default = std::string("0x10000000"); +TEST_F(ManifestFixerTest, ReplaceVersionName) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_name_default = std::string("Beta"); + -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android" - android:versionCode="0x20000000" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Beta")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Beta")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x10000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } -TEST_F(ManifestFixerTest, ReplaceVersionName) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_name_default = std::string("Beta"); +TEST_F(ManifestFixerTest, ReplaceVersionCode) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_code_default = std::string("0x10000000"); -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( - <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Beta")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x10000000")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } -TEST_F(ManifestFixerTest, ReplaceVersionCode) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_code_default = std::string("0x10000000"); +TEST_F(ManifestFixerTest, ReplaceVersionCodeMajor) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_code_major_default = std::string("0x20000000"); -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x10000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } TEST_F(ManifestFixerTest, DontReplaceVersionNameOrCode) { -ManifestFixerOptions options; -options.replace_version = true; + ManifestFixerOptions options; + options.replace_version = true; -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } TEST_F(ManifestFixerTest, EnsureManifestAttributesAreTyped) { diff --git a/tools/aapt2/link/NoDefaultResourceRemover.cpp b/tools/aapt2/link/NoDefaultResourceRemover.cpp index cfb4b26aba02..5173b8541943 100644 --- a/tools/aapt2/link/NoDefaultResourceRemover.cpp +++ b/tools/aapt2/link/NoDefaultResourceRemover.cpp @@ -22,17 +22,7 @@ namespace aapt { -static bool IsDefaultConfigRequired(const ConfigDescription& config) { - // We don't want to be overzealous with resource removal, so have strict requirements. - // If a resource defines a value for a locale-only configuration, the default configuration is - // required. - if (ConfigDescription::DefaultConfig().diff(config) == ConfigDescription::CONFIG_LOCALE) { - return true; - } - return false; -} - -static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry) { +static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry, int minSdk) { if (entry->visibility.level == Visibility::Level::kPublic) { // Removing a public API without the developer knowing is bad, so just leave this here for now. return true; @@ -44,22 +34,44 @@ static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry) { } // There is no default value defined, check if removal is required. + bool defaultRequired = false; for (const auto& config_value : entry->values) { - if (IsDefaultConfigRequired(config_value->config)) { - return false; + const int config = ConfigDescription::DefaultConfig().diff(config_value->config); + // If a resource defines a value for a locale-only configuration, the default configuration is + // required. + if (config == ConfigDescription::CONFIG_LOCALE) { + defaultRequired = true; + } + // If a resource defines a version-only config, the config value can be used as a default if + // the version is at most the minimum sdk version + else if (config == ConfigDescription::CONFIG_VERSION + && config_value->config.sdkVersion <= minSdk) { + return true; + } + // If a resource defines a value for a density only configuration, then that value could be used + // as a default and the entry should not be removed + else if (config == ConfigDescription::CONFIG_DENSITY + || (config == (ConfigDescription::CONFIG_DENSITY | ConfigDescription::CONFIG_VERSION) + && config_value->config.sdkVersion <= minSdk)) { + return true; } } - return true; + + return !defaultRequired; } bool NoDefaultResourceRemover::Consume(IAaptContext* context, ResourceTable* table) { - const ConfigDescription default_config = ConfigDescription::DefaultConfig(); for (auto& pkg : table->packages) { for (auto& type : pkg->types) { + // Gather the entries without defaults that must be removed + const int minSdk = context->GetMinSdkVersion(); const auto end_iter = type->entries.end(); - const auto new_end_iter = - std::stable_partition(type->entries.begin(), end_iter, KeepResource); - for (auto iter = new_end_iter; iter != end_iter; ++iter) { + const auto remove_iter = std::stable_partition(type->entries.begin(), end_iter, + [&minSdk](const std::unique_ptr<ResourceEntry>& entry) -> bool { + return KeepResource(entry, minSdk); + }); + + for (auto iter = remove_iter; iter != end_iter; ++iter) { const ResourceName name(pkg->name, type->type, (*iter)->name); IDiagnostics* diag = context->GetDiagnostics(); diag->Warn(DiagMessage() << "removing resource " << name @@ -74,7 +86,7 @@ bool NoDefaultResourceRemover::Consume(IAaptContext* context, ResourceTable* tab } } - type->entries.erase(new_end_iter, type->entries.end()); + type->entries.erase(remove_iter, end_iter); } } return true; diff --git a/tools/aapt2/link/NoDefaultResourceRemover_test.cpp b/tools/aapt2/link/NoDefaultResourceRemover_test.cpp index 943709a2af12..d129c9ac8db7 100644 --- a/tools/aapt2/link/NoDefaultResourceRemover_test.cpp +++ b/tools/aapt2/link/NoDefaultResourceRemover_test.cpp @@ -46,4 +46,64 @@ TEST(NoDefaultResourceRemoverTest, RemoveEntryWithNoDefaultAndOnlyLocales) { EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:string/baz"))); } +TEST(NoDefaultResourceRemoverTest, KeepEntryWithLocalesAndDensities) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(26).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("mdpi")) // v4 + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("xxxhdpi")) // v4 + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("w600dp-xhdpi")) // v13 + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep1"))); + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep2"))); + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + +TEST(NoDefaultResourceRemoverTest, RemoveEntryWithLocalesAndDensitiesLowVersion) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(3).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("mdpi")) // v4 + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + +TEST(NoDefaultResourceRemoverTest, KeepEntryWithVersion) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(8).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("v8")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("v9")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep1"))); + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + } // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index e819f51a5634..afb8ae097449 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -102,7 +102,17 @@ static bool MergeType(IAaptContext* context, const Source& src, ResourceTableTyp } static bool MergeEntry(IAaptContext* context, const Source& src, bool overlay, - ResourceEntry* dst_entry, ResourceEntry* src_entry) { + ResourceEntry* dst_entry, ResourceEntry* src_entry, + bool strict_visibility) { + if (strict_visibility + && dst_entry->visibility.level != Visibility::Level::kUndefined + && src_entry->visibility.level != dst_entry->visibility.level) { + context->GetDiagnostics()->Error( + DiagMessage(src) << "cannot merge resource '" << dst_entry->name << "' with conflicting visibilities: " + << "public and private"); + return false; + } + // Copy over the strongest visibility. if (src_entry->visibility.level > dst_entry->visibility.level) { // Only copy the ID if the source is public, or else the ID is meaningless. @@ -234,7 +244,7 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, continue; } - if (!MergeEntry(context_, src, overlay, dst_entry, src_entry.get())) { + if (!MergeEntry(context_, src, overlay, dst_entry, src_entry.get(), options_.strict_visibility)) { error = true; continue; } @@ -271,8 +281,17 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, dst_config_value->value = std::move(new_file_ref); } else { + Maybe<std::string> original_comment = (dst_config_value->value) + ? dst_config_value->value->GetComment() : Maybe<std::string>(); + dst_config_value->value = std::unique_ptr<Value>( src_config_value->value->Clone(&master_table_->string_pool)); + + // Keep the comment from the original resource and ignore all comments from overlaying + // resources + if (overlay && original_comment) { + dst_config_value->value->SetComment(original_comment.value()); + } } } } diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h index 47e23dded26f..24c5e1329244 100644 --- a/tools/aapt2/link/TableMerger.h +++ b/tools/aapt2/link/TableMerger.h @@ -35,6 +35,8 @@ namespace aapt { struct TableMergerOptions { // If true, resources in overlays can be added without previously having existed. bool auto_add_overlay = false; + // If true, resource overlays with conflicting visibility are not allowed. + bool strict_visibility = false; }; // TableMerger takes resource tables and merges all packages within the tables that have the same diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp index 34461c6b467d..79a734bffabd 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -178,6 +178,49 @@ TEST_F(TableMergerTest, OverrideResourceWithOverlay) { Pointee(Field(&BinaryPrimitive::value, Field(&android::Res_value::data, Eq(0u))))); } +TEST_F(TableMergerTest, DoNotOverrideResourceComment) { + std::unique_ptr<Value> foo_original = ResourceUtils::TryParseBool("true"); + foo_original->SetComment(android::StringPiece("Original foo comment")); + std::unique_ptr<Value> bar_original = ResourceUtils::TryParseBool("true"); + + std::unique_ptr<Value> foo_overlay = ResourceUtils::TryParseBool("false"); + foo_overlay->SetComment(android::StringPiece("Overlay foo comment")); + std::unique_ptr<Value> bar_overlay = ResourceUtils::TryParseBool("false"); + bar_overlay->SetComment(android::StringPiece("Overlay bar comment")); + std::unique_ptr<Value> baz_overlay = ResourceUtils::TryParseBool("false"); + baz_overlay->SetComment(android::StringPiece("Overlay baz comment")); + + std::unique_ptr<ResourceTable> base = + test::ResourceTableBuilder() + .SetPackageId("", 0x00) + .AddValue("bool/foo", std::move(foo_original)) + .AddValue("bool/bar", std::move(bar_original)) + .Build(); + + std::unique_ptr<ResourceTable> overlay = + test::ResourceTableBuilder() + .SetPackageId("", 0x00) + .AddValue("bool/foo", std::move(foo_overlay)) + .AddValue("bool/bar", std::move(bar_overlay)) + .AddValue("bool/baz", std::move(baz_overlay)) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + + ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); + ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); + + BinaryPrimitive* foo = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/foo"); + EXPECT_THAT(foo, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Original foo comment")))); + BinaryPrimitive* bar = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/bar"); + EXPECT_THAT(bar, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("")))); + BinaryPrimitive* baz = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/baz"); + EXPECT_THAT(baz, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Overlay baz comment")))); +} + TEST_F(TableMergerTest, OverrideSameResourceIdsWithOverlay) { std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() @@ -241,6 +284,37 @@ TEST_F(TableMergerTest, FailToOverrideConflictingEntryIdsWithOverlay) { ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); } +TEST_F(TableMergerTest, FailConflictingVisibility) { + std::unique_ptr<ResourceTable> base = + test::ResourceTableBuilder() + .SetPackageId("", 0x7f) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) + .Build(); + std::unique_ptr<ResourceTable> overlay = + test::ResourceTableBuilder() + .SetPackageId("", 0x7f) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPrivate) + .Build(); + + // It should fail if the "--strict-visibility" flag is set. + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = false; + options.strict_visibility = true; + TableMerger merger(context_.get(), &final_table, options); + + ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); + ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); + + // But it should still pass if the flag is not set. + ResourceTable final_table2; + options.strict_visibility = false; + TableMerger merger2(context_.get(), &final_table2, options); + + ASSERT_TRUE(merger2.Merge({}, base.get(), false /*overlay*/)); + ASSERT_TRUE(merger2.Merge({}, overlay.get(), true /*overlay*/)); +} + TEST_F(TableMergerTest, MergeAddResourceFromOverlay) { std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder().SetPackageId("", 0x7f).Build(); diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp index 588b3316e6fa..e92c121272eb 100644 --- a/tools/aapt2/optimize/MultiApkGenerator.cpp +++ b/tools/aapt2/optimize/MultiApkGenerator.cpp @@ -26,6 +26,7 @@ #include "ResourceUtils.h" #include "ValueVisitor.h" #include "configuration/ConfigurationParser.h" +#include "cmd/Util.h" #include "filter/AbiFilter.h" #include "filter/Filter.h" #include "format/Archive.h" @@ -265,7 +266,7 @@ bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact, // Make sure the first element is <manifest> with package attribute. xml::Element* manifest_el = manifest->root.get(); - if (manifest_el == nullptr) { + if (!manifest_el) { return false; } @@ -274,21 +275,35 @@ bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact, return false; } - // Update the versionCode attribute. - xml::Attribute* versionCode = manifest_el->FindAttribute(kSchemaAndroid, "versionCode"); - if (versionCode == nullptr) { + // Retrieve the versionCode attribute. + auto version_code = manifest_el->FindAttribute(kSchemaAndroid, "versionCode"); + if (!version_code) { diag->Error(DiagMessage(manifest->file.source) << "manifest must have a versionCode attribute"); return false; } - auto* compiled_version = ValueCast<BinaryPrimitive>(versionCode->compiled_value.get()); - if (compiled_version == nullptr) { + auto version_code_value = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + if (!version_code_value) { diag->Error(DiagMessage(manifest->file.source) << "versionCode is invalid"); return false; } - int new_version = compiled_version->value.data + artifact.version; - versionCode->compiled_value = ResourceUtils::TryParseInt(std::to_string(new_version)); + // Retrieve the versionCodeMajor attribute. + auto version_code_major = manifest_el->FindAttribute(kSchemaAndroid, "versionCodeMajor"); + BinaryPrimitive* version_code_major_value = nullptr; + if (version_code_major) { + version_code_major_value = ValueCast<BinaryPrimitive>(version_code_major->compiled_value.get()); + if (!version_code_major_value) { + diag->Error(DiagMessage(manifest->file.source) << "versionCodeMajor is invalid"); + return false; + } + } + + // Calculate and set the updated version code + uint64_t major = (version_code_major_value) + ? ((uint64_t) version_code_major_value->value.data) << 32 : 0; + uint64_t new_version = (major | version_code_value->value.data) + artifact.version; + SetLongVersionCode(manifest_el, new_version); // Check to see if the minSdkVersion needs to be updated. if (artifact.android_sdk) { diff --git a/tools/aapt2/optimize/ResourceFilter.cpp b/tools/aapt2/optimize/ResourceFilter.cpp new file mode 100644 index 000000000000..250b65197a7d --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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 "optimize/ResourceFilter.h" + +#include "ResourceTable.h" + +namespace aapt { + +ResourceFilter::ResourceFilter(const std::unordered_set<ResourceName>& blacklist) + : blacklist_(blacklist) { +} + +bool ResourceFilter::Consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto it = type->entries.begin(); it != type->entries.end(); ) { + ResourceName resource = ResourceName({}, type->type, (*it)->name); + if (blacklist_.find(resource) != blacklist_.end()) { + it = type->entries.erase(it); + } else { + ++it; + } + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/optimize/ResourceFilter.h b/tools/aapt2/optimize/ResourceFilter.h new file mode 100644 index 000000000000..d4baf654b0ff --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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_OPTIMIZE_RESOURCEFILTER_H +#define AAPT_OPTIMIZE_RESOURCEFILTER_H + +#include "android-base/macros.h" + +#include "process/IResourceTableConsumer.h" + +#include <unordered_set> + +namespace aapt { + +// Removes non-whitelisted entries from resource table. +class ResourceFilter : public IResourceTableConsumer { + public: + explicit ResourceFilter(const std::unordered_set<ResourceName>& blacklist); + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(ResourceFilter); + std::unordered_set<ResourceName> blacklist_; +}; + +} // namespace aapt + +#endif // AAPT_OPTIMIZE_RESOURCEFILTER_H diff --git a/tools/aapt2/optimize/ResourceFilter_test.cpp b/tools/aapt2/optimize/ResourceFilter_test.cpp new file mode 100644 index 000000000000..800b2bfd0403 --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter_test.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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 "optimize/ResourceFilter.h" + +#include "ResourceTable.h" +#include "test/Test.h" + +using ::aapt::test::HasValue; +using ::testing::Not; + +namespace aapt { + +TEST(ResourceFilterTest, SomeValuesAreFilteredOut) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/notblacklisted2", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted2", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set<ResourceName> blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + ResourceName({}, ResourceType::kString, "blacklisted2"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:string/notblacklisted2", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted2", default_config))); +} + +TEST(ResourceFilterTest, TypeIsCheckedBeforeFiltering) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/blacklisted", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set<ResourceName> blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/blacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/notblacklisted", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); +} + +} // namespace aapt diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index d1c9ca1644d9..9bef54e590c9 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -297,6 +297,53 @@ bool VerifyJavaStringFormat(const StringPiece& str) { return true; } +std::string Utf8ToModifiedUtf8(const std::string& utf8) { + // Java uses Modified UTF-8 which only supports the 1, 2, and 3 byte formats of UTF-8. To encode + // 4 byte UTF-8 codepoints, Modified UTF-8 allows the use of surrogate pairs in the same format + // of CESU-8 surrogate pairs. Calculate the size of the utf8 string with all 4 byte UTF-8 + // codepoints replaced with 2 3 byte surrogate pairs + size_t modified_size = 0; + const size_t size = utf8.size(); + for (size_t i = 0; i < size; i++) { + if (((uint8_t) utf8[i] >> 4) == 0xF) { + modified_size += 6; + i += 3; + } else { + modified_size++; + } + } + + // Early out if no 4 byte codepoints are found + if (size == modified_size) { + return utf8; + } + + std::string output; + output.reserve(modified_size); + for (size_t i = 0; i < size; i++) { + if (((uint8_t) utf8[i] >> 4) == 0xF) { + auto codepoint = (char32_t) utf32_from_utf8_at(utf8.data(), size, i, nullptr); + + // Calculate the high and low surrogates as UTF-16 would + char32_t high = ((codepoint - 0x10000) / 0x400) + 0xD800; + char32_t low = ((codepoint - 0x10000) % 0x400) + 0xDC00; + + // Encode each surrogate in UTF-8 + output.push_back((char) (0xE4 | ((high >> 12) & 0xF))); + output.push_back((char) (0x80 | ((high >> 6) & 0x3F))); + output.push_back((char) (0x80 | (high & 0x3F))); + output.push_back((char) (0xE4 | ((low >> 12) & 0xF))); + output.push_back((char) (0x80 | ((low >> 6) & 0x3F))); + output.push_back((char) (0x80 | (low & 0x3F))); + i += 3; + } else { + output.push_back(utf8[i]); + } + } + + return output; +} + std::u16string Utf8ToUtf16(const StringPiece& utf8) { ssize_t utf16_length = utf8_to_utf16_length( reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length()); diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h index 0eb35d18c06e..36b733376e6f 100644 --- a/tools/aapt2/util/Util.h +++ b/tools/aapt2/util/Util.h @@ -197,6 +197,9 @@ inline StringBuilder::operator bool() const { return error_.empty(); } +// Converts a UTF8 string into Modified UTF8 +std::string Utf8ToModifiedUtf8(const std::string& utf8); + // Converts a UTF8 string to a UTF16 string. std::u16string Utf8ToUtf16(const android::StringPiece& utf8); std::string Utf16ToUtf8(const android::StringPiece16& utf16); diff --git a/tools/aapt2/xml/XmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp index 402e5a459f4e..a023494ad8f7 100644 --- a/tools/aapt2/xml/XmlPullParser.cpp +++ b/tools/aapt2/xml/XmlPullParser.cpp @@ -38,6 +38,7 @@ XmlPullParser::XmlPullParser(InputStream* in) : in_(in), empty_(), depth_(0) { EndNamespaceHandler); XML_SetCharacterDataHandler(parser_, CharacterDataHandler); XML_SetCommentHandler(parser_, CommentDataHandler); + XML_SetCdataSectionHandler(parser_, StartCdataSectionHandler, EndCdataSectionHandler); event_queue_.push(EventData{Event::kStartDocument, 0, depth_++}); } @@ -287,6 +288,22 @@ void XMLCALL XmlPullParser::CommentDataHandler(void* user_data, parser->depth_, comment}); } +void XMLCALL XmlPullParser::StartCdataSectionHandler(void* user_data) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(user_data); + + parser->event_queue_.push(EventData{Event::kCdataStart, + XML_GetCurrentLineNumber(parser->parser_), + parser->depth_ }); +} + +void XMLCALL XmlPullParser::EndCdataSectionHandler(void* user_data) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(user_data); + + parser->event_queue_.push(EventData{Event::kCdataEnd, + XML_GetCurrentLineNumber(parser->parser_), + parser->depth_ }); +} + Maybe<StringPiece> FindAttribute(const XmlPullParser* parser, const StringPiece& name) { auto iter = parser->FindAttribute("", name); diff --git a/tools/aapt2/xml/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h index 63db66f0b2b7..6ebaa285745b 100644 --- a/tools/aapt2/xml/XmlPullParser.h +++ b/tools/aapt2/xml/XmlPullParser.h @@ -52,6 +52,8 @@ class XmlPullParser : public IPackageDeclStack { kEndElement, kText, kComment, + kCdataStart, + kCdataEnd, }; /** @@ -159,6 +161,8 @@ class XmlPullParser : public IPackageDeclStack { static void XMLCALL EndElementHandler(void* user_data, const char* name); static void XMLCALL EndNamespaceHandler(void* user_data, const char* prefix); static void XMLCALL CommentDataHandler(void* user_data, const char* comment); + static void XMLCALL StartCdataSectionHandler(void* user_data); + static void XMLCALL EndCdataSectionHandler(void* user_data); struct EventData { Event event; @@ -223,6 +227,10 @@ inline ::std::ostream& operator<<(::std::ostream& out, return out << "Text"; case XmlPullParser::Event::kComment: return out << "Comment"; + case XmlPullParser::Event::kCdataStart: + return out << "CdataStart"; + case XmlPullParser::Event::kCdataEnd: + return out << "CdataEnd"; } return out; } @@ -240,6 +248,8 @@ inline bool XmlPullParser::NextChildNode(XmlPullParser* parser, size_t start_dep case Event::kText: case Event::kComment: case Event::kStartElement: + case Event::kCdataStart: + case Event::kCdataEnd: return true; default: break; diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py index 70a47cf42755..018e9c9d5f57 100644 --- a/tools/apilint/apilint.py +++ b/tools/apilint/apilint.py @@ -69,12 +69,19 @@ class Field(): self.raw = raw.strip(" {;") self.blame = blame + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + raw = raw.split() self.split = list(raw) for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]: while r in raw: raw.remove(r) + # ignore annotations for now + raw = [ r for r in raw if not r.startswith("@") ] + self.typ = raw[0] self.name = raw[1].strip(";") if len(raw) >= 4 and raw[2] == "=": @@ -97,25 +104,39 @@ class Method(): self.raw = raw.strip(" {;") self.blame = blame - # drop generics for now - raw = re.sub("<.+?>", "", raw) + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + + # handle each clause differently + raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups() - raw = re.split("[\s(),;]+", raw) + # parse prefixes + raw = re.split("[\s]+", raw_prefix) for r in ["", ";"]: while r in raw: raw.remove(r) self.split = list(raw) - for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]: + for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator"]: while r in raw: raw.remove(r) self.typ = raw[0] self.name = raw[1] + + # parse args self.args = [] + for arg in re.split(",\s*", raw_args): + arg = re.split("\s", arg) + # ignore annotations for now + arg = [ a for a in arg if not a.startswith("@") ] + if len(arg[0]) > 0: + self.args.append(arg[0]) + + # parse throws self.throws = [] - target = self.args - for r in raw[2:]: - if r == "throws": target = self.throws - else: target.append(r) + for throw in re.split(",\s*", raw_throws): + self.throws.append(throw) + self.ident = ident(self.raw) def __hash__(self): @@ -135,12 +156,18 @@ class Class(): self.fields = [] self.methods = [] + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + raw = raw.split() self.split = list(raw) if "class" in raw: self.fullname = raw[raw.index("class")+1] elif "interface" in raw: self.fullname = raw[raw.index("interface")+1] + elif "@interface" in raw: + self.fullname = raw[raw.index("@interface")+1] else: raise ValueError("Funky class type %s" % (self.raw)) @@ -1334,18 +1361,25 @@ def verify_clone(clazz): error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()") +def is_interesting(clazz): + """Test if given class is interesting from an Android PoV.""" + + if clazz.pkg.name.startswith("java"): return False + if clazz.pkg.name.startswith("junit"): return False + if clazz.pkg.name.startswith("org.apache"): return False + if clazz.pkg.name.startswith("org.xml"): return False + if clazz.pkg.name.startswith("org.json"): return False + if clazz.pkg.name.startswith("org.w3c"): return False + if clazz.pkg.name.startswith("android.icu."): return False + return True + + def examine_clazz(clazz): """Find all style issues in the given class.""" notice(clazz) - 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 - if clazz.pkg.name.startswith("android.icu."): return + if not is_interesting(clazz): return verify_constants(clazz) verify_enums(clazz) @@ -1479,6 +1513,7 @@ def show_deprecations_at_birth(cur, prev): # Remove all existing things so we're left with new for prev_clazz in prev.values(): cur_clazz = cur[prev_clazz.fullname] + if not is_interesting(cur_clazz): continue sigs = { i.ident: i for i in prev_clazz.ctors } cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ] @@ -1506,6 +1541,38 @@ def show_deprecations_at_birth(cur, prev): print +def show_stats(cur, prev): + """Show API stats.""" + + stats = collections.defaultdict(int) + for cur_clazz in cur.values(): + if not is_interesting(cur_clazz): continue + + if cur_clazz.fullname not in prev: + stats['new_classes'] += 1 + stats['new_ctors'] += len(cur_clazz.ctors) + stats['new_methods'] += len(cur_clazz.methods) + stats['new_fields'] += len(cur_clazz.fields) + else: + prev_clazz = prev[cur_clazz.fullname] + + sigs = { i.ident: i for i in prev_clazz.ctors } + ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ]) + sigs = { i.ident: i for i in prev_clazz.methods } + methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ]) + sigs = { i.ident: i for i in prev_clazz.fields } + fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ]) + + if ctors + methods + fields > 0: + stats['extend_classes'] += 1 + stats['extend_ctors'] += ctors + stats['extend_methods'] += methods + stats['extend_fields'] += fields + + print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ]) + print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ]) + + 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.") @@ -1520,6 +1587,8 @@ if __name__ == "__main__": help="Show API changes noticed") parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True, help="Show API deprecations at birth") + parser.add_argument("--show-stats", action='store_const', const=True, + help="Show API stats") args = vars(parser.parse_args()) if args['no_color']: @@ -1539,6 +1608,14 @@ if __name__ == "__main__": show_deprecations_at_birth(cur, prev) sys.exit() + if args['show_stats']: + with current_file as f: + cur = _parse_stream(f) + with previous_file as f: + prev = _parse_stream(f) + show_stats(cur, prev) + sys.exit() + with current_file as f: cur_fail, cur_noticed = examine_stream(f) if not previous_file is None: diff --git a/tools/apilint/apilint_stats.sh b/tools/apilint/apilint_stats.sh new file mode 100755 index 000000000000..052d9a5265fe --- /dev/null +++ b/tools/apilint/apilint_stats.sh @@ -0,0 +1,7 @@ +#!/bin/bash +API=28 +while [ $API -gt 14 ]; do + echo "# Changes in API $((API))" + python tools/apilint/apilint.py --show-stats ../../prebuilts/sdk/$((API))/public/api/android.txt ../../prebuilts/sdk/$((API-1))/public/api/android.txt + let API=API-1 +done diff --git a/tools/incident_section_gen/main.cpp b/tools/incident_section_gen/main.cpp index 3689a8f2981e..639f98062b71 100644 --- a/tools/incident_section_gen/main.cpp +++ b/tools/incident_section_gen/main.cpp @@ -110,9 +110,7 @@ static bool generateIncidentSectionsCpp(Descriptor const* descriptor) N = descriptor->field_count(); for (int i=0; i<N; i++) { const FieldDescriptor* field = descriptor->field(i); - if (field->type() == FieldDescriptor::TYPE_MESSAGE) { - sections[field->name()] = field; - } + sections[field->name()] = field; } printf("IncidentSection const INCIDENT_SECTIONS[] = {\n"); @@ -404,7 +402,7 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { for (int i=0; i<descriptor->field_count(); i++) { const FieldDescriptor* field = descriptor->field(i); - if (field->type() != FieldDescriptor::TYPE_MESSAGE) { + if (field->type() != FieldDescriptor::TYPE_MESSAGE && field->type() != FieldDescriptor::TYPE_STRING) { continue; } const SectionFlags s = getSectionFlags(field); @@ -412,8 +410,7 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { case SECTION_NONE: continue; case SECTION_FILE: - printf(" new FileSection(%d, \"%s\", %s),\n", field->number(), s.args().c_str(), - s.device_specific() ? "true" : "false"); + printf(" new FileSection(%d, \"%s\"),\n", field->number(), s.args().c_str()); break; case SECTION_COMMAND: printf(" new CommandSection(%d,", field->number()); @@ -457,13 +454,13 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { const FieldDescriptor* field = fieldsInOrder[i]; const string fieldName = getFieldName(field); const Destination fieldDest = getFieldDest(field); - const string fieldMessageName = getMessageName(field->message_type(), fieldDest); - - skip[i] = true; - if (field->type() != FieldDescriptor::TYPE_MESSAGE) { + printPrivacy(fieldName, field, "NULL", fieldDest, "NULL"); continue; } + + skip[i] = true; + const string fieldMessageName = getMessageName(field->message_type(), fieldDest); // generate privacy flags for each section. if (generatePrivacyFlags(field->message_type(), fieldDest, variableNames, &parents)) { printPrivacy(fieldName, field, fieldMessageName, fieldDest, "NULL"); diff --git a/tools/stringslint/stringslint.py b/tools/stringslint/stringslint.py index d637ff346c82..03c0b9af66a0 100644 --- a/tools/stringslint/stringslint.py +++ b/tools/stringslint/stringslint.py @@ -20,11 +20,22 @@ a previous strings file, if provided. Usage: stringslint.py strings.xml Usage: stringslint.py strings.xml old_strings.xml + +In general: +* Errors signal issues that must be fixed before submitting, and are only + used when there are no false-positives. +* Warnings signal issues that might need to be fixed, but need manual + inspection due to risk of false-positives. +* Info signal issues that should be fixed to match best-practices, such + as providing comments to aid translation. """ -import re, sys +import re, sys, codecs import lxml.etree as ET +reload(sys) +sys.setdefaultencoding('utf8') + BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): @@ -43,10 +54,10 @@ def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): warnings = None -def warn(tag, msg, actual, expected): +def warn(tag, msg, actual, expected, color=YELLOW): global warnings key = "%s:%d" % (tag.attrib["name"], hash(msg)) - value = "%sLine %d: '%s':%s %s" % (format(fg=YELLOW, bold=True), + value = "%sLine %d: '%s':%s %s" % (format(fg=color, bold=True), tag.sourceline, tag.attrib["name"], format(reset=True), @@ -59,6 +70,46 @@ def warn(tag, msg, actual, expected): format(reset=True)) warnings[key] = value + +def error(tag, msg, actual, expected): + warn(tag, msg, actual, expected, RED) + +def info(tag, msg, actual, expected): + warn(tag, msg, actual, expected, CYAN) + +# Escaping logic borrowed from https://stackoverflow.com/a/24519338 +ESCAPE_SEQUENCE_RE = re.compile(r''' + ( \\U........ # 8-digit hex escapes + | \\u.... # 4-digit hex escapes + | \\x.. # 2-digit hex escapes + | \\[0-7]{1,3} # Octal escapes + | \\N\{[^}]+\} # Unicode characters by name + | \\[\\'"abfnrtv] # Single-character escapes + )''', re.UNICODE | re.VERBOSE) + +def decode_escapes(s): + def decode_match(match): + return codecs.decode(match.group(0), 'unicode-escape') + + s = re.sub(r"\n\s*", " ", s) + s = ESCAPE_SEQUENCE_RE.sub(decode_match, s) + s = re.sub(r"%(\d+\$)?[a-z]", "____", s) + s = re.sub(r"\^\d+", "____", s) + s = re.sub(r"<br/?>", "\n", s) + s = re.sub(r"</?[a-z]+>", "", s) + return s + +def sample_iter(tag): + if not isinstance(tag, ET._Comment) and re.match("{.*xliff.*}g", tag.tag) and "example" in tag.attrib: + yield tag.attrib["example"] + elif tag.text: + yield decode_escapes(tag.text) + for e in tag: + for v in sample_iter(e): + yield v + if e.tail: + yield decode_escapes(e.tail) + def lint(path): global warnings warnings = {} @@ -80,35 +131,45 @@ def lint(path): comment = last_comment last_comment = None + # Prepare string for analysis + text = "".join(child.itertext()) + sample = "".join(sample_iter(child)).strip().strip("'\"") + # Validate comment if comment is None: - warn(child, "Missing string comment to aid translation", + info(child, "Missing string comment to aid translation", None, None) continue if "do not translate" in comment.text.lower(): continue if "translatable" in child.attrib and child.attrib["translatable"].lower() == "false": continue - if re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text) is None: - warn(child, "Missing CHAR LIMIT to aid translation", + + limit = re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text) + if limit is None: + info(child, "Missing CHAR LIMIT to aid translation", repr(comment), "<!-- Description of string [CHAR LIMIT=32] -->") + elif re.match("\d+", limit.group(1)): + limit = int(limit.group(1)) + if len(sample) > limit: + warn(child, "Expanded string length is larger than CHAR LIMIT", + sample, None) # Look for common mistakes/substitutions - text = "".join(child.itertext()).strip() if "'" in text: - warn(child, "Turned quotation mark glyphs are more polished", + error(child, "Turned quotation mark glyphs are more polished", text, "This doesn\u2019t need to \u2018happen\u2019 today") if '"' in text and not text.startswith('"') and text.endswith('"'): - warn(child, "Turned quotation mark glyphs are more polished", + error(child, "Turned quotation mark glyphs are more polished", text, "This needs to \u201chappen\u201d today") if "..." in text: - warn(child, "Ellipsis glyph is more polished", + error(child, "Ellipsis glyph is more polished", text, "Loading\u2026") if "wi-fi" in text.lower(): - warn(child, "Non-breaking glyph is more polished", + error(child, "Non-breaking glyph is more polished", text, "Wi\u2011Fi") if "wifi" in text.lower(): - warn(child, "Using non-standard spelling", + error(child, "Using non-standard spelling", text, "Wi\u2011Fi") if re.search("\d-\d", text): warn(child, "Ranges should use en dash glyph", @@ -119,11 +180,17 @@ def lint(path): if ". " in text: warn(child, "Only use single space between sentences", text, "First idea. Second idea.") + if re.match(r"^[A-Z\s]{5,}$", text): + warn(child, "Actions should use android:textAllCaps in layout; ignore if acronym", + text, "Refresh data") + if " phone " in text and "product" not in child.attrib: + warn(child, "Strings mentioning phones should have variants for tablets", + text, None) # When more than one substitution, require indexes if len(re.findall("%[^%]", text)) > 1: if len(re.findall("%[^\d]", text)) > 0: - warn(child, "Substitutions must be indexed", + error(child, "Substitutions must be indexed", text, "Add %1$s to %2$s") # Require xliff substitutions @@ -132,15 +199,15 @@ def lint(path): if gc.tail and re.search("%[^%]", gc.tail): badsub = True if re.match("{.*xliff.*}g", gc.tag): if "id" not in gc.attrib: - warn(child, "Substitutions must define id attribute", + error(child, "Substitutions must define id attribute", None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") if "example" not in gc.attrib: - warn(child, "Substitutions must define example attribute", + error(child, "Substitutions must define example attribute", None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") else: if gc.text and re.search("%[^%]", gc.text): badsub = True if badsub: - warn(child, "Substitutions must be inside xliff tags", + error(child, "Substitutions must be inside xliff tags", text, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") return warnings |