Home | History | Annotate | Download | only in res
      1 package com.xtremelabs.robolectric.res;
      2 
      3 import static com.xtremelabs.robolectric.Robolectric.shadowOf;
      4 
      5 import com.xtremelabs.robolectric.Robolectric;
      6 import com.xtremelabs.robolectric.shadows.ShadowContextWrapper;
      7 import com.xtremelabs.robolectric.util.I18nException;
      8 import com.xtremelabs.robolectric.util.PropertiesHelper;
      9 
     10 import android.R;
     11 import android.content.Context;
     12 import android.graphics.drawable.AnimationDrawable;
     13 import android.graphics.drawable.ColorDrawable;
     14 import android.graphics.drawable.Drawable;
     15 import android.preference.PreferenceScreen;
     16 import android.text.TextUtils;
     17 import android.view.Menu;
     18 import android.view.View;
     19 import android.view.ViewGroup;
     20 
     21 import java.io.BufferedReader;
     22 import java.io.File;
     23 import java.io.FileFilter;
     24 import java.io.FileInputStream;
     25 import java.io.IOException;
     26 import java.io.InputStream;
     27 import java.io.InputStreamReader;
     28 import java.lang.reflect.Field;
     29 import java.util.HashSet;
     30 import java.util.Properties;
     31 import java.util.Set;
     32 
     33 public 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