Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] AndroidLinkResources and Styleables (#7306
Browse files Browse the repository at this point in the history
)

Fixes: #7194

Context: dotnet/maui#7038

The initial version of `$(AndroidLinkResources)` (9e6ce03) was too
broad in its removal of Resource classes and fields.  Certain fields
such as `Styleable` arrays were not called using the IL `stsfld`
opcode.  As a result they could not be easily replaced with constant
usage.

However, the linker removed *all* the fields from the `Resource` 
nested types.  This would result in the following error at runtime:

	System.BadImageFormatException: 'Could not resolve field token 0x0400000b'

This was because the `int[]` fields were removed as part of the
linking process. 

Fix this by leaving the `int[]` fields in the `Resource` nested types
instead of removing them.

We can still remove all the other `int` fields.

We now also need to fix up the `Resource` nested type constructors
to replace the `int` field access with the constant values like we do
for the rest of the app.   This was not required previously because
these constructors were removed, but now we have to keep them because
the static array initialization takes place in these constructors.
  • Loading branch information
dellis1972 committed Sep 13, 2022
1 parent 93411cf commit d521ac0
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 4 deletions.
3 changes: 3 additions & 0 deletions samples/HelloWorld/HelloLibrary/HelloLibrary.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<FileAlignment>512</FileAlignment>
<AndroidApplication>false</AndroidApplication>
<DebugType>portable</DebugType>
<AndroidUseIntermediateDesignerFile>True</AndroidUseIntermediateDesignerFile>
<AndroidResgenClass>Resource</AndroidResgenClass>
</PropertyGroup>
<Import
Condition="Exists('..\..\..\Configuration.props')"
Expand Down Expand Up @@ -71,5 +73,6 @@
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\Case_Check.png" />
<AndroidResource Include="Resources\values\Attr.xml" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion samples/HelloWorld/HelloLibrary/LibraryActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
using Android.Views;
using Android.Widget;

namespace Mono.Samples.Hello
namespace HelloLibrary
{
[Activity(Label = "Library Activity", Name="mono.samples.hello.LibraryActivity")]
public class LibraryActivity : Activity
Expand Down
7 changes: 7 additions & 0 deletions samples/HelloWorld/HelloLibrary/Resources/values/Attr.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyLibraryWidget">
<attr name="library_bool_attr" format="boolean" />
<attr name="library_int_attr" format="integer" />
</declare-styleable>
</resources>
1 change: 1 addition & 0 deletions samples/HelloWorld/HelloWorld/HelloWorld.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<ItemGroup>
<AndroidResource Include="Resources\layout\Main.axml" />
<AndroidResource Include="Resources\values\Strings.xml" />
<AndroidResource Include="Resources\values\Attr.xml" />
<AndroidResource Include="Resources\mipmap-hdpi\Icon.png" />
<AndroidResource Include="Resources\mipmap-mdpi\Icon.png" />
<AndroidResource Include="Resources\mipmap-xhdpi\Icon.png" />
Expand Down
7 changes: 7 additions & 0 deletions samples/HelloWorld/HelloWorld/Resources/values/Attr.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyWidget">
<attr name="bool_attr" format="boolean" />
<attr name="int_attr" format="integer" />
</declare-styleable>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using Mono.Cecil.Cil;
using System.Text.RegularExpressions;
using Mono.Collections.Generic;
#if ILLINK
using Microsoft.Android.Sdk.ILLink;
#endif
Expand Down Expand Up @@ -69,8 +70,17 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli
protected void ClearDesignerClass (TypeDefinition designer)
{
LogMessage ($" TryRemoving {designer.FullName}");
designer.NestedTypes.Clear ();
designer.Methods.Clear ();
// for each of the nested types clear all but the
// int[] fields.
for (int i = designer.NestedTypes.Count -1; i >= 0; i--) {
var nestedType = designer.NestedTypes [i];
RemoveFieldsFromType (nestedType, designer.Module);
if (nestedType.Fields.Count == 0) {
// no fields we do not need this class at all.
designer.NestedTypes.RemoveAt (i);
}
}
RemoveUpdateIdValues (designer);
designer.Fields.Clear ();
designer.Properties.Clear ();
designer.CustomAttributes.Clear ();
Expand Down Expand Up @@ -117,6 +127,48 @@ protected void FixType (TypeDefinition type, TypeDefinition localDesigner)
}
}

protected void RemoveFieldsFromType (TypeDefinition type, ModuleDefinition module)
{
for (int i = type.Fields.Count - 1; i >= 0; i--) {
var field = type.Fields [i];
if (field.FieldType.IsArray) {
continue;
}
LogMessage ($"Removing {type.Name}::{field.Name}");
type.Fields.RemoveAt (i);
}
}

protected void RemoveUpdateIdValues (TypeDefinition type)
{
foreach (var method in type.Methods) {
if (method.Name.Contains ("UpdateIdValues")) {
FixUpdateIdValuesBody (method);
} else {
FixBody (method.Body, type);
}
}

foreach (var nestedType in type.NestedTypes) {
RemoveUpdateIdValues (nestedType);
}
}

protected void FixUpdateIdValuesBody (MethodDefinition method)
{
List<Instruction> finalInstructions = new List<Instruction> ();
Collection<Instruction> instructions = method.Body.Instructions;
for (int i = 0; i < method.Body.Instructions.Count-1; i++) {
Instruction instruction = instructions[i];
string line = instruction.ToString ();
bool found = line.Contains ("Int32[]") || instruction.OpCode == OpCodes.Ret;
if (!found) {
method.Body.Instructions.Remove (instruction);
i--;
}
}
}

protected void FixupAssemblyTypes (AssemblyDefinition assembly, TypeDefinition designer)
{
foreach (ModuleDefinition module in assembly.Modules)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,19 @@ protected override void FixBody (MethodBody body, TypeDefinition designer)
Dictionary<Instruction, int> instructions = new Dictionary<Instruction, int>();
var processor = body.GetILProcessor ();
string designerFullName = $"{designer.FullName}/";
bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName);
string declaringTypeName = body.Method.DeclaringType.Name;
foreach (var i in body.Instructions)
{
string line = i.ToString ();
if (line.Contains (designerFullName) && !instructions.ContainsKey (i))
if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i))
{
var match = opCodeRegex.Match (line);
if (match.Success && match.Groups.Count == 5) {
string key = match.Groups[4].Value.Replace (designerFullName, string.Empty);
if (isDesignerMethod) {
key = declaringTypeName +"::" + key;
}
if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i))
instructions.Add(i, designerConstants [key]);
}
Expand Down
111 changes: 111 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,5 +712,116 @@ public void SingleProject_ApplicationId ()
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));
Assert.IsTrue (didStart, "Activity should have started.");
}

[Test]
public void AppWithStyleableUsageRuns ([Values (true, false)] bool isRelease, [Values (true, false)] bool linkResources)
{
AssertHasDevices ();

var rootPath = Path.Combine (Root, "temp", TestName);
var lib = new XamarinAndroidLibraryProject () {
ProjectName = "Styleable.Library"
};

lib.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") {
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<declare-styleable name='MyLibraryView'>
<attr name='MyBool' format='boolean' />
<attr name='MyInt' format='integer' />
</declare-styleable>
</resources>",
});
lib.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\librarylayout.xml") {
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
<Styleable.Library.MyLibraryLayout xmlns:app='http://schemas.android.com/apk/res-auto' app:MyBool='true' app:MyInt='128'/>
",
});
lib.Sources.Add (new BuildItem.Source ("MyLibraryLayout.cs") {
TextContent = () => @"using System;
namespace Styleable.Library {
public class MyLibraryLayout : Android.Widget.LinearLayout
{
public MyLibraryLayout (Android.Content.Context context, Android.Util.IAttributeSet attrs) : base (context, attrs)
{
Android.Content.Res.TypedArray a = context.Theme.ObtainStyledAttributes (attrs, Resource.Styleable.MyLibraryView, 0,0);
try {
bool b = a.GetBoolean (Resource.Styleable.MyLibraryView_MyBool, defValue: false);
if (!b)
throw new Exception (""MyBool was not true."");
int i = a.GetInteger (Resource.Styleable.MyLibraryView_MyInt, defValue: -1);
if (i != 128)
throw new Exception (""MyInt was not 128."");
}
finally {
a.Recycle();
}
}
}
}"
});

proj = new XamarinAndroidApplicationProject () {
IsRelease = isRelease,
};
proj.AddReference (lib);

proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") {
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<declare-styleable name='MyView'>
<attr name='MyBool' format='boolean' />
<attr name='MyInt' format='integer' />
</declare-styleable>
</resources>",
});
proj.SetProperty ("AndroidLinkResources", linkResources ? "False" : "True");
proj.LayoutMain = proj.LayoutMain.Replace ("<LinearLayout", "<UnnamedProject.MyLayout xmlns:app='http://schemas.android.com/apk/res-auto' app:MyBool='true' app:MyInt='128'")
.Replace ("</LinearLayout>", "</UnnamedProject.MyLayout>");

proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_MAINACTIVITY}",
@"public class MyLayout : Android.Widget.LinearLayout
{
public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet attrs) : base (context, attrs)
{
Android.Content.Res.TypedArray a = context.Theme.ObtainStyledAttributes (attrs, Resource.Styleable.MyView, 0,0);
try {
bool b = a.GetBoolean (Resource.Styleable.MyView_MyBool, defValue: false);
if (!b)
throw new Exception (""MyBool was not true."");
int i = a.GetInteger (Resource.Styleable.MyView_MyInt, defValue: -1);
if (i != 128)
throw new Exception (""MyInt was not 128."");
}
finally {
a.Recycle();
}
}
}
");

var abis = new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" };
proj.SetAndroidSupportedAbis (abis);
var libBuilder = CreateDllBuilder (Path.Combine (rootPath, lib.ProjectName));
Assert.IsTrue (libBuilder.Build (lib), "Library should have built succeeded.");
builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName));


Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");

if (Builder.UseDotNet)
Assert.True (builder.RunTarget (proj, "Run"), "Project should have run.");
else if (CommercialBuildAvailable)
Assert.True (builder.RunTarget (proj, "_Run"), "Project should have run.");
else
AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity");

var didStart = WaitForActivityToStart (proj.PackageName, "MainActivity",
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));
Assert.IsTrue (didStart, "Activity should have started.");
}
}
}

0 comments on commit d521ac0

Please sign in to comment.