Skip to content

Conversation

@jpobst
Copy link
Contributor

@jpobst jpobst commented Jan 6, 2022

Fixes #6606

Android provides the following Android.Graphics.Paint.Color getter/setter "pairs":

int getColor ();
void setColor (int);

long getColorLong ();   // API-29+
void setColor (long);   // API-29+

We have metadata which converts Android "Color-ints" into Android.Drawing.Color objects, however in the setColor case it is only checking method name and not parameter types, so it is being applied to both setColor overloads:

void setColor (Color);
void setColor (Color);   // API-29+

Our annotation code then applies the "API-29+" data to both matching method signatures, and our Property combining code matches both of them, removing both methods and producing:

Android.Drawable.Color Color { get; [SupportedOSPlatform ("android29.0")] set; }

[SupportedOSPlatform ("android29.0")]
long ColorLong { get; }  // No setter

By updating metadata:

  • Leave setColor (long) as long
  • Specifying that setColor (long) should not by "prop-ified"`

we can get the desired results:

Android.Drawable.Color Color { get; set; }

[SupportedOSPlatform ("android29.0")]
long ColorLong { get; }

[SupportedOSPlatform ("android29.0")]
void SetColor (long color) { .. }

Note we have chosen not to add the long setter to the existing getter because the property is virtual, and adding the setter would break users who have overridden the property.

@jpobst jpobst force-pushed the set-color-api branch 2 times, most recently from de43971 to 6ba9112 Compare January 6, 2022 17:05
@jpobst jpobst marked this pull request as ready for review January 6, 2022 21:13
@jpobst jpobst requested a review from jonpryor as a code owner January 6, 2022 21:13
<attr path="/api/package[@name='android.content.res']/class[@name='TypedArray']/method[@name='getColor']" name="return">Android.Graphics.Color</attr>
<attr path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='getColor']" name="return">Android.Graphics.Color</attr>
<attr path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='setColor']/parameter[position()=1]" name="type">Android.Graphics.Color</attr>
<attr path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='setColor' and parameter[1][@type='int']]/parameter[position()=1]" name="type">Android.Graphics.Color</attr>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update every expression which replaces int with Android.Graphics.Color to check for @type, not just this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Note the changes all should be no-ops, as there does not currently exist a long version. But this will help ensure that if a long version is added we won't break.

@jonpryor
Copy link
Contributor

jonpryor commented Jan 10, 2022

Fixes: https://github.com/xamarin/xamarin-android/issues/6606

Before API-29, the [`android.graphics.Paint`][0] class had a "color"
property:

	// Android Java
	/* partial */ class Paint {
	    public int  getColor();
	    public void setColor(int color);
	}

These methods were bound into a C# `Paint.Color` property:

	// API-28 C# Binding
	partial class Paint {
	    public virtual Android.Graphics.Color Color {
	        [Register ("getColor", "()I", …)]   get;
	        [Register ("setColor", "(I)V", …)]  set;
	    }
	}

In API-29 (bound in 936a09d1), a "colorLong" property was added via
[`Paint.getColorLong()`][1] and [`Paint.setColor(long)`][2]

	// Android Java
	/* partial */ class Paint {
	    public int  getColor();
	    public void setColor(int color);
	    public long getColorLong();
	    public void setColor(long color);
	}

Unfortunately, the "colorLong" property was mis-bound:
`Paint.setColor(long)` was "associated" with the existing `Color`
property, and `Paint.getColorLong()` was bound as a read-only property:

	// API-29 C# Binding
	partial class Paint {
	    public virtual Android.Graphics.Color Color {
	        [Register ("getColor", "()I", …)]   get;
	        [Register ("setColor", "(I)V", ApiSince=29, …)]  set;
	    }
	    public virtual long ColorLong {
	        [Register ("getColorLong", "()J", ApiSince=29, …)]   get;
	    }
	}

Note that the "association" was in the form of updating
`RegisterAttribute.ApiSince`, but it did *not* update the method
which was invoked; `Paint.setColor(int)` was still invoked.

Consequently, code attempting to use the `Paint.Color` setter would
get a [CA1416 warning][3] when building under .NET 6+:

	partial class MyDrawable : ShapeDrawable {
	    public MyDrawable() : base (new RectShape())
	    {
	        Paint.Color = Color.Black;
	        // Warning CA1416 : This call site is reachable on: 'Android' 21.0 and later. 'Paint.Color.set' is only supported on: 'android' 29.0 and later.
	    }
	}

The `Color` property was updated to "associate" (and ignore) the
`setColor(long)` method because `src/Mono.Android/metadata` only
checked parameter names and not parameter *types* when replacing
`int`s with `Color`s:

	<attr
	    path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='setColor']/parameter[position()=1]"
	    name="type"
	>Android.Graphics.Color</attr>

Fix this by updating the above XPath expression to check the
parameter type:

	<attr
	    path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='setColor' and parameter[1][@type='int']]/parameter[position()=1]"
	    name="type"
	>Android.Graphics.Color</attr>

and also add metadtata to state that `setColor(long)` should *not*
be associated with a property, by clearing the `propertyName` value:

	<attr
	    api-since="29"
	    path="/api/package[@name='android.graphics']/class[@name='Paint']/method[@name='setColor' and count(parameter)=1 and parameter[1][@type='long']]"
	    name="propertyName"
	></attr>

This results in the following C# binding:

	// Fixed API-29 C# Binding
	partial class Paint {
	    public virtual Android.Graphics.Color Color {
	        [Register ("getColor", "()I", …)]   get;
	        [Register ("setColor", "(I)V", …)]  set;
	    }
	    public virtual long ColorLong {
	        [Register ("getColorLong", "()J", ApiSince=29, …)]   get;
	    }

	    [Register ("setColor", "(J)V", ApiSince=29, …)]
	    public virtual void SetColor (long color);
	}

[0]: https://developer.android.com/reference/android/graphics/Paint
[1]: https://developer.android.com/reference/android/graphics/Paint#getColorLong()
[2]: https://developer.android.com/reference/android/graphics/Paint#setColor(long)
[3]: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1416

@jonpryor
Copy link
Contributor

Aside: this Paint.Color breakage is the only breakage I see in:

% git grep 'CannotChangeAttribute.*ApiSince' tests/api-compatibilit
tests/api-compatibility/acceptable-breakages-v10.0.txt:CannotChangeAttribute : Attribute 'Android.Runtime.RegisterAttribute' on 'Android.Graphics.Paint.Color.set(Android.Graphics.Color)' changed from '[RegisterAttribute("setColor", "(I)V", "GetSetColor_IHandler")]' in the contract to '[RegisterAttribute("setColor", "(I)V", "GetSetColor_IHandler", ApiSince=29)]' in the implementation.
tests/api-compatibility/acceptable-breakages-v8.0.txt:CannotChangeAttribute : Attribute 'Android.Runtime.RegisterAttribute' on 'Java.Lang.Class.GetAnnotationsByType(Java.Lang.Class)' changed from '[RegisterAttribute("getAnnotationsByType", "(Ljava/lang/Class;)[Ljava/lang/Object;", "", ApiSince=24)]' in the contract to '[RegisterAttribute("getAnnotationsByType", "(Ljava/lang/Class;)[Ljava/lang/annotation/Annotation;", "", ApiSince=24)]' in the implementation.
tests/api-compatibility/acceptable-breakages-v8.0.txt:CannotChangeAttribute : Attribute 'Android.Runtime.RegisterAttribute' on 'Java.Lang.Class.GetDeclaredAnnotation(Java.Lang.Class)' changed from '[RegisterAttribute("getDeclaredAnnotation", "(Ljava/lang/Class;)Ljava/lang/Object;", "", ApiSince=24)]' in the contract to '[RegisterAttribute("getDeclaredAnnotation", "(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;", "", ApiSince=24)]' in the implementation.

So, yay?

@jonpryor
Copy link
Contributor

@jpobst: another problem that comes to mind: I think this is (kinda) an API break, because it means existing code which does:

partial class MyPaint : Paint {
    public override long ColorLong =>}

will no longer compile, as it's missing the property setter in the override.

I thus think we may want to de-propertify setColor(long), and make it an explicit method:

partial class Paint {
    public virtual long ColorLong {get;}
    public virtual void SetColor(long color);
}

@jpobst
Copy link
Contributor Author

jpobst commented Jan 11, 2022

I thus think we may want to de-propertify setColor(long), and make it an explicit method:

Updated.

@jonpryor jonpryor merged commit 582adfd into main Jan 11, 2022
@jonpryor jonpryor deleted the set-color-api branch January 11, 2022 19:28
@github-actions github-actions bot locked and limited conversation to collaborators Jan 24, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warning CA1416 : This call site is reachable on: 'Android' 21.0 and later. 'Paint.Color.set' is only supported on: 'android' 29.0 and later.

3 participants