Skip to content

Commit

Permalink
[generator] Add support for [ObsoletedOSPlatform] (#1026)
Browse files Browse the repository at this point in the history
Context: dotnet/android#7234

Add support for a new option
`generator --lang-features=obsoleted-platform-attributes`.

When used, for an API that was obsoleted in API levels greater than
our .NET 7 minimum (API-21), we will generate .NET 7's new
`[ObsoletedOSPlatform]` attribute *instead of* `[Obsolete]`:

	// New
	[global::System.Runtime.Versioning.ObsoletedOSPlatform ("android22.0",
	    @"This class is obsoleted in this android platform")]
	public partial class CookieSpecParamBean : Org.Apache.Http.Params.HttpAbstractParamBean {
	}

	// Previous
	[global::System.Obsolete (@"This class is obsoleted in this android platform")]
	public partial class CookieSpecParamBean : Org.Apache.Http.Params.HttpAbstractParamBean {
	}

This is useful in a .NET 7+ context because we always *compile*
against the "latest" `Mono.Android`, even if you are *targeting* an
earlier version.

For example, the use of `[Obsolete]` means that using
`CookieSpecParamBean` would always result in a CS0618 obsolete usage
warning, even when building with `$(SupportedOSPlatformVersion)`=21.
(`CookieSpecParamBean` was obsoleted in API-22.)
  • Loading branch information
jpobst committed Aug 23, 2022
1 parent 6d1ae4e commit d3ea180
Show file tree
Hide file tree
Showing 26 changed files with 180 additions and 27 deletions.
13 changes: 13 additions & 0 deletions src/utils/XmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,18 @@ public static string XGetAttribute (this XPathNavigator nav, string name, string
var attr = nav.GetAttribute (name, ns);
return attr != null ? attr.Trim () : null;
}

public static int? XGetAttributeAsIntOrNull (this XElement element, string name)
{
var attr = element.Attribute (name);

if (attr?.Value is null)
return null;

if (int.TryParse (attr.Value, out var val))
return val;

return null;
}
}
}
80 changes: 80 additions & 0 deletions tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,86 @@ public void ObsoleteBoundMethodAbstractDeclaration ()
Assert.True (writer.ToString ().Contains ("[global::System.Obsolete (@\"This is so old!\")]"), writer.ToString ());
}

[Test]
public void ObsoletedOSPlatformAttributeSupport ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
<class abstract='false' deprecated='This is a class deprecated since 25!' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;' deprecated-since='25'>
<field deprecated='This is a field deprecated since 25!' final='true' name='ACCEPT_HANDOVER' jni-signature='Ljava/lang/String;' static='true' transient='false' type='java.lang.String' type-generic-aware='java.lang.String' value='&quot;android.permission.ACCEPT_HANDOVER&quot;' visibility='public' volatile='false' deprecated-since='25'></field>
<constructor deprecated='This is a constructor deprecated since 25!' final='false' name='MyClass' jni-signature='()V' bridge='false' static='false' type='com.xamarin.android.MyClass' synthetic='false' visibility='public' deprecated-since='25'></constructor>
<method abstract='true' deprecated='This is a method deprecated since 25!' final='false' name='countAffectedRows' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='25'></method>
<method abstract='false' deprecated='This is a property getter deprecated since 25!' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='25'></method>
<method abstract='false' deprecated='This is a property setter deprecated since 25!' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='25'>
<parameter name='count' type='int' jni-type='I'></parameter>
</method>
</class>
</package>
</api>";

options.UseObsoletedOSPlatformAttributes = true;

var gens = ParseApiDefinition (xml);
var iface = gens.Single (g => g.Name == "MyClass");

generator.Context.ContextTypes.Push (iface);
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
generator.Context.ContextTypes.Pop ();

// Ensure [ObsoletedOSPlatform] was written
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a class deprecated since 25!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a field deprecated since 25!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a constructor deprecated since 25!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a method deprecated since 25!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\", @\"This is a property getter deprecated since 25! This is a property setter deprecated since 25!\")]"), writer.ToString ());
}

[Test]
public void ObsoletedOSPlatformAttributeUnneededSupport ()
{
var xml = @"<api>
<package name='java.lang' jni-name='java/lang'>
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
</package>
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
<class abstract='false' deprecated='This is a class deprecated since 19!' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;' deprecated-since='19'>
<field deprecated='This is a field deprecated since 0!' final='true' name='ACCEPT_HANDOVER' jni-signature='Ljava/lang/String;' static='true' transient='false' type='java.lang.String' type-generic-aware='java.lang.String' value='&quot;android.permission.ACCEPT_HANDOVER&quot;' visibility='public' volatile='false' deprecated-since='0'></field>
<constructor deprecated='This is a constructor deprecated since empty string!' final='false' name='MyClass' jni-signature='()V' bridge='false' static='false' type='com.xamarin.android.MyClass' synthetic='false' visibility='public' deprecated-since=''></constructor>
<method abstract='true' deprecated='deprecated' final='false' name='countAffectedRows' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='25'></method>
<method abstract='true' deprecated='This method has an invalid deprecated-since!' final='false' name='countAffectedRows2' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='foo'></method>
<method abstract='false' deprecated='deprecated' final='false' name='getCount' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='22'></method>
<method abstract='false' deprecated='deprecated' final='false' name='setCount' jni-signature='(I)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='public' deprecated-since='22'>
<parameter name='count' type='int' jni-type='I'></parameter>
</method>
</class>
</package>
</api>";

options.UseObsoletedOSPlatformAttributes = true;

var gens = ParseApiDefinition (xml);
var iface = gens.Single (g => g.Name == "MyClass");

generator.Context.ContextTypes.Push (iface);
generator.WriteType (iface, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
generator.Context.ContextTypes.Pop ();

// These should use [Obsolete] because they have always been obsolete in all currently supported versions (21+)
Assert.True (writer.ToString ().Contains ("[global::System.Obsolete (@\"This is a class deprecated since 19!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Obsolete (@\"This is a field deprecated since 0!\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Obsolete (@\"This is a constructor deprecated since empty string!\")]"), writer.ToString ());

// This should not have a message because the default "deprecated" message isn't useful
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android25.0\")]"), writer.ToString ());
Assert.True (writer.ToString ().Contains ("[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android22.0\")]"), writer.ToString ());

// This should use [Obsolete] because the 'deprecated-since' attribute could not be parsed
Assert.True (writer.ToString ().Contains ("[global::System.Obsolete (@\"This method has an invalid deprecated-since!\")]"), writer.ToString ());
}

[Test]
[NonParallelizable] // We are setting a static property on Report
public void WarnIfTypeNameMatchesNamespace ()
Expand Down
1 change: 1 addition & 0 deletions tools/generator/CodeGenerationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer)
public bool SupportNestedInterfaceTypes { get; set; }
public bool SupportNullableReferenceTypes { get; set; }
public bool UseShallowReferencedTypes { get; set; }
public bool UseObsoletedOSPlatformAttributes { get; set; }
public bool RemoveConstSugar => BuildingCoreAssembly;

bool? buildingCoreAssembly;
Expand Down
1 change: 1 addition & 0 deletions tools/generator/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
SupportDefaultInterfaceMethods = options.SupportDefaultInterfaceMethods,
SupportNestedInterfaceTypes = options.SupportNestedInterfaceTypes,
SupportNullableReferenceTypes = options.SupportNullableReferenceTypes,
UseObsoletedOSPlatformAttributes = options.UseObsoletedOSPlatformAttributes,
};
var resolverCache = new TypeDefinitionCache ();

Expand Down
4 changes: 3 additions & 1 deletion tools/generator/CodeGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public CodeGeneratorOptions ()
public bool SupportNestedInterfaceTypes { get; set; }
public bool SupportNullableReferenceTypes { get; set; }
public bool UseLegacyJavaResolver { get; set; }
public bool UseObsoletedOSPlatformAttributes { get; set; }

public XmldocStyle XmldocStyle { get; set; } = XmldocStyle.IntelliSense;

Expand Down Expand Up @@ -102,12 +103,13 @@ public static CodeGeneratorOptions Parse (string[] args)
"SDK Platform {VERSION}/API level.",
v => opts.ApiLevel = v },
{ "lang-features=",
"For internal use. (Flags: interface-constants,default-interface-methods,nullable-reference-types)",
"For internal use. (Flags: interface-constants,default-interface-methods,nested-interface-types,nullable-reference-types,obsoleted-platform-attributes)",
v => {
opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true;
opts.SupportDefaultInterfaceMethods = v?.Contains ("default-interface-methods") == true;
opts.SupportNestedInterfaceTypes = v?.Contains ("nested-interface-types") == true;
opts.SupportNullableReferenceTypes = v?.Contains ("nullable-reference-types") == true;
opts.UseObsoletedOSPlatformAttributes = v?.Contains ("obsoleted-platform-attributes") == true;
}},
{ "preserve-enums",
"For internal use.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ public static Ctor CreateCtor (GenBase declaringType, XElement elem, CodeGenerat
ApiAvailableSince = declaringType.ApiAvailableSince,
CustomAttributes = elem.XGetAttribute ("customAttributes"),
Deprecated = elem.Deprecated (),
DeprecatedSince = elem.XGetAttributeAsIntOrNull ("deprecated-since"),
GenericArguments = elem.GenericArguments (),
Name = elem.XGetAttribute ("name"),
Visibility = elem.Visibility ()
Expand Down Expand Up @@ -200,6 +201,7 @@ public static Field CreateField (GenBase declaringType, XElement elem, CodeGener
var field = new Field {
ApiAvailableSince = declaringType.ApiAvailableSince,
DeprecatedComment = elem.XGetAttribute ("deprecated"),
DeprecatedSince = elem.XGetAttributeAsIntOrNull ("deprecated-since"),
IsAcw = true,
IsDeprecated = elem.XGetAttribute ("deprecated") != "not deprecated",
IsDeprecatedError = elem.XGetAttribute ("deprecated-error") == "true",
Expand Down Expand Up @@ -237,6 +239,7 @@ public static Field CreateField (GenBase declaringType, XElement elem, CodeGener
public static GenBaseSupport CreateGenBaseSupport (XElement pkg, XElement elem, CodeGenerationOptions opt, bool isInterface)
{
var support = new GenBaseSupport {
DeprecatedSince = elem.XGetAttributeAsIntOrNull ("deprecated-since"),
IsAcw = true,
IsDeprecated = elem.XGetAttribute ("deprecated") != "not deprecated",
IsGeneratable = true,
Expand Down Expand Up @@ -348,6 +351,7 @@ public static Method CreateMethod (GenBase declaringType, XElement elem, CodeGen
ArgsType = elem.Attribute ("argsType")?.Value,
CustomAttributes = elem.XGetAttribute ("customAttributes"),
Deprecated = elem.Deprecated (),
DeprecatedSince = elem.XGetAttributeAsIntOrNull ("deprecated-since"),
ExplicitInterface = elem.XGetAttribute ("explicitInterface"),
EventName = elem.Attribute ("eventName")?.Value,
GenerateAsyncWrapper = elem.Attribute ("generateAsyncWrapper") != null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class Field : ApiVersionsSupport.IApiAvailability, ISourceLineInfo
public string Annotation { get; set; }
public int ApiAvailableSince { get; set; }
public string DeprecatedComment { get; set; }
public int? DeprecatedSince { get; set; }
public bool IsAcw { get; set; }
public bool IsDeprecated { get; set; }
public bool IsDeprecatedError { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ public bool ContainsName (string name)

public string DeprecatedComment => support.DeprecatedComment;

public int? DeprecatedSince => support.DeprecatedSince;

IEnumerable<GenBase> Descendants (IList<GenBase> gens)
{
foreach (var directDescendants in gens.Where (x => x.BaseGen == this)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class GenBaseSupport
public bool IsAcw { get; set; }
public bool IsDeprecated { get; set; }
public string DeprecatedComment { get; set; }
public int? DeprecatedSince { get; set; }
public bool IsGeneratable { get; set; }
public bool IsGeneric { get; set; }
public bool IsObfuscated { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protected MethodBase (GenBase declaringType)
public string AssemblyName { get; set; }
public GenBase DeclaringType { get; }
public string Deprecated { get; set; }
public int? DeprecatedSince { get; set; }
public GenericParameterDefinitionList GenericArguments { get; set; }
public bool IsAcw { get; set; }
public bool IsValid { get; private set; }
Expand Down
12 changes: 5 additions & 7 deletions tools/generator/SourceWriters/Attributes/ObsoleteAttr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@ public class ObsoleteAttr : AttributeWriter

public ObsoleteAttr (string message = null, bool isError = false)
{
Message = message;
Message = message?.Replace ("\"", "\"\"").Trim ();
IsError = isError;
}

public override void WriteAttribute (CodeWriter writer)
{
var parts = new List<string> ();
var content = string.Empty;

if (Message != null)
parts.Add ($"@\"{Message}\"");
if (Message != null || IsError)
content += $"@\"{Message}\"";

if (IsError)
parts.Add ("error: true");

var content = string.Join (", ", parts.ToArray ());
content += ", error: true";

if (content.HasValue ())
writer.WriteLine ($"[global::System.Obsolete ({content})]");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Xamarin.SourceWriter;

namespace generator.SourceWriters
{
public class ObsoletedOSPlatformAttr : AttributeWriter
{
public string Message { get; set; }
public int Version { get; }

public ObsoletedOSPlatformAttr (string message, int version)
{
Message = message;
Version = version;
}

public override void WriteAttribute (CodeWriter writer)
{
if (Message.HasValue ())
writer.WriteLine ($"[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android{Version}.0\", @\"{Message.Replace ("\"", "\"\"")}\")]");
else
writer.WriteLine ($"[global::System.Runtime.Versioning.ObsoletedOSPlatform (\"android{Version}.0\")]");
}
}
}
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public BoundClass (ClassGen klass, CodeGenerationOptions opt, CodeGeneratorConte
klass.JavadocInfo?.AddJavadocs (Comments);
Comments.Add ($"// Metadata.xml XPath class reference: path=\"{klass.MetadataXPathReference}\"");

SourceWriterExtensions.AddObsolete (Attributes, klass.DeprecatedComment, klass.IsDeprecated);
SourceWriterExtensions.AddObsolete (Attributes, klass.DeprecatedComment, opt, forceDeprecate: klass.IsDeprecated, deprecatedSince: klass.DeprecatedSince);

SourceWriterExtensions.AddSupportedOSPlatform (Attributes, klass, opt);

Expand Down
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public BoundConstructor (ClassGen klass, Ctor constructor, bool useBase, CodeGen
Attributes.Add (new RegisterAttr (".ctor", constructor.JniSignature, string.Empty, additionalProperties: constructor.AdditionalAttributeString ()));
}

SourceWriterExtensions.AddObsolete (Attributes, constructor.Deprecated);
SourceWriterExtensions.AddObsolete (Attributes, constructor.Deprecated, opt, deprecatedSince: constructor.DeprecatedSince);

if (constructor.CustomAttributes != null)
Attributes.Add (new CustomAttr (constructor.CustomAttributes));
Expand Down
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public BoundField (GenBase type, Field field, CodeGenerationOptions opt)
if (field.IsEnumified)
Attributes.Add (new GeneratedEnumAttr ());

SourceWriterExtensions.AddObsolete (Attributes, field.DeprecatedComment, field.IsDeprecated, isError: field.IsDeprecatedError);
SourceWriterExtensions.AddObsolete (Attributes, field.DeprecatedComment, opt, field.IsDeprecated, isError: field.IsDeprecatedError, deprecatedSince: field.DeprecatedSince);

if (field.Annotation.HasValue ())
Attributes.Add (new CustomAttr (field.Annotation));
Expand Down
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundFieldAsProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public BoundFieldAsProperty (GenBase type, Field field, CodeGenerationOptions op
Attributes.Add (new RegisterAttr (field.JavaName, additionalProperties: field.AdditionalAttributeString ()));
}

SourceWriterExtensions.AddObsolete (Attributes, field.DeprecatedComment, field.IsDeprecated, isError: field.IsDeprecatedError);
SourceWriterExtensions.AddObsolete (Attributes, field.DeprecatedComment, opt, field.IsDeprecated, isError: field.IsDeprecatedError, deprecatedSince: field.DeprecatedSince);

SetVisibility (field.Visibility);
UseExplicitPrivateKeyword = true;
Expand Down
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public BoundInterface (InterfaceGen iface, CodeGenerationOptions opt, CodeGenera
iface.JavadocInfo?.AddJavadocs (Comments);
Comments.Add ($"// Metadata.xml XPath interface reference: path=\"{iface.MetadataXPathReference}\"");

SourceWriterExtensions.AddObsolete (Attributes, iface.DeprecatedComment, iface.IsDeprecated);
SourceWriterExtensions.AddObsolete (Attributes, iface.DeprecatedComment, opt, iface.IsDeprecated, deprecatedSince: iface.DeprecatedSince);

if (!iface.IsConstSugar (opt)) {
var signature = string.IsNullOrWhiteSpace (iface.Namespace)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public BoundInterfaceMethodDeclaration (Method method, string adapter, CodeGener
if (method.DeclaringType.IsGeneratable)
Comments.Add ($"// Metadata.xml XPath method reference: path=\"{method.GetMetadataXPathReference (method.DeclaringType)}\"");

SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated);
SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated, opt, deprecatedSince: method.DeprecatedSince);

if (method.IsReturnEnumified)
Attributes.Add (new GeneratedEnumAttr (true));
Expand Down
2 changes: 1 addition & 1 deletion tools/generator/SourceWriters/BoundMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public BoundMethod (GenBase type, Method method, CodeGenerationOptions opt, bool
if (method.DeclaringType.IsGeneratable)
Comments.Add ($"// Metadata.xml XPath method reference: path=\"{method.GetMetadataXPathReference (method.DeclaringType)}\"");

SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated);
SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated, opt, deprecatedSince: method.DeprecatedSince);

if (method.IsReturnEnumified)
Attributes.Add (new GeneratedEnumAttr (true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public BoundMethodAbstractDeclaration (GenBase gen, Method method, CodeGeneratio
if (method.DeclaringType.IsGeneratable)
Comments.Add ($"// Metadata.xml XPath method reference: path=\"{method.GetMetadataXPathReference (method.DeclaringType)}\"");

SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated);
SourceWriterExtensions.AddObsolete (Attributes, method.Deprecated, opt, deprecatedSince: method.DeprecatedSince);

SourceWriterExtensions.AddSupportedOSPlatform (Attributes, method, opt);

Expand Down
Loading

0 comments on commit d3ea180

Please sign in to comment.