1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . IO ;
4+ using System . Linq ;
5+ using System . Text . RegularExpressions ;
6+ using System . Xml . Linq ;
7+ using System . Xml . XPath ;
8+
9+ namespace Monodroid {
10+ static class AndroidResource {
11+
12+ public static void UpdateXmlResource ( string filename , Dictionary < string , string > acwMap , IEnumerable < string > additionalDirectories = null )
13+ {
14+ // use a temporary file so we only update the real file if things actually changed
15+ string tmpfile = filename + ".bk" ;
16+ try {
17+ XDocument doc = XDocument . Load ( filename , LoadOptions . SetLineInfo ) ;
18+
19+ // The assumption here is that the file we're fixing up is in a directory below the
20+ // obj/${Configuration}/res/ directory and so appending ../gives us the actual path to
21+ // 'res/'
22+ UpdateXmlResource ( Path . Combine ( Path . GetDirectoryName ( filename ) , ".." ) , doc . Root , acwMap , additionalDirectories ) ;
23+ using ( var stream = File . OpenWrite ( tmpfile ) )
24+ using ( var xw = new LinePreservedXmlWriter ( new StreamWriter ( stream ) ) )
25+ xw . WriteNode ( doc . CreateNavigator ( ) , false ) ;
26+ Xamarin . Android . Tasks . MonoAndroidHelper . CopyIfChanged ( tmpfile , filename ) ;
27+ File . Delete ( tmpfile ) ;
28+ }
29+ catch ( Exception e ) {
30+ if ( File . Exists ( tmpfile ) ) {
31+ File . Delete ( tmpfile ) ;
32+ }
33+ Console . Error . WriteLine ( "AndroidResgen: Warning while updating Resource XML '{0}': {1}" , filename , e . Message ) ;
34+ return ;
35+ }
36+ }
37+
38+ static readonly XNamespace android = "http://schemas.android.com/apk/res/android" ;
39+ static readonly XNamespace res_auto = "http://schemas.android.com/apk/res-auto" ;
40+ static readonly Regex r = new Regex ( @"^@\+?(?<package>[^:]+:)?(anim|color|drawable|layout|menu)/(?<file>.*)$" ) ;
41+ static readonly string [ ] fixResourcesAliasPaths = {
42+ "/resources/item" ,
43+ "/resources/integer-array/item" ,
44+ "/resources/array/item" ,
45+ "/resources/style/item" ,
46+ } ;
47+
48+ public static void UpdateXmlResource ( XElement e )
49+ {
50+ UpdateXmlResource ( e , new Dictionary < string , string > ( ) ) ;
51+ }
52+
53+ public static void UpdateXmlResource ( XElement e , Dictionary < string , string > acwMap )
54+ {
55+ UpdateXmlResource ( null , e , acwMap ) ;
56+ }
57+
58+ static IEnumerable < T > Prepend < T > ( this IEnumerable < T > l , T another ) where T : XNode
59+ {
60+ yield return another ;
61+ foreach ( var e in l )
62+ yield return e ;
63+ }
64+
65+ static void UpdateXmlResource ( string resourcesBasePath , XElement e , Dictionary < string , string > acwMap , IEnumerable < string > additionalDirectories = null )
66+ {
67+ foreach ( var elem in GetElements ( e ) . Prepend ( e ) ) {
68+ TryFixCustomView ( elem , acwMap ) ;
69+ }
70+
71+ foreach ( var path in fixResourcesAliasPaths ) {
72+ foreach ( XElement item in e . XPathSelectElements ( path ) . Prepend ( e ) ) {
73+ TryFixResourceAlias ( item , resourcesBasePath , additionalDirectories ) ;
74+ }
75+ }
76+
77+ foreach ( XAttribute a in GetAttributes ( e ) ) {
78+ if ( a . IsNamespaceDeclaration )
79+ continue ;
80+
81+ if ( TryFixFragment ( a , acwMap ) )
82+ continue ;
83+
84+ if ( TryFixResAuto ( a , acwMap ) )
85+ continue ;
86+
87+ if ( TryFixCustomClassAttribute ( a , acwMap ) )
88+ continue ;
89+
90+ if ( a . Name . Namespace != android &&
91+ ! ( a . Name . LocalName == "layout" && a . Name . Namespace == XNamespace . None &&
92+ a . Parent . Name . LocalName == "include" && a . Parent . Name . Namespace == XNamespace . None ) )
93+ continue ;
94+
95+ Match m = r . Match ( a . Value ) ;
96+ if ( ! m . Success )
97+ continue ;
98+ if ( m . Groups [ "package" ] . Success )
99+ continue ;
100+ a . Value = TryLowercaseValue ( a . Value , resourcesBasePath , additionalDirectories ) ;
101+ }
102+ }
103+
104+ static bool ResourceNeedsToBeLowerCased ( string value , string resourceBasePath , IEnumerable < string > additionalDirectories )
105+ {
106+ // Might be a bit of an overkill, but the data comes (indirectly) from the user since it's the
107+ // path to the msbuild's intermediate output directory and that location can be changed by the
108+ // user. It's better to be safe than sorry.
109+ resourceBasePath = ( resourceBasePath ?? String . Empty ) . Trim ( ) ;
110+ if ( String . IsNullOrEmpty ( resourceBasePath ) )
111+ return true ;
112+
113+ // Avoid resource names that are all whitespace
114+ value = ( value ?? String . Empty ) . Trim ( ) ;
115+ if ( String . IsNullOrEmpty ( value ) )
116+ return false ; // let's save some time
117+ if ( value . Length < 4 || value [ 0 ] != '@' ) // 4 is the minimum length since we need a string
118+ // that is at least of the following
119+ // form: @x/y. Checking it here saves some time
120+ // below.
121+ return true ;
122+
123+ string filePath = null ;
124+ int slash = value . IndexOf ( '/' ) ;
125+ int colon = value . IndexOf ( ':' ) ;
126+ if ( colon == - 1 )
127+ colon = 0 ;
128+
129+ // Determine the the potential definition file's path based on the resource type.
130+ string dirPrefix = value . Substring ( colon + 1 , slash - colon - 1 ) . ToLowerInvariant ( ) ;
131+ string fileNamePattern = value . Substring ( slash + 1 ) . ToLowerInvariant ( ) + ".*" ;
132+
133+ if ( Directory . EnumerateDirectories ( resourceBasePath , dirPrefix + "*" ) . Any ( dir => Directory . EnumerateFiles ( dir , fileNamePattern ) . Any ( ) ) )
134+ return true ;
135+
136+ // check additional directories if we have them incase the resource is in a library project
137+ if ( additionalDirectories != null )
138+ foreach ( var additionalDirectory in additionalDirectories )
139+ if ( Directory . EnumerateDirectories ( additionalDirectory , dirPrefix + "*" ) . Any ( dir => Directory . EnumerateFiles ( dir , fileNamePattern ) . Any ( ) ) )
140+ return true ;
141+
142+ // No need to change the reference case.
143+ return false ;
144+ }
145+
146+ static IEnumerable < XAttribute > GetAttributes ( XElement e )
147+ {
148+ foreach ( XAttribute a in e . Attributes ( ) )
149+ yield return a ;
150+ foreach ( XElement c in e . Elements ( ) )
151+ foreach ( XAttribute a in GetAttributes ( c ) )
152+ yield return a ;
153+ }
154+
155+ static IEnumerable < XElement > GetElements ( XElement e )
156+ {
157+ foreach ( var a in e . Elements ( ) ) {
158+ yield return a ;
159+
160+ foreach ( var b in GetElements ( a ) )
161+ yield return b ;
162+ }
163+ }
164+
165+ private static void TryFixResourceAlias ( XElement elem , string resourceBasePath , IEnumerable < string > additionalDirectories )
166+ {
167+ // Looks for any resources aliases:
168+ // <item type="layout" name="">@layout/Page1</item>
169+ // <item type="layout" name="">@drawable/Page1</item>
170+ // and corrects the alias to be lower case.
171+ if ( elem . Name == "item" && ! string . IsNullOrEmpty ( elem . Value ) ) {
172+ string value = elem . Value . Trim ( ) ;
173+ Match m = r . Match ( value ) ;
174+ if ( m . Success ) {
175+ elem . Value = TryLowercaseValue ( elem . Value , resourceBasePath , additionalDirectories ) ;
176+ }
177+ }
178+ }
179+
180+ private static bool TryFixFragment ( XAttribute attr , Dictionary < string , string > acwMap )
181+ {
182+ // Looks for any:
183+ // <fragment class="My.DotNet.Class"
184+ // <fragment android:name="My.DotNet.Class" ...
185+ // and tries to change it to the ACW name
186+ if ( attr . Parent . Name != "fragment" )
187+ return false ;
188+
189+ if ( attr . Name == "class" || attr . Name == android + "name" ) {
190+ if ( acwMap . ContainsKey ( attr . Value ) ) {
191+ attr . Value = acwMap [ attr . Value ] ;
192+
193+ return true ;
194+ }
195+ }
196+
197+ return false ;
198+ }
199+
200+ private static bool TryFixResAuto ( XAttribute attr , Dictionary < string , string > acwMap )
201+ {
202+ if ( attr . Name . Namespace != res_auto )
203+ return false ;
204+ switch ( attr . Name . LocalName ) {
205+ case "rectLayout" :
206+ case "roundLayout" :
207+ attr . Value = attr . Value . ToLowerInvariant ( ) ;
208+ return true ;
209+ }
210+ return false ;
211+ }
212+
213+ private static bool TryFixCustomView ( XElement elem , Dictionary < string , string > acwMap )
214+ {
215+ // Looks for any <My.DotNet.Class ...
216+ // and tries to change it to the ACW name
217+ if ( acwMap . ContainsKey ( elem . Name . ToString ( ) ) ) {
218+ elem . Name = acwMap [ elem . Name . ToString ( ) ] ;
219+ return true ;
220+ }
221+
222+ return false ;
223+ }
224+
225+ private static bool TryFixCustomClassAttribute ( XAttribute attr , Dictionary < string , string > acwMap )
226+ {
227+ /* Some attributes reference a Java class name.
228+ * try to convert those like for TryFixCustomView
229+ */
230+ if ( attr . Name != ( res_auto + "layout_behavior" ) // For custom CoordinatorLayout behavior
231+ && ( attr . Parent . Name != "transition" || attr . Name . LocalName != "class" ) ) // For custom transitions
232+ return false ;
233+
234+ string mappedValue ;
235+ if ( ! acwMap . TryGetValue ( attr . Value , out mappedValue ) )
236+ return false ;
237+
238+ attr . Value = mappedValue ;
239+ return true ;
240+ }
241+
242+ private static string TryLowercaseValue ( string value , string resourceBasePath , IEnumerable < string > additionalDirectories )
243+ {
244+ int s = value . LastIndexOf ( '/' ) ;
245+ if ( s >= 0 ) {
246+ if ( ResourceNeedsToBeLowerCased ( value , resourceBasePath , additionalDirectories ) )
247+ return value . Substring ( 0 , s ) + "/" + value . Substring ( s + 1 ) . ToLowerInvariant ( ) ;
248+ }
249+ return value ;
250+ }
251+ }
252+ }
0 commit comments