blob: 2b4c4d20b791b347bf663bf797e947309bc2788a [file] [log] [blame]
Adam Lesinski1ab598f2015-08-14 14:26:04 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include "AppInfo.h"
18#include "Debug.h"
19#include "Flags.h"
20#include "JavaClassGenerator.h"
21#include "NameMangler.h"
22#include "ProguardRules.h"
23#include "XmlDom.h"
24
25#include "compile/IdAssigner.h"
26#include "flatten/Archive.h"
27#include "flatten/TableFlattener.h"
28#include "flatten/XmlFlattener.h"
29#include "link/Linkers.h"
30#include "link/TableMerger.h"
31#include "process/IResourceTableConsumer.h"
32#include "process/SymbolTable.h"
33#include "unflatten/BinaryResourceParser.h"
34#include "unflatten/FileExportHeaderReader.h"
35#include "util/Files.h"
36#include "util/StringPiece.h"
37
38#include <fstream>
39#include <sys/stat.h>
40#include <utils/FileMap.h>
41#include <vector>
42
43namespace aapt {
44
45struct LinkOptions {
46 std::string outputPath;
47 std::string manifestPath;
48 std::vector<std::string> includePaths;
49 Maybe<std::string> generateJavaClassPath;
50 Maybe<std::string> generateProguardRulesPath;
51 bool noAutoVersion = false;
52 bool staticLib = false;
53 bool verbose = false;
54 bool outputToDirectory = false;
55};
56
57struct LinkContext : public IAaptContext {
58 StdErrDiagnostics mDiagnostics;
59 std::unique_ptr<NameMangler> mNameMangler;
60 std::u16string mCompilationPackage;
61 uint8_t mPackageId;
62 std::unique_ptr<ISymbolTable> mSymbols;
63
64 IDiagnostics* getDiagnostics() override {
65 return &mDiagnostics;
66 }
67
68 NameMangler* getNameMangler() override {
69 return mNameMangler.get();
70 }
71
72 StringPiece16 getCompilationPackage() override {
73 return mCompilationPackage;
74 }
75
76 uint8_t getPackageId() override {
77 return mPackageId;
78 }
79
80 ISymbolTable* getExternalSymbols() override {
81 return mSymbols.get();
82 }
83};
84
85struct LinkCommand {
86 LinkOptions mOptions;
87 LinkContext mContext;
88
89 std::string buildResourceFileName(const ResourceFile& resFile) {
90 std::stringstream out;
91 out << "res/" << resFile.name.type;
92 if (resFile.config != ConfigDescription{}) {
93 out << "-" << resFile.config;
94 }
95 out << "/";
96
97 if (mContext.getNameMangler()->shouldMangle(resFile.name.package)) {
98 out << NameMangler::mangleEntry(resFile.name.package, resFile.name.entry);
99 } else {
100 out << resFile.name.entry;
101 }
102 out << file::getExtension(resFile.source.path);
103 return out.str();
104 }
105
106 /**
107 * Creates a SymbolTable that loads symbols from the various APKs and caches the
108 * results for faster lookup.
109 */
110 std::unique_ptr<ISymbolTable> createSymbolTableFromIncludePaths() {
111 AssetManagerSymbolTableBuilder builder;
112 for (const std::string& path : mOptions.includePaths) {
113 if (mOptions.verbose) {
114 mContext.getDiagnostics()->note(
115 DiagMessage(Source{ path }) << "loading include path");
116 }
117
118 std::unique_ptr<android::AssetManager> assetManager =
119 util::make_unique<android::AssetManager>();
120 int32_t cookie = 0;
121 if (!assetManager->addAssetPath(android::String8(path.data(), path.size()), &cookie)) {
122 mContext.getDiagnostics()->error(
123 DiagMessage(Source{ path }) << "failed to load include path");
124 return {};
125 }
126 builder.add(std::move(assetManager));
127 }
128 return builder.build();
129 }
130
131 /**
132 * Loads the resource table (not inside an apk) at the given path.
133 */
134 std::unique_ptr<ResourceTable> loadTable(const std::string& input) {
135 std::string errorStr;
136 Maybe<android::FileMap> map = file::mmapPath(input, &errorStr);
137 if (!map) {
138 mContext.getDiagnostics()->error(DiagMessage(Source{ input }) << errorStr);
139 return {};
140 }
141
142 std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
143 BinaryResourceParser parser(&mContext, table.get(), Source{ input },
144 map.value().getDataPtr(), map.value().getDataLength());
145 if (!parser.parse()) {
146 return {};
147 }
148 return table;
149 }
150
151 /**
152 * Inflates an XML file from the source path.
153 */
154 std::unique_ptr<XmlResource> loadXml(const std::string& path) {
155 std::ifstream fin(path, std::ifstream::binary);
156 if (!fin) {
157 mContext.getDiagnostics()->error(DiagMessage(Source{ path }) << strerror(errno));
158 return {};
159 }
160
161 return xml::inflate(&fin, mContext.getDiagnostics(), Source{ path });
162 }
163
164 /**
165 * Inflates a binary XML file from the source path.
166 */
167 std::unique_ptr<XmlResource> loadBinaryXmlSkipFileExport(const std::string& path) {
168 // Read header for symbol info and export info.
169 std::string errorStr;
170 Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
171 if (!maybeF) {
172 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
173 return {};
174 }
175
176 ssize_t offset = getWrappedDataOffset(maybeF.value().getDataPtr(),
177 maybeF.value().getDataLength(), &errorStr);
178 if (offset < 0) {
179 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
180 return {};
181 }
182
183 std::unique_ptr<XmlResource> xmlRes = xml::inflate(
184 (const uint8_t*) maybeF.value().getDataPtr() + (size_t) offset,
185 maybeF.value().getDataLength() - offset,
186 mContext.getDiagnostics(), Source(path));
187 if (!xmlRes) {
188 return {};
189 }
190 return xmlRes;
191 }
192
193 Maybe<ResourceFile> loadFileExportHeader(const std::string& path) {
194 // Read header for symbol info and export info.
195 std::string errorStr;
196 Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
197 if (!maybeF) {
198 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
199 return {};
200 }
201
202 ResourceFile resFile;
203 ssize_t offset = unwrapFileExportHeader(maybeF.value().getDataPtr(),
204 maybeF.value().getDataLength(),
205 &resFile, &errorStr);
206 if (offset < 0) {
207 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
208 return {};
209 }
210 return std::move(resFile);
211 }
212
213 bool copyFileToArchive(const std::string& path, const std::string& outPath, uint32_t flags,
214 IArchiveWriter* writer) {
215 std::string errorStr;
216 Maybe<android::FileMap> maybeF = file::mmapPath(path, &errorStr);
217 if (!maybeF) {
218 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
219 return false;
220 }
221
222 ssize_t offset = getWrappedDataOffset(maybeF.value().getDataPtr(),
223 maybeF.value().getDataLength(),
224 &errorStr);
225 if (offset < 0) {
226 mContext.getDiagnostics()->error(DiagMessage(path) << errorStr);
227 return false;
228 }
229
230 ArchiveEntry* entry = writer->writeEntry(outPath, flags, &maybeF.value(),
231 offset, maybeF.value().getDataLength() - offset);
232 if (!entry) {
233 mContext.getDiagnostics()->error(
234 DiagMessage(mOptions.outputPath) << "failed to write file " << outPath);
235 return false;
236 }
237 return true;
238 }
239
240 Maybe<AppInfo> extractAppInfoFromManifest(XmlResource* xmlRes) {
241 xml::Node* node = xmlRes->root.get();
242
243 // Find the first xml::Element.
244 while (node && !xml::nodeCast<xml::Element>(node)) {
245 node = !node->children.empty() ? node->children.front().get() : nullptr;
246 }
247
248 // Make sure the first element is <manifest> with package attribute.
249 if (xml::Element* manifestEl = xml::nodeCast<xml::Element>(node)) {
250 if (manifestEl->namespaceUri.empty() && manifestEl->name == u"manifest") {
251 if (xml::Attribute* packageAttr = manifestEl->findAttribute({}, u"package")) {
252 return AppInfo{ packageAttr->value };
253 }
254 }
255 }
256 return {};
257 }
258
259 bool verifyNoExternalPackages(ResourceTable* table) {
260 bool error = false;
261 for (const auto& package : table->packages) {
262 if (mContext.getCompilationPackage() != package->name ||
263 !package->id || package->id.value() != mContext.getPackageId()) {
264 // We have a package that is not related to the one we're building!
265 for (const auto& type : package->types) {
266 for (const auto& entry : type->entries) {
267 for (const auto& configValue : entry->values) {
268 mContext.getDiagnostics()->error(DiagMessage(configValue.source)
269 << "defined resource '"
270 << ResourceNameRef(package->name,
271 type->type,
272 entry->name)
273 << "' for external package '"
274 << package->name << "'");
275 error = true;
276 }
277 }
278 }
279 }
280 }
281 return !error;
282 }
283
284 std::unique_ptr<IArchiveWriter> makeArchiveWriter() {
285 if (mOptions.outputToDirectory) {
286 return createDirectoryArchiveWriter(mOptions.outputPath);
287 } else {
288 return createZipFileArchiveWriter(mOptions.outputPath);
289 }
290 }
291
292 bool flattenTable(ResourceTable* table, IArchiveWriter* writer) {
293 BigBuffer buffer(1024);
294 TableFlattenerOptions options = {};
295 options.useExtendedChunks = mOptions.staticLib;
296 TableFlattener flattener(&buffer, options);
297 if (!flattener.consume(&mContext, table)) {
298 return false;
299 }
300
301 ArchiveEntry* entry = writer->writeEntry("resources.arsc", ArchiveEntry::kAlign, buffer);
302 if (!entry) {
303 mContext.getDiagnostics()->error(
304 DiagMessage() << "failed to write resources.arsc to archive");
305 return false;
306 }
307 return true;
308 }
309
310 bool flattenXml(XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel,
311 IArchiveWriter* writer) {
312 BigBuffer buffer(1024);
313 XmlFlattenerOptions options = {};
314 options.keepRawValues = mOptions.staticLib;
315 options.maxSdkLevel = maxSdkLevel;
316 XmlFlattener flattener(&buffer, options);
317 if (!flattener.consume(&mContext, xmlRes)) {
318 return false;
319 }
320
321 ArchiveEntry* entry = writer->writeEntry(path, ArchiveEntry::kCompress, buffer);
322 if (!entry) {
323 mContext.getDiagnostics()->error(
324 DiagMessage() << "failed to write " << path << " to archive");
325 return false;
326 }
327 return true;
328 }
329
330 bool writeJavaFile(ResourceTable* table, const StringPiece16& package) {
331 if (!mOptions.generateJavaClassPath) {
332 return true;
333 }
334
335 std::string outPath = mOptions.generateJavaClassPath.value();
336 file::appendPath(&outPath, file::packageToPath(util::utf16ToUtf8(package)));
337 file::mkdirs(outPath);
338 file::appendPath(&outPath, "R.java");
339
340 std::ofstream fout(outPath, std::ofstream::binary);
341 if (!fout) {
342 mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
343 return false;
344 }
345
346 JavaClassGeneratorOptions javaOptions;
347 if (mOptions.staticLib) {
348 javaOptions.useFinal = false;
349 }
350
351 JavaClassGenerator generator(table, javaOptions);
352 if (!generator.generate(mContext.getCompilationPackage(), &fout)) {
353 mContext.getDiagnostics()->error(DiagMessage(outPath) << generator.getError());
354 return false;
355 }
356 return true;
357 }
358
359 bool writeProguardFile(const proguard::KeepSet& keepSet) {
360 if (!mOptions.generateProguardRulesPath) {
361 return true;
362 }
363
364 std::ofstream fout(mOptions.generateProguardRulesPath.value(), std::ofstream::binary);
365 if (!fout) {
366 mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
367 return false;
368 }
369
370 proguard::writeKeepSet(&fout, keepSet);
371 if (!fout) {
372 mContext.getDiagnostics()->error(DiagMessage() << strerror(errno));
373 return false;
374 }
375 return true;
376 }
377
378 int run(const std::vector<std::string>& inputFiles) {
379 // Load the AndroidManifest.xml
380 std::unique_ptr<XmlResource> manifestXml = loadXml(mOptions.manifestPath);
381 if (!manifestXml) {
382 return 1;
383 }
384
385 if (Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get())) {
386 mContext.mCompilationPackage = maybeAppInfo.value().package;
387 } else {
388 mContext.getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
389 << "no package specified in <manifest> tag");
390 return 1;
391 }
392
393 if (!util::isJavaPackageName(mContext.mCompilationPackage)) {
394 mContext.getDiagnostics()->error(DiagMessage(mOptions.manifestPath)
395 << "invalid package name '"
396 << mContext.mCompilationPackage
397 << "'");
398 return 1;
399 }
400
401 mContext.mNameMangler = util::make_unique<NameMangler>(
402 NameManglerPolicy{ mContext.mCompilationPackage });
403 mContext.mPackageId = 0x7f;
404 mContext.mSymbols = createSymbolTableFromIncludePaths();
405 if (!mContext.mSymbols) {
406 return 1;
407 }
408
409 if (mOptions.verbose) {
410 mContext.getDiagnostics()->note(
411 DiagMessage() << "linking package '" << mContext.mCompilationPackage << "' "
412 << "with package ID " << std::hex << (int) mContext.mPackageId);
413 }
414
415 ResourceTable mergedTable;
416 TableMerger tableMerger(&mContext, &mergedTable);
417
418 struct FilesToProcess {
419 Source source;
420 ResourceFile file;
421 };
422
423 bool error = false;
424 std::queue<FilesToProcess> filesToProcess;
425 for (const std::string& input : inputFiles) {
426 if (util::stringEndsWith<char>(input, ".apk")) {
427 // TODO(adamlesinski): Load resources from a static library APK
428 // Merge the table into TableMerger.
429
430 } else if (util::stringEndsWith<char>(input, ".arsc.flat")) {
431 if (mOptions.verbose) {
432 mContext.getDiagnostics()->note(DiagMessage() << "linking " << input);
433 }
434
435 std::unique_ptr<ResourceTable> table = loadTable(input);
436 if (!table) {
437 return 1;
438 }
439
440 if (!tableMerger.merge(Source(input), table.get())) {
441 return 1;
442 }
443
444 } else {
445 // Extract the exported IDs here so we can build the resource table.
446 if (Maybe<ResourceFile> maybeF = loadFileExportHeader(input)) {
447 ResourceFile& f = maybeF.value();
448
449 if (f.name.package.empty()) {
450 f.name.package = mContext.getCompilationPackage().toString();
451 }
452
453 Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(f.name);
454
455 // Add this file to the table.
456 if (!mergedTable.addFileReference(mangledName ? mangledName.value() : f.name,
457 f.config, f.source,
458 util::utf8ToUtf16(buildResourceFileName(f)),
459 mContext.getDiagnostics())) {
460 error = true;
461 }
462
463 // Add the exports of this file to the table.
464 for (SourcedResourceName& exportedSymbol : f.exportedSymbols) {
465 if (exportedSymbol.name.package.empty()) {
466 exportedSymbol.name.package = mContext.getCompilationPackage()
467 .toString();
468 }
469
470 Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(
471 exportedSymbol.name);
472 if (!mergedTable.addResource(
473 mangledName ? mangledName.value() : exportedSymbol.name,
474 {}, {}, f.source.withLine(exportedSymbol.line),
475 util::make_unique<Id>(), mContext.getDiagnostics())) {
476 error = true;
477 }
478 }
479
480 filesToProcess.push(FilesToProcess{ Source(input), std::move(f) });
481 } else {
482 return 1;
483 }
484 }
485 }
486
487 if (error) {
488 mContext.getDiagnostics()->error(DiagMessage() << "failed parsing input");
489 return 1;
490 }
491
492 if (!verifyNoExternalPackages(&mergedTable)) {
493 return 1;
494 }
495
496 if (!mOptions.staticLib) {
497 PrivateAttributeMover mover;
498 if (!mover.consume(&mContext, &mergedTable)) {
499 mContext.getDiagnostics()->error(
500 DiagMessage() << "failed moving private attributes");
501 return 1;
502 }
503 }
504
505 {
506 IdAssigner idAssigner;
507 if (!idAssigner.consume(&mContext, &mergedTable)) {
508 mContext.getDiagnostics()->error(DiagMessage() << "failed assigning IDs");
509 return 1;
510 }
511 }
512
513 mContext.mNameMangler = util::make_unique<NameMangler>(
514 NameManglerPolicy{ mContext.mCompilationPackage, tableMerger.getMergedPackages() });
515 mContext.mSymbols = JoinedSymbolTableBuilder()
516 .addSymbolTable(util::make_unique<SymbolTableWrapper>(&mergedTable))
517 .addSymbolTable(std::move(mContext.mSymbols))
518 .build();
519
520 {
521 ReferenceLinker linker;
522 if (!linker.consume(&mContext, &mergedTable)) {
523 mContext.getDiagnostics()->error(DiagMessage() << "failed linking references");
524 return 1;
525 }
526 }
527
528 proguard::KeepSet proguardKeepSet;
529
530 std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter();
531 if (!archiveWriter) {
532 mContext.getDiagnostics()->error(DiagMessage() << "failed to create archive");
533 return 1;
534 }
535
536 {
537 XmlReferenceLinker manifestLinker;
538 if (manifestLinker.consume(&mContext, manifestXml.get())) {
539
540 if (!proguard::collectProguardRulesForManifest(Source(mOptions.manifestPath),
541 manifestXml.get(),
542 &proguardKeepSet)) {
543 error = true;
544 }
545
546 if (!flattenXml(manifestXml.get(), "AndroidManifest.xml", {},
547 archiveWriter.get())) {
548 error = true;
549 }
550 } else {
551 error = true;
552 }
553 }
554
555 for (; !filesToProcess.empty(); filesToProcess.pop()) {
556 FilesToProcess& f = filesToProcess.front();
557 if (f.file.name.type != ResourceType::kRaw &&
558 util::stringEndsWith<char>(f.source.path, ".xml.flat")) {
559 if (mOptions.verbose) {
560 mContext.getDiagnostics()->note(DiagMessage() << "linking " << f.source.path);
561 }
562
563 std::unique_ptr<XmlResource> xmlRes = loadBinaryXmlSkipFileExport(f.source.path);
564 if (!xmlRes) {
565 return 1;
566 }
567
568 xmlRes->file = std::move(f.file);
569
570 XmlReferenceLinker xmlLinker;
571 if (xmlLinker.consume(&mContext, xmlRes.get())) {
572 if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(),
573 &proguardKeepSet)) {
574 error = true;
575 }
576
577 Maybe<size_t> maxSdkLevel;
578 if (!mOptions.noAutoVersion) {
579 maxSdkLevel = std::max<size_t>(xmlRes->file.config.sdkVersion, 1u);
580 }
581
582 if (!flattenXml(xmlRes.get(), buildResourceFileName(xmlRes->file), maxSdkLevel,
583 archiveWriter.get())) {
584 error = true;
585 }
586
587 if (!mOptions.noAutoVersion) {
588 Maybe<ResourceTable::SearchResult> result = mergedTable.findResource(
589 xmlRes->file.name);
590 for (int sdkLevel : xmlLinker.getSdkLevels()) {
591 if (sdkLevel > xmlRes->file.config.sdkVersion &&
592 shouldGenerateVersionedResource(result.value().entry,
593 xmlRes->file.config,
594 sdkLevel)) {
595 xmlRes->file.config.sdkVersion = sdkLevel;
596 if (!mergedTable.addFileReference(xmlRes->file.name,
597 xmlRes->file.config,
598 xmlRes->file.source,
599 util::utf8ToUtf16(
600 buildResourceFileName(xmlRes->file)),
601 mContext.getDiagnostics())) {
602 error = true;
603 continue;
604 }
605
606 if (!flattenXml(xmlRes.get(), buildResourceFileName(xmlRes->file),
607 sdkLevel, archiveWriter.get())) {
608 error = true;
609 }
610 }
611 }
612 }
613
614 } else {
615 error = true;
616 }
617 } else {
618 if (mOptions.verbose) {
619 mContext.getDiagnostics()->note(DiagMessage() << "copying " << f.source.path);
620 }
621
622 if (!copyFileToArchive(f.source.path, buildResourceFileName(f.file), 0,
623 archiveWriter.get())) {
624 error = true;
625 }
626 }
627 }
628
629 if (error) {
630 mContext.getDiagnostics()->error(DiagMessage() << "failed linking file resources");
631 return 1;
632 }
633
634 if (!mOptions.noAutoVersion) {
635 AutoVersioner versioner;
636 if (!versioner.consume(&mContext, &mergedTable)) {
637 mContext.getDiagnostics()->error(DiagMessage() << "failed versioning styles");
638 return 1;
639 }
640 }
641
642 if (!flattenTable(&mergedTable, archiveWriter.get())) {
643 mContext.getDiagnostics()->error(DiagMessage() << "failed to write resources.arsc");
644 return 1;
645 }
646
647 if (mOptions.generateJavaClassPath) {
648 if (!writeJavaFile(&mergedTable, mContext.getCompilationPackage())) {
649 return 1;
650 }
651 }
652
653 if (mOptions.generateProguardRulesPath) {
654 if (!writeProguardFile(proguardKeepSet)) {
655 return 1;
656 }
657 }
658
659 if (mOptions.verbose) {
660 Debug::printTable(&mergedTable);
661 for (; !tableMerger.getFileMergeQueue()->empty();
662 tableMerger.getFileMergeQueue()->pop()) {
663 const FileToMerge& f = tableMerger.getFileMergeQueue()->front();
664 mContext.getDiagnostics()->note(
665 DiagMessage() << f.srcPath << " -> " << f.dstPath << " from (0x"
666 << std::hex << (uintptr_t) f.srcTable << std::dec);
667 }
668 }
669
670 return 0;
671 }
672};
673
674int link(const std::vector<StringPiece>& args) {
675 LinkOptions options;
676 Flags flags = Flags()
677 .requiredFlag("-o", "Output path", &options.outputPath)
678 .requiredFlag("--manifest", "Path to the Android manifest to build",
679 &options.manifestPath)
680 .optionalFlagList("-I", "Adds an Android APK to link against", &options.includePaths)
681 .optionalFlag("--java", "Directory in which to generate R.java",
682 &options.generateJavaClassPath)
683 .optionalFlag("--proguard", "Output file for generated Proguard rules",
684 &options.generateProguardRulesPath)
685 .optionalSwitch("--no-auto-version",
686 "Disables automatic style and layout SDK versioning",
687 &options.noAutoVersion)
688 .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified "
689 "by -o",
690 &options.outputToDirectory)
691 .optionalSwitch("--static-lib", "Generate a static Android library", &options.staticLib)
692 .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
693
694 if (!flags.parse("aapt2 link", args, &std::cerr)) {
695 return 1;
696 }
697
698 LinkCommand cmd = { options };
699 return cmd.run(flags.getArgs());
700}
701
702} // namespace aapt