diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets
index 6a5bb66fd0f..007eb304e89 100644
--- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets
@@ -30,6 +30,13 @@ Copyright (C) 2016 Xamarin. All rights reserved.
<_GenerateResourceCaseMapFile>$(_DesignerIntermediateOutputPath)case_map.txt
+
+
+
+
"GR";
+
+ public string RTxtFile { get; set; }
+
+ [Required]
+ public string ResourceDirectory { get; set; }
+
+ public string[] AdditionalResourceDirectories { get; set; }
+
+ public string JavaPlatformJarPath { get; set; }
+
+ public string CaseMapFile { get; set; }
+
+ public override bool RunTask ()
+ {
+ // Parse the Resource files and then generate an R.txt file
+ var writer = new RtxtWriter ();
+
+ var resource_fixup = MonoAndroidHelper.LoadMapFile (BuildEngine4, CaseMapFile, StringComparer.OrdinalIgnoreCase);
+
+ var javaPlatformDirectory = Path.GetDirectoryName (JavaPlatformJarPath);
+ var parser = new FileResourceParser () { Log = Log, JavaPlatformDirectory = javaPlatformDirectory, ResourceFlagFile = ResourceFlagFile};
+ var resources = parser.Parse (ResourceDirectory, AdditionalResourceDirectories, resource_fixup);
+
+ writer.Write (RTxtFile, resources);
+
+ return !Log.HasLoggedErrors;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs
index 47481c6993c..51211ac48dc 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs
@@ -344,6 +344,7 @@ void Extract (
string importsDir = Path.Combine (outDirForDll, ImportsDirectory);
string resDir = Path.Combine (importsDir, "res");
string resDirArchive = Path.Combine (resDir, "..", "res.zip");
+ string rTxt = Path.Combine (importsDir, "R.txt");
string assetsDir = Path.Combine (importsDir, "assets");
bool updated = false;
@@ -358,7 +359,7 @@ void Extract (
AddJar (jars, Path.GetFullPath (file));
}
}
- if (Directory.Exists (resDir)) {
+ if (Directory.Exists (resDir) || File.Exists (rTxt)) {
var skipProcessing = aarFile.GetMetadata (AndroidSkipResourceProcessing);
if (string.IsNullOrEmpty (skipProcessing)) {
skipProcessing = "True";
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs
index 085cafde15d..473047f8328 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs
@@ -471,6 +471,30 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt
Directory.Delete (Path.Combine (Root, path), recursive: true);
}
+ [Test]
+ [Category ("SmokeTests")]
+ public void RtxtGeneratorOutput ()
+ {
+ var path = Path.Combine ("temp", TestName);
+ int platform = AndroidSdkResolver.GetMaxInstalledPlatform ();
+ string resPath = Path.Combine (Root, path, "res");
+ string rTxt = Path.Combine (Root, path, "R.txt");
+ CreateResourceDirectory (path);
+ List errors = new List ();
+ List messages = new List ();
+ IBuildEngine engine = new MockBuildEngine (TestContext.Out, errors: errors, messages: messages);
+ var generateRtxt = new GenerateRtxt () {
+ BuildEngine = engine,
+ RtxtFile = rTxt,
+ ResourceDirectory = resPath,
+ JavaPlatformJarPath = Path.Combine (AndroidSdkDirectory, "platforms", $"android-{platform}", "android.jar"),
+ };
+ Assert.IsTrue (generateRtxt.Execute (), "Task should have succeeded.");
+ FileAssert.Exists (rTxt, $"{rTxt} should have been created.");
+
+ Directory.Delete (Path.Combine (Root, path), recursive: true);
+ }
+
[Test]
[Category ("SmokeTests")]
public void CompareAapt2AndManagedParserOutput ()
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs
new file mode 100644
index 00000000000..8b6fa0d6242
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs
@@ -0,0 +1,219 @@
+using System;
+using System.CodeDom;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.Build.Utilities;
+using Microsoft.Android.Build.Tasks;
+
+namespace Xamarin.Android.Tasks
+{
+ class FileResourceParser : ResourceParser
+ {
+ Dictionary arrayMapping = new Dictionary ();
+ public Dictionary Parse (string resourceDirectory, IEnumerable additionalResourceDirectories, Dictionary resourceMap)
+ {
+ Log.LogDebugMessage ($"Processing Directory {resourceDirectory}");
+ var result = new Dictionary ();
+ Dictionary> resources = new Dictionary> ();
+ foreach (var dir in Directory.EnumerateDirectories (resourceDirectory, "*", SearchOption.TopDirectoryOnly)) {
+ foreach (var file in Directory.EnumerateFiles (dir, "*.*", SearchOption.AllDirectories)) {
+ ProcessResourceFile (file, resources);
+ }
+ }
+ if (additionalResourceDirectories != null) {
+ foreach (var dir in additionalResourceDirectories) {
+ Log.LogDebugMessage ($"Processing Directory {dir}");
+ if (Directory.Exists (dir)) {
+ foreach (var file in Directory.EnumerateFiles (dir, "*.*", SearchOption.AllDirectories)) {
+ ProcessResourceFile (file, resources);
+ }
+ } else {
+ Log.LogDebugMessage ($"Skipping non-existent directory: {dir}");
+ }
+ }
+ }
+ return result;
+ }
+
+ void ProcessResourceFile (string file, Dictionary> resources)
+ {
+ var fileName = Path.GetFileNameWithoutExtension (file);
+ if (string.IsNullOrEmpty (fileName))
+ return;
+ if (fileName.EndsWith (".9", StringComparison.OrdinalIgnoreCase))
+ fileName = Path.GetFileNameWithoutExtension (fileName);
+ var path = Directory.GetParent (file).Name;
+ var ext = Path.GetExtension (file);
+ switch (ext) {
+ case ".xml":
+ case ".axml":
+ if (string.Compare (path, "raw", StringComparison.OrdinalIgnoreCase) == 0)
+ goto default;
+ try {
+ ProcessXmlFile (file, resources);
+ } catch (XmlException ex) {
+ Log.LogCodedWarning ("XA1000", Properties.Resources.XA1000, file, ex);
+ }
+ break;
+ default:
+ break;
+ }
+ if (!resources.ContainsKey (path))
+ resources[path] = new SortedSet();
+ var r = new R () {
+ ResourceTypeName = path,
+ Identifier = fileName,
+ Id = -1,
+ };
+ resources[path].Add (r);
+ }
+
+ void ProcessStyleable (XmlReader reader, Dictionary> resources)
+ {
+ string topName = null;
+ int fieldCount = 0;
+ List fields = new List ();
+ List attribs = new List ();
+ while (reader.Read ()) {
+ if (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.Comment)
+ continue;
+ string name = null;
+ if (string.IsNullOrEmpty (topName)) {
+ if (reader.HasAttributes) {
+ while (reader.MoveToNextAttribute ()) {
+ if (reader.Name.Replace ("android:", "") == "name")
+ topName = reader.Value;
+ }
+ }
+ }
+ if (!reader.IsStartElement () || reader.LocalName == "declare-styleable")
+ continue;
+ if (reader.HasAttributes) {
+ while (reader.MoveToNextAttribute ()) {
+ if (reader.Name.Replace ("android:", "") == "name")
+ name = reader.Value;
+ }
+ }
+ reader.MoveToElement ();
+ if (reader.LocalName == "attr") {
+ attribs.Add (name);
+ } else {
+ if (name != null) {
+ var r = new R () {
+ ResourceTypeName = "id",
+ Identifier = name,
+ Id = -1,
+ };
+ resources [r.ResourceTypeName].Add (r);
+ }
+ }
+ }
+ var field = new R () {
+ ResourceTypeName = "styleable",
+ Identifier = topName,
+ Type = RType.Array,
+ };
+ if (!arrayMapping.ContainsKey (field)) {
+ attribs.Sort (StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < attribs.Count; i++) {
+ string name = attribs [i];
+ if (!name.StartsWith ("android:", StringComparison.OrdinalIgnoreCase)) {
+ var r = new R () {
+ ResourceTypeName = "attrib",
+ Identifier = name,
+ Id = -1,
+ };
+ resources [r.ResourceTypeName].Add (r);
+ fields.Add (r);
+ } else {
+ // this is an android:xxx resource, we should not calculate the id
+ // we should get it from "somewhere" maybe the pubic.xml
+ var r = new R () {
+ ResourceTypeName = "attrib",
+ Identifier = name,
+ Id = 0,
+ };
+ fields.Add (r);
+ }
+ }
+ if (field.Type != RType.Array)
+ return;
+ arrayMapping.Add (field, fields.ToArray ());
+ }
+ }
+
+ void ProcessXmlFile (string file, Dictionary> resources)
+ {
+ using (var reader = XmlReader.Create (file)) {
+ while (reader.Read ()) {
+ if (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.Comment)
+ continue;
+ if (reader.IsStartElement ()) {
+ var elementName = reader.Name;
+ if (reader.HasAttributes) {
+ string name = null;
+ string type = null;
+ string id = null;
+ string custom_id = null;
+ while (reader.MoveToNextAttribute ()) {
+ if (reader.LocalName == "name")
+ name = reader.Value;
+ if (reader.LocalName == "type")
+ type = reader.Value;
+ if (reader.LocalName == "id") {
+ string[] values = reader.Value.Split ('/');
+ if (values.Length != 2) {
+ id = reader.Value.Replace ("@+id/", "").Replace ("@id/", "");
+ } else {
+ if (values [0] != "@+id" && values [0] != "@id" && !values [0].Contains ("android:")) {
+ custom_id = values [0].Replace ("@", "").Replace ("+", "");
+ }
+ id = values [1];
+ }
+
+ }
+ if (reader.LocalName == "inflatedId") {
+ string inflateId = reader.Value.Replace ("@+id/", "").Replace ("@id/", "");
+ var r = new R () {
+ ResourceTypeName = "id",
+ Identifier = inflateId,
+ Id = -1,
+ };
+ resources[r.ResourceTypeName].Add (r);
+ }
+ }
+ if (name?.Contains ("android:") ?? false)
+ continue;
+ if (id?.Contains ("android:") ?? false)
+ continue;
+ // Move the reader back to the element node.
+ reader.MoveToElement ();
+ //if (!string.IsNullOrEmpty (name))
+ //CreateResourceField (type ?? elementName, name, reader.ReadSubtree ());
+ //if (!string.IsNullOrEmpty (custom_id) && !custom_types.TryGetValue (custom_id, out customClass)) {
+ //customClass = CreateClass (custom_id);
+ //custom_types.Add (custom_id, customClass);
+ //}
+ if (!string.IsNullOrEmpty (id)) {
+ //CreateIntField (customClass ?? ids, id);
+ var r = new R () {
+ ResourceTypeName = custom_id ?? "id",
+ Identifier = id,
+ Id = -1,
+ };
+ resources[r.ResourceTypeName].Add (r);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs
index 2ea0c52825c..f3cf692c0dd 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs
@@ -14,7 +14,7 @@
namespace Xamarin.Android.Tasks
{
- class ManagedResourceParser : ResourceParser
+ class ManagedResourceParser : FileResourceParser
{
class CompareTuple : IComparer<(int Key, CodeMemberField Value)>
{
@@ -298,9 +298,8 @@ void ProcessRtxtFile (string file)
{
var parser = new RtxtParser ();
var resources = parser.Parse (file, Log, map);
- foreach (var resource in resources) {
- var r = resource.Value;
- var cl = CreateClass (r.ResourceType);
+ foreach (var r in resources) {
+ var cl = CreateClass (r.ResourceTypeName);
switch (r.Type) {
case RType.Integer:
CreateIntField (cl, r.Identifier, r.Id);
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs
index cae7e5f83eb..2b80f14609d 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs
@@ -11,17 +11,24 @@ public enum RType {
Integer,
Array,
}
+ public enum ResourceType {
+ System,
+ Custom,
+ }
public struct R {
public RType Type;
public int Id;
public int [] Ids;
public string Identifier;
- public string ResourceType;
+ public string ResourceTypeName;
+ public ResourceType ResourceType;
+
+ public string Key => $"{ResourceTypeName}:{Identifier}";
public override string ToString () {
if (Type == RType.Integer)
- return $"int {ResourceType} {Identifier} 0x{Id.ToString ("x")}";
- return $"int[] {ResourceType} {Identifier} {{{String.Join (",", Ids.Select (x => $"0x{x.ToString ("x")}"))}}}";
+ return $"int {ResourceTypeName} {Identifier} 0x{Id.ToString ("x")}";
+ return $"int[] {ResourceTypeName} {Identifier} {{{String.Join (",", Ids.Select (x => $"0x{x.ToString ("x")}"))}}}";
}
}
@@ -30,84 +37,84 @@ public class RtxtParser {
TaskLoggingHelper log;
Dictionary map;
- public Dictionary Parse (string file, TaskLoggingHelper logger, Dictionary mapping){
+ public static Dictionary knownTypes = new Dictionary () {
+ { "anim", StringComparison.OrdinalIgnoreCase },
+ { "animator", StringComparison.OrdinalIgnoreCase },
+ { "attr", StringComparison.OrdinalIgnoreCase },
+ { "array", StringComparison.OrdinalIgnoreCase },
+ { "bool", StringComparison.OrdinalIgnoreCase },
+ { "color", StringComparison.OrdinalIgnoreCase },
+ { "dimen", StringComparison.OrdinalIgnoreCase },
+ { "drawable", StringComparison.OrdinalIgnoreCase },
+ { "id", StringComparison.OrdinalIgnoreCase },
+ { "integer", StringComparison.OrdinalIgnoreCase },
+ { "interpolator", StringComparison.OrdinalIgnoreCase },
+ { "layout", StringComparison.OrdinalIgnoreCase },
+ { "menu", StringComparison.OrdinalIgnoreCase },
+ { "mipmap", StringComparison.OrdinalIgnoreCase },
+ { "plurals", StringComparison.OrdinalIgnoreCase },
+ { "raw", StringComparison.OrdinalIgnoreCase },
+ { "string", StringComparison.OrdinalIgnoreCase },
+ { "style", StringComparison.OrdinalIgnoreCase },
+ { "styleable", StringComparison.OrdinalIgnoreCase },
+ { "transition", StringComparison.OrdinalIgnoreCase },
+ { "xml", StringComparison.OrdinalIgnoreCase },
+ };
+
+ public IEnumerable Parse (string file, TaskLoggingHelper logger, Dictionary mapping){
log = logger;
map = mapping;
- var result = new Dictionary ();
+ var result = new List ();
if (File.Exists (file))
ProcessRtxtFile (file, result);
-
return result;
}
- void ProcessRtxtFile (string file, Dictionary result)
+ void ProcessRtxtFile (string file, IList result)
{
var lines = File.ReadLines (file);
foreach (var line in lines) {
var items = line.Split (new char [] { ' ' }, 4);
int value = items [1] != "styleable" ? Convert.ToInt32 (items [3], 16) : -1;
string itemName = ResourceIdentifier.GetResourceName(items [1], items [2], map, log);
- string itemKey = $"{items [1]}:{itemName}";
- switch (items [1]) {
- case "anim":
- case "animator":
- case "attr":
- case "array":
- case "bool":
- case "color":
- case "dimen":
- case "drawable":
- case "font":
- case "id":
- case "integer":
- case "interpolator":
- case "layout":
- case "menu":
- case "mipmap":
- case "plurals":
- case "raw":
- case "string":
- case "style":
- case "transition":
- case "xml":
- result [itemKey] = new R () {
- ResourceType = items[1],
- Identifier = itemName,
- Id = value,
- };
- break;
- case "styleable":
- switch (items [0]) {
- case "int":
- result [itemKey] = new R () {
- ResourceType = items[1],
- Identifier = itemName,
- Id = Convert.ToInt32 (items [3], 10),
- };
- break;
- case "int[]":
- var arrayValues = items [3].Trim (new char [] { '{', '}' })
- .Replace (" ", "")
- .Split (new char [] { ',' });
+ if (knownTypes.ContainsKey (items [1])) {
+ if (items [1] == "styleable") {
+ switch (items [0]) {
+ case "int":
+ result.Add (new R () {
+ ResourceTypeName = items[1],
+ Identifier = itemName,
+ Id = Convert.ToInt32 (items [3], 10),
+ });
+ break;
+ case "int[]":
+ var arrayValues = items [3].Trim (new char [] { '{', '}' })
+ .Replace (" ", "")
+ .Split (new char [] { ',' });
- result [itemKey] = new R () {
- ResourceType = items[1],
- Type = RType.Array,
- Identifier = itemName,
- Ids = arrayValues.Select (x => string.IsNullOrEmpty (x) ? -1 : Convert.ToInt32 (x, 16)).ToArray (),
- };
- break;
+ result.Add (new R () {
+ ResourceTypeName = items[1],
+ Type = RType.Array,
+ Identifier = itemName,
+ Ids = arrayValues.Select (x => string.IsNullOrEmpty (x) ? -1 : Convert.ToInt32 (x, 16)).ToArray (),
+ });
+ break;
+ }
+ continue;
}
- break;
- // for custom views
- default:
- result [itemKey] = new R () {
- ResourceType = items[1],
+ result.Add (new R () {
+ ResourceTypeName = items[1],
Identifier = itemName,
Id = value,
- };
- break;
+ });
+ continue;
}
+ result.Add (new R () {
+ ResourceTypeName = items[1],
+ ResourceType = ResourceType.Custom,
+ Identifier = itemName,
+ Id = value,
+ });
}
}
}