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, + }); } } }