1package com.xtremelabs.robolectric.res;
2
3import static com.xtremelabs.robolectric.Robolectric.shadowOf;
4
5import com.xtremelabs.robolectric.Robolectric;
6import com.xtremelabs.robolectric.shadows.ShadowContextWrapper;
7import com.xtremelabs.robolectric.util.I18nException;
8import com.xtremelabs.robolectric.util.PropertiesHelper;
9
10import android.R;
11import android.content.Context;
12import android.graphics.drawable.AnimationDrawable;
13import android.graphics.drawable.ColorDrawable;
14import android.graphics.drawable.Drawable;
15import android.preference.PreferenceScreen;
16import android.text.TextUtils;
17import android.view.Menu;
18import android.view.View;
19import android.view.ViewGroup;
20
21import java.io.BufferedReader;
22import java.io.File;
23import java.io.FileFilter;
24import java.io.FileInputStream;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.InputStreamReader;
28import java.lang.reflect.Field;
29import java.util.HashSet;
30import java.util.Properties;
31import java.util.Set;
32
33public class ResourceLoader {
34	private static final FileFilter MENU_DIR_FILE_FILTER = new FileFilter() {
35		@Override
36		public boolean accept( File file ) {
37			return isMenuDirectory( file.getPath() );
38		}
39	};
40	private static final FileFilter LAYOUT_DIR_FILE_FILTER = new FileFilter() {
41		@Override
42		public boolean accept( File file ) {
43			return isLayoutDirectory( file.getPath() );
44		}
45	};
46	private static final FileFilter DRAWABLE_DIR_FILE_FILTER = new FileFilter() {
47		@Override
48		public boolean accept( File file ) {
49			return isDrawableDirectory( file.getPath() );
50		}
51	};
52
53	private File resourceDir;
54	private File assetsDir;
55	private int sdkVersion;
56	private Class rClass;
57
58	private final ResourceExtractor resourceExtractor;
59	private ViewLoader viewLoader;
60	private MenuLoader menuLoader;
61	private PreferenceLoader preferenceLoader;
62	private final StringResourceLoader stringResourceLoader;
63	private final PluralResourceLoader pluralResourceLoader;
64	private final StringArrayResourceLoader stringArrayResourceLoader;
65	private final AttrResourceLoader attrResourceLoader;
66	private final ColorResourceLoader colorResourceLoader;
67	private final DrawableResourceLoader drawableResourceLoader;
68	private final RawResourceLoader rawResourceLoader;
69	private final DimenResourceLoader dimenResourceLoader;
70	private final IntegerResourceLoader integerResourceLoader;
71	private boolean isInitialized = false;
72	private boolean strictI18n = false;
73	private String locale="";
74
75	private final Set<Integer> ninePatchDrawableIds = new HashSet<Integer>();
76
77	public ResourceLoader(  int sdkVersion, Class rClass, File resourceDir, File assetsDir ) throws Exception {
78		this( sdkVersion, rClass, resourceDir, assetsDir, "");
79	}
80
81	public ResourceLoader( int sdkVersion, Class rClass, File resourceDir, File assetsDir, String locale ) throws Exception {
82		this.sdkVersion = sdkVersion;
83		this.assetsDir = assetsDir;
84		this.rClass = rClass;
85		this.locale = locale;
86
87		resourceExtractor = new ResourceExtractor();
88		if ( rClass != null ) {
89		  resourceExtractor.addLocalRClass( rClass );
90		}
91		resourceExtractor.addSystemRClass( R.class );
92
93		stringResourceLoader = new StringResourceLoader( resourceExtractor );
94		pluralResourceLoader = new PluralResourceLoader( resourceExtractor, stringResourceLoader );
95		stringArrayResourceLoader = new StringArrayResourceLoader( resourceExtractor, stringResourceLoader );
96		colorResourceLoader = new ColorResourceLoader( resourceExtractor );
97		attrResourceLoader = new AttrResourceLoader( resourceExtractor );
98		drawableResourceLoader = new DrawableResourceLoader( resourceExtractor, resourceDir );
99		rawResourceLoader = new RawResourceLoader( resourceExtractor, resourceDir );
100		dimenResourceLoader = new DimenResourceLoader( resourceExtractor );
101		integerResourceLoader = new IntegerResourceLoader( resourceExtractor );
102
103		this.resourceDir = resourceDir;
104	}
105
106	public void setStrictI18n( boolean strict ) {
107		this.strictI18n = strict;
108		if ( viewLoader != null ) {
109			viewLoader.setStrictI18n( strict );
110		}
111		if ( menuLoader != null ) {
112			menuLoader.setStrictI18n( strict );
113		}
114		if ( preferenceLoader != null ) {
115			preferenceLoader.setStrictI18n( strict );
116		}
117	}
118
119	public boolean getStrictI18n() {
120		return strictI18n;
121	}
122
123	private void init() {
124		if ( isInitialized ) {
125			return;
126		}
127
128		try {
129			if ( resourceDir != null ) {
130				viewLoader = new ViewLoader( resourceExtractor, attrResourceLoader );
131				menuLoader = new MenuLoader( resourceExtractor, attrResourceLoader );
132				preferenceLoader = new PreferenceLoader( resourceExtractor );
133
134				viewLoader.setStrictI18n( strictI18n );
135				menuLoader.setStrictI18n( strictI18n );
136				preferenceLoader.setStrictI18n( strictI18n );
137
138				File systemResourceDir = getSystemResourceDir( getPathToAndroidResources() );
139				File localValueResourceDir = getValueResourceDir( resourceDir );
140				File systemValueResourceDir = getValueResourceDir( systemResourceDir );
141				File preferenceDir = getPreferenceResourceDir( resourceDir );
142
143				loadStringResources( localValueResourceDir, systemValueResourceDir );
144				loadPluralsResources( localValueResourceDir, systemValueResourceDir );
145				loadValueResources( localValueResourceDir, systemValueResourceDir );
146				loadDimenResources( localValueResourceDir, systemValueResourceDir );
147				loadIntegerResource( localValueResourceDir, systemValueResourceDir );
148				loadViewResources( systemResourceDir, resourceDir );
149				loadMenuResources( resourceDir );
150				loadDrawableResources( resourceDir );
151				loadPreferenceResources( preferenceDir );
152
153				listNinePatchResources(ninePatchDrawableIds, resourceDir);
154			} else {
155				viewLoader = null;
156				menuLoader = null;
157				preferenceLoader = null;
158			}
159		} catch ( I18nException e ) {
160			throw e;
161		} catch ( Exception e ) {
162			throw new RuntimeException( e );
163		}
164		isInitialized = true;
165	}
166
167	private File getSystemResourceDir( String pathToAndroidResources ) {
168		return pathToAndroidResources != null ? new File( pathToAndroidResources ) : null;
169	}
170
171	private void loadStringResources( File localResourceDir, File systemValueResourceDir ) throws Exception {
172		DocumentLoader stringResourceDocumentLoader = new DocumentLoader( this.stringResourceLoader );
173		loadValueResourcesFromDirs( stringResourceDocumentLoader, localResourceDir, systemValueResourceDir );
174	}
175
176	private void loadPluralsResources( File localResourceDir, File systemValueResourceDir ) throws Exception {
177		DocumentLoader stringResourceDocumentLoader = new DocumentLoader( this.pluralResourceLoader );
178		loadValueResourcesFromDirs( stringResourceDocumentLoader, localResourceDir, systemValueResourceDir );
179	}
180
181	private void loadValueResources( File localResourceDir, File systemValueResourceDir ) throws Exception {
182		DocumentLoader valueResourceLoader = new DocumentLoader( stringArrayResourceLoader, colorResourceLoader,
183				attrResourceLoader );
184		loadValueResourcesFromDirs( valueResourceLoader, localResourceDir, systemValueResourceDir );
185	}
186
187	private void loadDimenResources( File localResourceDir, File systemValueResourceDir ) throws Exception {
188		DocumentLoader dimenResourceDocumentLoader = new DocumentLoader( this.dimenResourceLoader );
189		loadValueResourcesFromDirs( dimenResourceDocumentLoader, localResourceDir, systemValueResourceDir );
190	}
191
192	private void loadIntegerResource( File localResourceDir, File systemValueResourceDir ) throws Exception {
193		DocumentLoader integerResourceDocumentLoader = new DocumentLoader( this.integerResourceLoader );
194		loadValueResourcesFromDirs( integerResourceDocumentLoader, localResourceDir, systemValueResourceDir );
195	}
196
197	private void loadViewResources( File systemResourceDir, File xmlResourceDir ) throws Exception {
198		DocumentLoader viewDocumentLoader = new DocumentLoader( viewLoader );
199		loadLayoutResourceXmlSubDirs( viewDocumentLoader, xmlResourceDir, false );
200		loadLayoutResourceXmlSubDirs( viewDocumentLoader, systemResourceDir, true );
201	}
202
203	private void loadMenuResources( File xmlResourceDir ) throws Exception {
204		DocumentLoader menuDocumentLoader = new DocumentLoader( menuLoader );
205		loadMenuResourceXmlDirs( menuDocumentLoader, xmlResourceDir );
206	}
207
208	private void loadDrawableResources( File xmlResourceDir ) throws Exception {
209		DocumentLoader drawableDocumentLoader = new DocumentLoader( drawableResourceLoader );
210		loadDrawableResourceXmlDirs( drawableDocumentLoader, xmlResourceDir );
211	}
212
213	private void loadPreferenceResources( File xmlResourceDir ) throws Exception {
214		if ( xmlResourceDir.exists() ) {
215			DocumentLoader preferenceDocumentLoader = new DocumentLoader( preferenceLoader );
216			preferenceDocumentLoader.loadResourceXmlDir( xmlResourceDir );
217		}
218	}
219
220	private void loadLayoutResourceXmlSubDirs( DocumentLoader layoutDocumentLoader, File xmlResourceDir, boolean isSystem )
221			throws Exception {
222		if ( xmlResourceDir != null ) {
223			layoutDocumentLoader.loadResourceXmlDirs( isSystem, xmlResourceDir.listFiles( LAYOUT_DIR_FILE_FILTER ) );
224		}
225	}
226
227	private void loadMenuResourceXmlDirs( DocumentLoader menuDocumentLoader, File xmlResourceDir ) throws Exception {
228		if ( xmlResourceDir != null ) {
229			menuDocumentLoader.loadResourceXmlDirs( xmlResourceDir.listFiles( MENU_DIR_FILE_FILTER ) );
230		}
231	}
232
233	private void loadDrawableResourceXmlDirs( DocumentLoader drawableResourceLoader, File xmlResourceDir ) throws Exception {
234		if ( xmlResourceDir != null ) {
235			drawableResourceLoader.loadResourceXmlDirs( xmlResourceDir.listFiles( DRAWABLE_DIR_FILE_FILTER ) );
236		}
237	}
238
239	private void loadValueResourcesFromDirs( DocumentLoader documentLoader, File localValueResourceDir,
240			File systemValueResourceDir ) throws Exception {
241		loadValueResourcesFromDir( documentLoader, localValueResourceDir );
242		loadSystemResourceXmlDir( documentLoader, systemValueResourceDir );
243	}
244
245	private void loadValueResourcesFromDir( DocumentLoader documentloader, File xmlResourceDir ) throws Exception {
246		if ( xmlResourceDir != null ) {
247			documentloader.loadResourceXmlDir( xmlResourceDir );
248		}
249	}
250
251	private void loadSystemResourceXmlDir( DocumentLoader documentLoader, File stringResourceDir ) throws Exception {
252		if ( stringResourceDir != null ) {
253			documentLoader.loadSystemResourceXmlDir( stringResourceDir );
254		}
255	}
256
257	private File getValueResourceDir( File xmlResourceDir ) {
258		String valuesDir = "values";
259		if( !TextUtils.isEmpty( locale ) ){
260			valuesDir += "-"+ locale;
261		}
262		File result = ( xmlResourceDir != null ) ? new File( xmlResourceDir, valuesDir ) : null;
263		if( result != null && !result.exists() ){
264			throw new RuntimeException("Couldn't find value resource directory: " + result.getAbsolutePath() );
265		}
266		return result;
267	}
268
269	private File getPreferenceResourceDir( File xmlResourceDir ) {
270		return xmlResourceDir != null ? new File( xmlResourceDir, "xml" ) : null;
271	}
272
273	private String getPathToAndroidResources() {
274		String resFolder = getAndroidResourcePathFromLocalProperties();
275		if (resFolder == null) {
276			resFolder = getAndroidResourcePathFromSystemEnvironment();
277			if (resFolder == null) {
278				resFolder = getAndroidResourcePathFromSystemProperty();
279				if (resFolder == null) {
280					resFolder = getAndroidResourcePathByExecingWhichAndroid();
281				}
282			}
283		}
284
285		// Go through last 5 sdk versions looking for resource folders.
286		if (resFolder != null) {
287			for (int i = sdkVersion; i >= sdkVersion - 5 && i >= 4; i--) {
288				File resourcePath = new File(resFolder, getAndroidResourceSubPath(i));
289				if (resourcePath.exists()) {
290					return resourcePath.getAbsolutePath();
291				} else {
292					System.out.println("WARNING: Unable to find Android resources at: " +
293							resourcePath.toString() + " continuing.");
294				}
295			}
296		} else {
297			System.out.println("WARNING: Unable to find path to Android SDK");
298		}
299
300		return null;
301	}
302
303	private String getAndroidResourcePathFromLocalProperties() {
304		// Hand tested
305		// This is the path most often taken by IntelliJ
306		File rootDir = resourceDir.getParentFile();
307		String localPropertiesFileName = "local.properties";
308		File localPropertiesFile = new File( rootDir, localPropertiesFileName );
309		if ( !localPropertiesFile.exists() ) {
310			localPropertiesFile = new File( localPropertiesFileName );
311		}
312		if ( localPropertiesFile.exists() ) {
313			Properties localProperties = new Properties();
314			try {
315				localProperties.load( new FileInputStream( localPropertiesFile ) );
316				PropertiesHelper.doSubstitutions( localProperties );
317				return localProperties.getProperty( "sdk.dir" );
318			} catch ( IOException e ) {
319				// fine, we'll try something else
320			}
321		}
322		return null;
323	}
324
325	private String getAndroidResourcePathFromSystemEnvironment() {
326		// Hand tested
327		return System.getenv().get( "ANDROID_HOME" );
328	}
329
330	private String getAndroidResourcePathFromSystemProperty() {
331		// this is used by the android-maven-plugin
332		return System.getProperty( "android.sdk.path" );
333	}
334
335	private String getAndroidResourcePathByExecingWhichAndroid() {
336		// Hand tested
337		// Should always work from the command line. Often fails in IDEs because
338		// they don't pass the full PATH in the environment
339		try {
340			Process process = Runtime.getRuntime().exec( new String[] { "which", "android" } );
341			String sdkPath = new BufferedReader( new InputStreamReader( process.getInputStream() ) ).readLine();
342			if ( sdkPath != null && sdkPath.endsWith( "tools/android" ) ) {
343			    return sdkPath.substring(0, sdkPath.indexOf( "tools/android"));
344			}
345		} catch ( IOException e ) {
346			// fine we'll try something else
347		}
348		return null;
349	}
350
351	private static String getAndroidResourceSubPath(int version) {
352		return "platforms/android-" + version + "/data/res";
353	}
354
355	static boolean isLayoutDirectory( String path ) {
356		return path.contains( File.separator + "layout" );
357	}
358
359	static boolean isDrawableDirectory( String path ) {
360		return path.contains( File.separator + "drawable" );
361	}
362
363	static boolean isMenuDirectory( String path ) {
364		return path.contains( File.separator + "menu" );
365	}
366
367	/*
368	 * For tests only...
369	 */
370	protected ResourceLoader( StringResourceLoader stringResourceLoader ) {
371		resourceExtractor = new ResourceExtractor();
372		this.stringResourceLoader = stringResourceLoader;
373		pluralResourceLoader = null;
374		viewLoader = null;
375		stringArrayResourceLoader = null;
376		attrResourceLoader = null;
377		colorResourceLoader = null;
378		drawableResourceLoader = null;
379		rawResourceLoader = null;
380		dimenResourceLoader = null;
381		integerResourceLoader = null;
382	}
383
384	public static ResourceLoader getFrom( Context context ) {
385		ResourceLoader resourceLoader = shadowOf( context.getApplicationContext() ).getResourceLoader();
386		resourceLoader.init();
387		return resourceLoader;
388	}
389
390	public String getNameForId( int viewId ) {
391		init();
392		return resourceExtractor.getResourceName( viewId );
393	}
394
395	public View inflateView( Context context, int resource, ViewGroup viewGroup ) {
396		init();
397		return viewLoader.inflateView( context, resource, viewGroup );
398	}
399
400	public int getColorValue( int id ) {
401		init();
402		return colorResourceLoader.getValue( id );
403	}
404
405	public String getStringValue( int id ) {
406		init();
407		return stringResourceLoader.getValue( id );
408	}
409
410	public String getPluralStringValue( int id, int quantity ) {
411		init();
412		return pluralResourceLoader.getValue( id, quantity );
413	}
414
415	public float getDimenValue( int id ) {
416		init();
417		return dimenResourceLoader.getValue( id );
418	}
419
420	public int getIntegerValue( int id ) {
421		init();
422		return integerResourceLoader.getValue( id );
423	}
424
425	public boolean isDrawableXml( int resourceId ) {
426		init();
427		return drawableResourceLoader.isXml( resourceId );
428	}
429
430    public boolean isAnimatableXml( int resourceId ) {
431        init();
432        return drawableResourceLoader.isAnimationDrawable( resourceId );
433    }
434
435	public int[] getDrawableIds( int resourceId ) {
436		init();
437		return drawableResourceLoader.getDrawableIds( resourceId );
438	}
439
440	public Drawable getXmlDrawable( int resourceId ) {
441		return drawableResourceLoader.getXmlDrawable( resourceId );
442	}
443
444	public Drawable getAnimDrawable( int resourceId ) {
445		return getInnerRClassDrawable( resourceId, "$anim", AnimationDrawable.class );
446	}
447
448	public Drawable getColorDrawable( int resourceId ) {
449		return getInnerRClassDrawable( resourceId, "$color", ColorDrawable.class );
450	}
451
452	@SuppressWarnings("rawtypes")
453	private Drawable getInnerRClassDrawable( int drawableResourceId, String suffix, Class returnClass ) {
454		ShadowContextWrapper shadowApp = Robolectric.shadowOf( Robolectric.application );
455		Class rClass = shadowApp.getResourceLoader().getLocalRClass();
456
457		// Check to make sure there is actually an R Class, if not
458		// return just a BitmapDrawable
459		if ( rClass == null ) {
460			return null;
461		}
462
463		// Load the Inner Class for interrogation
464		Class animClass = null;
465		try {
466			animClass = Class.forName( rClass.getCanonicalName() + suffix );
467		} catch ( ClassNotFoundException e ) {
468			return null;
469		}
470
471		// Try to find the passed in resource ID
472		try {
473			for ( Field field : animClass.getDeclaredFields() ) {
474				if ( field.getInt( animClass ) == drawableResourceId ) {
475					return ( Drawable ) returnClass.newInstance();
476				}
477			}
478		} catch ( Exception e ) {
479		}
480
481		return null;
482	}
483
484	public boolean isNinePatchDrawable(int drawableResourceId) {
485		return ninePatchDrawableIds.contains(drawableResourceId);
486	}
487
488	/**
489	 * Returns a collection of resource IDs for all nine-patch drawables
490	 * in the project.
491	 *
492	 * @param resourceIds
493	 * @param dir
494	 */
495	private void listNinePatchResources(Set<Integer> resourceIds, File dir) {
496		File[] files = dir.listFiles();
497		if (files != null) {
498			for (File f : files) {
499				if (f.isDirectory() && isDrawableDirectory(f.getPath())) {
500					listNinePatchResources(resourceIds, f);
501				} else {
502					String name = f.getName();
503					if (name.endsWith(".9.png")) {
504						String[] tokens = name.split("\\.9\\.png$");
505						resourceIds.add(resourceExtractor.getResourceId("@drawable/" + tokens[0]));
506					}
507				}
508			}
509		}
510	}
511
512	public InputStream getRawValue( int id ) {
513		init();
514		return rawResourceLoader.getValue( id );
515	}
516
517	public String[] getStringArrayValue( int id ) {
518		init();
519		return stringArrayResourceLoader.getArrayValue( id );
520	}
521
522	public void inflateMenu( Context context, int resource, Menu root ) {
523		init();
524		menuLoader.inflateMenu( context, resource, root );
525	}
526
527	public PreferenceScreen inflatePreferences( Context context, int resourceId ) {
528		init();
529		return preferenceLoader.inflatePreferences( context, resourceId );
530	}
531
532	public File getAssetsBase() {
533		return assetsDir;
534	}
535
536	@SuppressWarnings("rawtypes")
537	public Class getLocalRClass() {
538		return rClass;
539	}
540
541	public void setLocalRClass( Class clazz ) {
542		rClass = clazz;
543	}
544
545	public ResourceExtractor getResourceExtractor() {
546		return resourceExtractor;
547	}
548
549	public ViewLoader.ViewNode getLayoutViewNode( String layoutName ) {
550		return viewLoader.viewNodesByLayoutName.get( layoutName );
551	}
552
553	public void setLayoutQualifierSearchPath( String... locations ) {
554		init();
555		viewLoader.setLayoutQualifierSearchPath( locations );
556	}
557}
558