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