Skip to content

Commit d521ac0

Browse files
authored
[Xamarin.Android.Build.Tasks] AndroidLinkResources and Styleables (#7306)
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.
1 parent 93411cf commit d521ac0

File tree

8 files changed

+190
-4
lines changed

8 files changed

+190
-4
lines changed

samples/HelloWorld/HelloLibrary/HelloLibrary.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
<FileAlignment>512</FileAlignment>
1515
<AndroidApplication>false</AndroidApplication>
1616
<DebugType>portable</DebugType>
17+
<AndroidUseIntermediateDesignerFile>True</AndroidUseIntermediateDesignerFile>
18+
<AndroidResgenClass>Resource</AndroidResgenClass>
1719
</PropertyGroup>
1820
<Import
1921
Condition="Exists('..\..\..\Configuration.props')"
@@ -71,5 +73,6 @@
7173
</ItemGroup>
7274
<ItemGroup>
7375
<AndroidResource Include="Resources\drawable\Case_Check.png" />
76+
<AndroidResource Include="Resources\values\Attr.xml" />
7477
</ItemGroup>
7578
</Project>

samples/HelloWorld/HelloLibrary/LibraryActivity.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
using Android.Views;
1717
using Android.Widget;
1818

19-
namespace Mono.Samples.Hello
19+
namespace HelloLibrary
2020
{
2121
[Activity(Label = "Library Activity", Name="mono.samples.hello.LibraryActivity")]
2222
public class LibraryActivity : Activity
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<declare-styleable name="MyLibraryWidget">
4+
<attr name="library_bool_attr" format="boolean" />
5+
<attr name="library_int_attr" format="integer" />
6+
</declare-styleable>
7+
</resources>

samples/HelloWorld/HelloWorld/HelloWorld.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<ItemGroup>
6767
<AndroidResource Include="Resources\layout\Main.axml" />
6868
<AndroidResource Include="Resources\values\Strings.xml" />
69+
<AndroidResource Include="Resources\values\Attr.xml" />
6970
<AndroidResource Include="Resources\mipmap-hdpi\Icon.png" />
7071
<AndroidResource Include="Resources\mipmap-mdpi\Icon.png" />
7172
<AndroidResource Include="Resources\mipmap-xhdpi\Icon.png" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<declare-styleable name="MyWidget">
4+
<attr name="bool_attr" format="boolean" />
5+
<attr name="int_attr" format="integer" />
6+
</declare-styleable>
7+
</resources>

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using Mono.Cecil.Cil;
99
using System.Text.RegularExpressions;
10+
using Mono.Collections.Generic;
1011
#if ILLINK
1112
using Microsoft.Android.Sdk.ILLink;
1213
#endif
@@ -69,8 +70,17 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli
6970
protected void ClearDesignerClass (TypeDefinition designer)
7071
{
7172
LogMessage ($" TryRemoving {designer.FullName}");
72-
designer.NestedTypes.Clear ();
73-
designer.Methods.Clear ();
73+
// for each of the nested types clear all but the
74+
// int[] fields.
75+
for (int i = designer.NestedTypes.Count -1; i >= 0; i--) {
76+
var nestedType = designer.NestedTypes [i];
77+
RemoveFieldsFromType (nestedType, designer.Module);
78+
if (nestedType.Fields.Count == 0) {
79+
// no fields we do not need this class at all.
80+
designer.NestedTypes.RemoveAt (i);
81+
}
82+
}
83+
RemoveUpdateIdValues (designer);
7484
designer.Fields.Clear ();
7585
designer.Properties.Clear ();
7686
designer.CustomAttributes.Clear ();
@@ -117,6 +127,48 @@ protected void FixType (TypeDefinition type, TypeDefinition localDesigner)
117127
}
118128
}
119129

130+
protected void RemoveFieldsFromType (TypeDefinition type, ModuleDefinition module)
131+
{
132+
for (int i = type.Fields.Count - 1; i >= 0; i--) {
133+
var field = type.Fields [i];
134+
if (field.FieldType.IsArray) {
135+
continue;
136+
}
137+
LogMessage ($"Removing {type.Name}::{field.Name}");
138+
type.Fields.RemoveAt (i);
139+
}
140+
}
141+
142+
protected void RemoveUpdateIdValues (TypeDefinition type)
143+
{
144+
foreach (var method in type.Methods) {
145+
if (method.Name.Contains ("UpdateIdValues")) {
146+
FixUpdateIdValuesBody (method);
147+
} else {
148+
FixBody (method.Body, type);
149+
}
150+
}
151+
152+
foreach (var nestedType in type.NestedTypes) {
153+
RemoveUpdateIdValues (nestedType);
154+
}
155+
}
156+
157+
protected void FixUpdateIdValuesBody (MethodDefinition method)
158+
{
159+
List<Instruction> finalInstructions = new List<Instruction> ();
160+
Collection<Instruction> instructions = method.Body.Instructions;
161+
for (int i = 0; i < method.Body.Instructions.Count-1; i++) {
162+
Instruction instruction = instructions[i];
163+
string line = instruction.ToString ();
164+
bool found = line.Contains ("Int32[]") || instruction.OpCode == OpCodes.Ret;
165+
if (!found) {
166+
method.Body.Instructions.Remove (instruction);
167+
i--;
168+
}
169+
}
170+
}
171+
120172
protected void FixupAssemblyTypes (AssemblyDefinition assembly, TypeDefinition designer)
121173
{
122174
foreach (ModuleDefinition module in assembly.Modules)

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RemoveResourceDesignerStep.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,19 @@ protected override void FixBody (MethodBody body, TypeDefinition designer)
5555
Dictionary<Instruction, int> instructions = new Dictionary<Instruction, int>();
5656
var processor = body.GetILProcessor ();
5757
string designerFullName = $"{designer.FullName}/";
58+
bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName);
59+
string declaringTypeName = body.Method.DeclaringType.Name;
5860
foreach (var i in body.Instructions)
5961
{
6062
string line = i.ToString ();
61-
if (line.Contains (designerFullName) && !instructions.ContainsKey (i))
63+
if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i))
6264
{
6365
var match = opCodeRegex.Match (line);
6466
if (match.Success && match.Groups.Count == 5) {
6567
string key = match.Groups[4].Value.Replace (designerFullName, string.Empty);
68+
if (isDesignerMethod) {
69+
key = declaringTypeName +"::" + key;
70+
}
6671
if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i))
6772
instructions.Add(i, designerConstants [key]);
6873
}

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,5 +712,116 @@ public void SingleProject_ApplicationId ()
712712
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));
713713
Assert.IsTrue (didStart, "Activity should have started.");
714714
}
715+
716+
[Test]
717+
public void AppWithStyleableUsageRuns ([Values (true, false)] bool isRelease, [Values (true, false)] bool linkResources)
718+
{
719+
AssertHasDevices ();
720+
721+
var rootPath = Path.Combine (Root, "temp", TestName);
722+
var lib = new XamarinAndroidLibraryProject () {
723+
ProjectName = "Styleable.Library"
724+
};
725+
726+
lib.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") {
727+
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
728+
<resources>
729+
<declare-styleable name='MyLibraryView'>
730+
<attr name='MyBool' format='boolean' />
731+
<attr name='MyInt' format='integer' />
732+
</declare-styleable>
733+
</resources>",
734+
});
735+
lib.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\librarylayout.xml") {
736+
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
737+
<Styleable.Library.MyLibraryLayout xmlns:app='http://schemas.android.com/apk/res-auto' app:MyBool='true' app:MyInt='128'/>
738+
",
739+
});
740+
lib.Sources.Add (new BuildItem.Source ("MyLibraryLayout.cs") {
741+
TextContent = () => @"using System;
742+
743+
namespace Styleable.Library {
744+
public class MyLibraryLayout : Android.Widget.LinearLayout
745+
{
746+
747+
public MyLibraryLayout (Android.Content.Context context, Android.Util.IAttributeSet attrs) : base (context, attrs)
748+
{
749+
Android.Content.Res.TypedArray a = context.Theme.ObtainStyledAttributes (attrs, Resource.Styleable.MyLibraryView, 0,0);
750+
try {
751+
bool b = a.GetBoolean (Resource.Styleable.MyLibraryView_MyBool, defValue: false);
752+
if (!b)
753+
throw new Exception (""MyBool was not true."");
754+
int i = a.GetInteger (Resource.Styleable.MyLibraryView_MyInt, defValue: -1);
755+
if (i != 128)
756+
throw new Exception (""MyInt was not 128."");
757+
}
758+
finally {
759+
a.Recycle();
760+
}
761+
}
762+
}
763+
}"
764+
});
765+
766+
proj = new XamarinAndroidApplicationProject () {
767+
IsRelease = isRelease,
768+
};
769+
proj.AddReference (lib);
770+
771+
proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") {
772+
TextContent = () => @"<?xml version='1.0' encoding='utf-8'?>
773+
<resources>
774+
<declare-styleable name='MyView'>
775+
<attr name='MyBool' format='boolean' />
776+
<attr name='MyInt' format='integer' />
777+
</declare-styleable>
778+
</resources>",
779+
});
780+
proj.SetProperty ("AndroidLinkResources", linkResources ? "False" : "True");
781+
proj.LayoutMain = proj.LayoutMain.Replace ("<LinearLayout", "<UnnamedProject.MyLayout xmlns:app='http://schemas.android.com/apk/res-auto' app:MyBool='true' app:MyInt='128'")
782+
.Replace ("</LinearLayout>", "</UnnamedProject.MyLayout>");
783+
784+
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_MAINACTIVITY}",
785+
@"public class MyLayout : Android.Widget.LinearLayout
786+
{
787+
788+
public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet attrs) : base (context, attrs)
789+
{
790+
Android.Content.Res.TypedArray a = context.Theme.ObtainStyledAttributes (attrs, Resource.Styleable.MyView, 0,0);
791+
try {
792+
bool b = a.GetBoolean (Resource.Styleable.MyView_MyBool, defValue: false);
793+
if (!b)
794+
throw new Exception (""MyBool was not true."");
795+
int i = a.GetInteger (Resource.Styleable.MyView_MyInt, defValue: -1);
796+
if (i != 128)
797+
throw new Exception (""MyInt was not 128."");
798+
}
799+
finally {
800+
a.Recycle();
801+
}
802+
}
803+
}
804+
");
805+
806+
var abis = new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" };
807+
proj.SetAndroidSupportedAbis (abis);
808+
var libBuilder = CreateDllBuilder (Path.Combine (rootPath, lib.ProjectName));
809+
Assert.IsTrue (libBuilder.Build (lib), "Library should have built succeeded.");
810+
builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName));
811+
812+
813+
Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");
814+
815+
if (Builder.UseDotNet)
816+
Assert.True (builder.RunTarget (proj, "Run"), "Project should have run.");
817+
else if (CommercialBuildAvailable)
818+
Assert.True (builder.RunTarget (proj, "_Run"), "Project should have run.");
819+
else
820+
AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity");
821+
822+
var didStart = WaitForActivityToStart (proj.PackageName, "MainActivity",
823+
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));
824+
Assert.IsTrue (didStart, "Activity should have started.");
825+
}
715826
}
716827
}

0 commit comments

Comments
 (0)