Skip to content

Commit e790874

Browse files
atsushienojonpryor
authored andcommitted
[manifest-attribute-codegen] Add manifest-attribute-codegen (#1096)
`manifest-attribute-codegen.exe` is a simple tool that collects Android SDK resource information about `AndroidManifest.xml`, then generate a unified element/attribute definition with "which API introduced this" information. For example, the `//activity/@android:logo` attribute was added in API-15, while `//activity/@android:turnScreenOn` is from API-27. We will be able to use this information to more easily review our custom attributes, e.g. `Android.App.ActivityAttribute`, to ensure that they contain the appropriate properties. `build-tools/manifest-attribute-codegen/manifest-definition.xml` contains the current list of `AndroidManifest.xml` elements and their attributes as of API-27.
1 parent e5b1c92 commit e790874

File tree

3 files changed

+696
-0
lines changed

3 files changed

+696
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# What is this?
2+
3+
manifest-attribute-codegen.exe is a simple tool that collects Android SDK
4+
resource information about AndroidManifest.xml, then generate a unified
5+
element/attribute definition with "which API introduced this" information.
6+
7+
With that, we can find what needs to be added to xamarin-android.
8+
9+
(Note that this tools is not for generating code, at least for now.)
10+
11+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Xml;
7+
using System.Xml.Linq;
8+
9+
10+
namespace Xamarin.Android.Tools.ManifestAttributeCodeGenerator
11+
{
12+
public class Driver
13+
{
14+
public static void Main (string [] args)
15+
{
16+
var sdk = args.FirstOrDefault ()
17+
?? Environment.GetEnvironmentVariable ("ANDROID_SDK_HOME")
18+
?? Environment.GetEnvironmentVariable ("ANDROID_SDK_PATH");
19+
if (sdk == null)
20+
throw new InvalidOperationException ("Pass Android SDK location as the command argument, or specify ANDROID_SDK_HOME or ANDROID_SDK_PATH environment variable");
21+
22+
var manifests = Directory.GetDirectories (Path.Combine (sdk, "platforms"), "android-*")
23+
.Select (d => Path.Combine (d, "data", "res", "values", "attrs_manifest.xml"))
24+
.Where (f => File.Exists (f))
25+
.ToList ();
26+
manifests.Sort (StringComparer.OrdinalIgnoreCase);
27+
var defs = manifests.Select (m => ManifestDefinition.FromFileName (m));
28+
29+
var merged = new ManifestDefinition ();
30+
foreach (var def in defs) {
31+
foreach (var el in def.Elements) {
32+
var element = merged.Elements.FirstOrDefault (_ => _.Name == el.Name);
33+
if (element == null)
34+
merged.Elements.Add ((element = new ElementDefinition {
35+
ApiLevel = el.ApiLevel,
36+
Name = el.Name,
37+
Parents = (string []) el.Parents?.Clone (),
38+
}));
39+
foreach (var at in el.Attributes) {
40+
var attribute = element.Attributes.FirstOrDefault (_ => _.Name == at.Name);
41+
if (attribute == null)
42+
element.Attributes.Add ((attribute = new AttributeDefinition {
43+
ApiLevel = at.ApiLevel,
44+
Name = at.Name,
45+
Format = at.Format,
46+
}));
47+
foreach (var en in at.Enums) {
48+
var enumeration = at.Enums.FirstOrDefault (_ => _.Name == en.Name);
49+
if (enumeration == null)
50+
attribute.Enums.Add (new EnumDefinition {
51+
ApiLevel = en.ApiLevel,
52+
Name = en.Name,
53+
Value = en.Value,
54+
});
55+
}
56+
}
57+
}
58+
}
59+
var sw = new StringWriter ();
60+
merged.WriteXml (sw);
61+
Console.WriteLine ("<!-- TODO: `compatible-screens` is not defined -->");
62+
Console.WriteLine (sw.ToString ()
63+
.Replace (" api-level='10'", "")
64+
.Replace (" format=''", "")
65+
.Replace (" parent=''", ""));
66+
}
67+
}
68+
69+
class ManifestDefinition
70+
{
71+
public int ApiLevel { get; set; }
72+
public IList<ElementDefinition> Elements { get; private set; } = new List<ElementDefinition> ();
73+
74+
public static ManifestDefinition FromFileName (string filePath)
75+
{
76+
var dirName = new FileInfo (filePath).Directory.Parent.Parent.Parent.Name;
77+
var api = int.Parse (dirName.Substring (dirName.IndexOf ('-') + 1));
78+
return new ManifestDefinition () {
79+
ApiLevel = api,
80+
Elements = XDocument.Load (filePath).Root.Elements ("declare-styleable")
81+
.Select (e => ElementDefinition.FromElement (api, e))
82+
.ToList ()
83+
};
84+
}
85+
86+
public void WriteXml (TextWriter w)
87+
{
88+
w.WriteLine ("<m>");
89+
foreach (var e in Elements)
90+
e.WriteXml (w);
91+
w.WriteLine ("</m>");
92+
}
93+
94+
public void WriteCode (TextWriter w)
95+
{
96+
foreach (var e in Elements)
97+
e.WriteCode (w);
98+
}
99+
}
100+
101+
class ElementDefinition
102+
{
103+
public int ApiLevel { get; set; }
104+
public string Name { get; set; }
105+
public string [] Parents { get; set; }
106+
public IList<AttributeDefinition> Attributes { get; private set; } = new List<AttributeDefinition> ();
107+
108+
public string ActualElementName {
109+
get { return Name.ToActualName (); }
110+
}
111+
112+
static readonly char [] sep = new char [] {' '};
113+
114+
public static ElementDefinition FromElement (int api, XElement e)
115+
{
116+
return new ElementDefinition () {
117+
ApiLevel = api,
118+
Name = e.Attribute ("name").Value,
119+
Parents = e.Attribute ("parent")?.Value?.Split ().Select (s => s.Trim ()).Where (s => !string.IsNullOrWhiteSpace (s)).ToArray (),
120+
Attributes = e.Elements ("attr")
121+
.Select (a => AttributeDefinition.FromElement (api, a))
122+
.ToList ()
123+
};
124+
}
125+
126+
public void WriteXml (TextWriter w)
127+
{
128+
w.WriteLine ($" <e name='{ActualElementName}' api-level='{ApiLevel}'>");
129+
if (Parents != null && Parents.Any ())
130+
foreach (var p in Parents)
131+
w.WriteLine ($" <parent>{p.ToActualName ()}</parent>");
132+
foreach (var a in Attributes)
133+
a.WriteXml (w);
134+
w.WriteLine (" </e>");
135+
}
136+
137+
public void WriteCode (TextWriter w)
138+
{
139+
}
140+
}
141+
142+
class AttributeDefinition
143+
{
144+
public int ApiLevel { get; set; }
145+
public string Name { get; set; }
146+
public string Format { get; set; }
147+
public IList<EnumDefinition> Enums { get; set; } = new List<EnumDefinition> ();
148+
149+
public static AttributeDefinition FromElement (int api, XElement e)
150+
{
151+
return new AttributeDefinition {
152+
ApiLevel = api,
153+
Name = e.Attribute ("name").Value,
154+
Format = e.Attribute ("format")?.Value,
155+
Enums = e.Elements ("enum")
156+
.Select (n => new EnumDefinition {
157+
ApiLevel = api,
158+
Name = n.Attribute ("name").Value,
159+
Value = n.Attribute ("value").Value,
160+
})
161+
.ToList ()
162+
};
163+
}
164+
165+
public void WriteXml (TextWriter w)
166+
{
167+
w.Write ($" <a name='{Name}' format='{Format}' api-level='{ApiLevel}'");
168+
if (Enums.Any ()) {
169+
w.WriteLine (">");
170+
foreach (var e in Enums)
171+
w.WriteLine ($" <enum-definition name='{e.Name}' value='{e.Value}' api-level='{e.ApiLevel}' />");
172+
w.WriteLine (" </a>");
173+
}
174+
else
175+
w.WriteLine (" />");
176+
}
177+
}
178+
179+
class EnumDefinition
180+
{
181+
public int ApiLevel { get; set; }
182+
public string Name { get; set; }
183+
public string Value { get; set; }
184+
}
185+
186+
static class StringExtensions
187+
{
188+
static StringExtensions ()
189+
{
190+
// micro unit testing, am so clever!
191+
if (Hyphenate ("AndSoOn") != "and-so-on")
192+
throw new InvalidOperationException ("Am so buggy 1 " + Hyphenate ("AndSoOn"));
193+
if (Hyphenate ("aBigProblem") != "a-big-problem")
194+
throw new InvalidOperationException ("Am so buggy 2");
195+
if (Hyphenate ("my-two-cents") != "my-two-cents")
196+
throw new InvalidOperationException ("Am so buggy 3");
197+
}
198+
199+
public static string Hyphenate (this string s)
200+
{
201+
var sb = new StringBuilder (s.Length * 2);
202+
for (int i = 0; i < s.Length; i++) {
203+
if (char.IsUpper (s [i])) {
204+
if (i > 0)
205+
sb.Append ('-');
206+
sb.Append (char.ToLowerInvariant (s [i]));
207+
}
208+
else
209+
sb.Append (s [i]);
210+
}
211+
return sb.ToString ();
212+
}
213+
214+
const string prefix = "AndroidManifest";
215+
216+
public static string ToActualName (this string s)
217+
{
218+
s = s.IndexOf ('.') < 0 ? s : s.Substring (s.LastIndexOf ('.') + 1);
219+
220+
var ret = (s.StartsWith (prefix, StringComparison.Ordinal) ? s.Substring (prefix.Length) : s).Hyphenate ();
221+
return ret.Length == 0 ? "manifest" : ret;
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)