Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Add ConvertCustomView Task (#2129)
Browse files Browse the repository at this point in the history
Fixes: #2100

Currently the `<ConvertResourcesCases/>` task is called twice on ALL
resources referenced by the project.  This is terribly inefficient,
but unfortunately is required: the first pass is done before the
`Compile` target, and fixes up the casing for things like
drawables/etc. so that they're ready to be processed by `aapt`.
The second pass is done after `<GenerateJavaStubs/>`, which happens
*after* the `Compile` target.  This is to replace any custom view
references with the correct `{md5}.View` style references, as we
replace the normal readable namespace with an md5 hash.

The problem was that `<ConvertResourcesCases/>` is doing a TON of
work it doesn't really need to do the second time around. For example
checking if it needs to lower case names of items.  The second pass
really only needs to worry about custom views.  In addition to that
it was also re-scanning ALL the files again.

This commit introduces a new `<ConvertCustomView/>` task.  The sole
purpose of this task is to fix up the layout files which contain
custom views.  It does nothing else.  To make this even quicker we
modify `<ConvertResourcesCases/>` to emit a mapping file,
`customview-map.txt`.  This file contains entries like:

	MonoDroid.Example.MyLayout;/fullpath/to/file.xml
	android.support.v7.widget.ActionBarOverlayLayout;/fullpath/to/some/other/file.xml

This allows us to know which files contain ANY layout.

This information in conjunction with the `acw_map.txt` file will
allow us to do a targeted update: we go through the `acw_map.txt`
values and fix up those files where we have entires in 
`customview-map.txt`.

This reduces the amount of time spent processing files quite a bit.
For a Blank Xamarin Forms app from a clean build.

	2639 ms  ConvertResourcesCases                      1 calls
	   3 ms  ConvertCustomView                          1 calls

Normally the `ConvertResourcesCases` would be called twice and would
take a total of 5-6 seconds.
  • Loading branch information
dellis1972 authored and jonpryor committed Sep 5, 2018
1 parent a677c17 commit 1886e6f
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 99 deletions.
145 changes: 145 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/ConvertCustomView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (C) 2018 Microsoft, Inc. All rights reserved.

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Monodroid;

namespace Xamarin.Android.Tasks {
public class ConvertCustomView : Task {

[Required]
public string CustomViewMapFile { get; set; }

[Required]
public string AcwMapFile { get; set; }

public string ResourceNameCaseMap { get; set; }

public ITaskItem [] ResourceDirectories { get; set; }

public override bool Execute ()
{
var resource_name_case_map = MonoAndroidHelper.LoadResourceCaseMap (ResourceNameCaseMap);
var acw_map = MonoAndroidHelper.LoadAcwMapFile (AcwMapFile);
var customViewMap = MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile);
var processed = new HashSet<string> ();

foreach (var kvp in acw_map) {
var key = kvp.Key;
var value = kvp.Value;
if (key == value)
continue;
if (customViewMap.TryGetValue (key, out HashSet<string> resourceFiles)) {
foreach (var file in resourceFiles) {
if (processed.Contains (file))
continue;
if (!File.Exists (file))
continue;
var document = XDocument.Load (file);
var e = document.Root;
bool update = false;
foreach (var elem in AndroidResource.GetElements (e).Prepend (e)) {
update |= TryFixCustomView (elem, acw_map, (level, message) => {
ITaskItem resdir = ResourceDirectories?.FirstOrDefault (x => file.StartsWith (x.ItemSpec)) ?? null;
switch (level) {
case TraceLevel.Error:
Log.FixupResourceFilenameAndLogCodedError ("XA1002", message, file, resdir.ItemSpec, resource_name_case_map);
break;
case TraceLevel.Warning:
Log.FixupResourceFilenameAndLogCodedError ("XA1001", message, file, resdir.ItemSpec, resource_name_case_map);
break;
default:
Log.LogDebugMessage (message);
break;
}
});
}
foreach (XAttribute a in AndroidResource.GetAttributes (e)) {
update |= TryFixCustomClassAttribute (a, acw_map);
update |= TryFixFragment (a, acw_map);
}
if (update) {
document.Save (file);
}
processed.Add (file);
}
}
}

return !Log.HasLoggedErrors;
}

static readonly XNamespace res_auto = "http://schemas.android.com/apk/res-auto";
static readonly XNamespace android = "http://schemas.android.com/apk/res/android";

bool TryFixCustomClassAttribute (XAttribute attr, Dictionary<string, string> acwMap)
{
/* Some attributes reference a Java class name.
* try to convert those like for TryFixCustomView
*/
if (attr.Name != (res_auto + "layout_behavior") && // For custom CoordinatorLayout behavior
(attr.Parent.Name != "transition" || attr.Name.LocalName != "class")) // For custom transitions
return false;

if (!acwMap.TryGetValue (attr.Value, out string mappedValue))
return false;

attr.Value = mappedValue;
return true;
}

bool TryFixFragment (XAttribute attr, Dictionary<string, string> acwMap)
{
// Looks for any:
// <fragment class="My.DotNet.Class"
// <fragment android:name="My.DotNet.Class" ...
// and tries to change it to the ACW name
if (attr.Parent.Name != "fragment")
return false;

if (attr.Name == "class" || attr.Name == android + "name") {
if (acwMap.TryGetValue (attr.Value, out string mappedValue)) {
attr.Value = mappedValue;

return true;
} else if (attr.Value?.Contains (',') ?? false) {
// attr.Value could be an assembly-qualified name that isn't in acw-map.txt;
// see e5b1c92c, https://github.com/xamarin/xamarin-android/issues/1296#issuecomment-365091948
var n = attr.Value.Substring (0, attr.Value.IndexOf (','));
if (acwMap.TryGetValue (n, out mappedValue)) {
attr.Value = mappedValue;
return true;
}
}
}

return false;
}

bool TryFixCustomView (XElement elem, Dictionary<string, string> acwMap, Action<TraceLevel, string> logMessage = null)
{
// Looks for any <My.DotNet.Class ...
// and tries to change it to the ACW name
string name = elem.Name.ToString ();
if (acwMap.TryGetValue (name, out string mappedValue)) {
elem.Name = mappedValue;
return true;
}
if (logMessage == null)
return false;
var matchingKey = acwMap.FirstOrDefault (x => String.Equals (x.Key, name, StringComparison.OrdinalIgnoreCase));
if (matchingKey.Key != null) {
// we have elements with slightly different casing.
// lets issue a error.
logMessage (TraceLevel.Error, $"We found a matching key '{matchingKey.Key}' for '{name}'. But the casing was incorrect. Please correct the casing");
}
return false;
}
}
}
67 changes: 35 additions & 32 deletions src/Xamarin.Android.Build.Tasks/Tasks/ConvertResourcesCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,31 @@ public class ConvertResourcesCases : Task
[Required]
public string AcwMapFile { get; set; }

[Required]
public string CustomViewMapFile { get; set; }

public string AndroidConversionFlagFile { get; set; }

public string ResourceNameCaseMap { get; set; }

Dictionary<string,string> resource_name_case_map;
Dictionary<string, HashSet<string>> customViewMap;

public override bool Execute ()
{
Log.LogDebugMessage ("ConvertResourcesCases Task");
Log.LogDebugMessage (" ResourceDirectories: {0}", ResourceDirectories);
Log.LogDebugMessage (" AcwMapFile: {0}", AcwMapFile);
Log.LogDebugMessage (" AndroidConversionFlagFile: {0}", AndroidConversionFlagFile);
Log.LogDebugMessage (" ResourceNameCaseMap: {0}", ResourceNameCaseMap);

resource_name_case_map = MonoAndroidHelper.LoadResourceCaseMap (ResourceNameCaseMap);
var acw_map = MonoAndroidHelper.LoadAcwMapFile (AcwMapFile);


if (CustomViewMapFile != null)
customViewMap = Xamarin.Android.Tasks.MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile);

// Look in the resource xml's for capitalized stuff and fix them
FixupResources (acw_map);

if (customViewMap != null)
Xamarin.Android.Tasks.MonoAndroidHelper.SaveCustomViewMapFile (BuildEngine4, CustomViewMapFile, customViewMap);

return true;
}

Expand Down Expand Up @@ -80,31 +85,29 @@ void FixupResources (ITaskItem item, Dictionary<string, string> acwMap)
continue;
}
Log.LogDebugMessage (" Processing: {0} {1} > {2}", file, srcmodifiedDate, lastUpdate);
var tmpdest = Path.GetTempFileName ();
File.Copy (file, tmpdest, overwrite: true);
MonoAndroidHelper.SetWriteable (tmpdest);
MonoAndroidHelper.SetWriteable (file);
try {
bool success = AndroidResource.UpdateXmlResource (resdir, tmpdest, acwMap,
resourcedirectories, (t, m) => {
string targetfile = file;
if (targetfile.StartsWith (resdir, StringComparison.InvariantCultureIgnoreCase)) {
targetfile = file.Substring (resdir.Length).TrimStart (Path.DirectorySeparatorChar);
if (resource_name_case_map.TryGetValue (targetfile, out string temp))
targetfile = temp;
targetfile = Path.Combine ("Resources", targetfile);
}
switch (t) {
case TraceLevel.Error:
Log.LogCodedError ("XA1002", file: targetfile, lineNumber: 0, message: m);
break;
case TraceLevel.Warning:
Log.LogCodedWarning ("XA1001", file: targetfile, lineNumber: 0, message: m);
break;
default:
Log.LogDebugMessage (m);
break;
bool success = AndroidResource.UpdateXmlResource (resdir, file, acwMap,
resourcedirectories, (level, message) => {
switch (level) {
case TraceLevel.Error:
Log.FixupResourceFilenameAndLogCodedError ("XA1002", message, file, resdir, resource_name_case_map);
break;
case TraceLevel.Warning:
Log.FixupResourceFilenameAndLogCodedError ("XA1001", message, file, resdir, resource_name_case_map);
break;
default:
Log.LogDebugMessage (message);
break;
}
});
}, registerCustomView : (e, filename) => {
if (customViewMap == null)
return;
HashSet<string> set;
if (!customViewMap.TryGetValue (e, out set))
customViewMap.Add (e, set = new HashSet<string> ());
set.Add (filename);
});
if (!success) {
//If we failed to write the file, a warning is logged, we should skip to the next file
continue;
Expand All @@ -115,11 +118,11 @@ void FixupResources (ITaskItem item, Dictionary<string, string> acwMap)
// doesn't support those type of BOM (it really wants the document to start
// with "<?"). Since there is no way to plug into the file saving mechanism in X.S
// we strip those here and point the designer to use resources from obj/
MonoAndroidHelper.CleanBOM (tmpdest);
MonoAndroidHelper.CleanBOM (file);


MonoAndroidHelper.CopyIfChanged (tmpdest, file);
} finally {
File.Delete (tmpdest);

}
}
}
Expand Down
45 changes: 41 additions & 4 deletions src/Xamarin.Android.Build.Tasks/Tasks/CopyAndConvertResources.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
Expand All @@ -23,10 +24,18 @@ public class CopyAndConvertResources : Task
[Required]
public string CacheFile { get; set; }

[Required]
public string CustomViewMapFile { get; set; }

public ITaskItem [] ResourceDirectories { get; set; }

public string ResourceNameCaseMap { get; set; }

[Output]
public ITaskItem[] ModifiedFiles { get; set; }

private List<ITaskItem> modifiedFiles = new List<ITaskItem>();
Dictionary<string, HashSet<string>> customViewMap;

public override bool Execute ()
{
Expand All @@ -39,6 +48,10 @@ public override bool Execute ()
throw new ArgumentException ("source and destination count mismatch");

var acw_map = MonoAndroidHelper.LoadAcwMapFile (AcwMapFile);
var resource_name_case_map = MonoAndroidHelper.LoadResourceCaseMap (ResourceNameCaseMap);

if (CustomViewMapFile != null)
customViewMap = Xamarin.Android.Tasks.MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile);

var xmlFilesToUpdate = new Dictionary<string,string> ();
for (int i = 0; i < SourceFiles.Length; i++) {
Expand Down Expand Up @@ -85,11 +98,32 @@ public override bool Execute ()
var dstmodifiedDate = File.Exists (destfilename) ? File.GetLastWriteTimeUtc (destfilename) : DateTime.MinValue;
var tmpdest = Path.GetTempFileName ();
var res = Path.Combine (Path.GetDirectoryName (filename), "..");
MonoAndroidHelper.CopyIfChanged (filename, tmpdest);
MonoAndroidHelper.SetWriteable (tmpdest);
MonoAndroidHelper.CopyIfChanged (filename, destfilename);
MonoAndroidHelper.SetWriteable (destfilename);
try {
AndroidResource.UpdateXmlResource (res, tmpdest, acw_map);
if (MonoAndroidHelper.CopyIfChanged (tmpdest, destfilename)) {
var updated = AndroidResource.UpdateXmlResource (res, destfilename, acw_map, logMessage: (level, message) => {
ITaskItem resdir = ResourceDirectories?.FirstOrDefault (x => filename.StartsWith (x.ItemSpec)) ?? null;
switch (level) {
case TraceLevel.Error:
Log.FixupResourceFilenameAndLogCodedError ("XA1002", message, filename, resdir.ItemSpec, resource_name_case_map);
break;
case TraceLevel.Warning:
Log.FixupResourceFilenameAndLogCodedError ("XA1001", message, filename, resdir.ItemSpec, resource_name_case_map);
break;
default:
Log.LogDebugMessage (message);
break;
}
}, registerCustomView: (e, file) => {
if (customViewMap == null)
return;
HashSet<string> set;
if (!customViewMap.TryGetValue (e, out set))
customViewMap.Add (e, set = new HashSet<string> ());
set.Add (file);

});
if (updated) {
if (!modifiedFiles.Any (i => i.ItemSpec == destfilename))
modifiedFiles.Add (new TaskItem (destfilename));
}
Expand All @@ -102,6 +136,9 @@ public override bool Execute ()

Log.LogDebugTaskItems (" ModifiedFiles:", ModifiedFiles);

if (customViewMap != null)
Xamarin.Android.Tasks.MonoAndroidHelper.SaveCustomViewMapFile (BuildEngine4, CustomViewMapFile, customViewMap);

return true;
}
}
Expand Down
Loading

0 comments on commit 1886e6f

Please sign in to comment.